diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/attribute/LazyAttributeValues.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/attribute/LazyAttributeValues.java index e70b9130254a..a4a4de09fe07 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/attribute/LazyAttributeValues.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/attribute/LazyAttributeValues.java @@ -53,6 +53,7 @@ import org.hisp.dhis.jsontree.JsonArray; import org.hisp.dhis.jsontree.JsonMixed; import org.hisp.dhis.jsontree.JsonObject; +import org.hisp.dhis.jsontree.JsonString; import org.hisp.dhis.jsontree.JsonValue; import org.intellij.lang.annotations.Language; @@ -148,7 +149,7 @@ private static TreeMap parseObjectJson(JsonObject map) { .collect( Collectors.toMap( Map.Entry::getKey, - e -> e.getValue().asObject().getString("value").string(), + e -> parseValue(e.getValue().asObject().get("value")), (a, b) -> a, TreeMap::new)); } @@ -160,11 +161,21 @@ private static TreeMap parseArrayJson(JsonArray arr) { .collect( Collectors.toMap( obj -> obj.getObject("attribute").getString("id").string(), - obj -> obj.getString("value").string(), + obj -> parseValue(obj.get("value")), (a, b) -> a, TreeMap::new)); } + @Nonnull + private static String parseValue(JsonValue value) { + if (value.isUndefined()) return ""; + return switch (value.type()) { + case NULL -> ""; + case STRING -> value.as(JsonString.class).string(); + default -> value.toJson(); + }; + } + private void init(TreeMap from) { keys = new String[from.size()]; values = new String[keys.length]; diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryCombo.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryCombo.java index 0660842aede8..1e44fb1d2afb 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryCombo.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryCombo.java @@ -34,10 +34,12 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; import java.util.ArrayList; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.stream.Collectors; +import javax.annotation.Nonnull; import org.hisp.dhis.common.BaseIdentifiableObject; import org.hisp.dhis.common.CombinationGenerator; import org.hisp.dhis.common.DataDimensionType; @@ -223,6 +225,18 @@ public void removeAllCategories() { categories.clear(); } + public void addCategoryOptionCombo(@Nonnull CategoryOptionCombo coc) { + this.getOptionCombos().add(coc); + } + + public void removeCategoryOptionCombo(@Nonnull CategoryOptionCombo coc) { + this.getOptionCombos().remove(coc); + } + + public void removeCategoryOptionCombos(@Nonnull Collection cocs) { + cocs.forEach(this::removeCategoryOptionCombo); + } + // ------------------------------------------------------------------------- // Getters and setters // ------------------------------------------------------------------------- diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryComboStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryComboStore.java index c21d38bc9b40..1eb0aa76c45d 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryComboStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryComboStore.java @@ -27,13 +27,25 @@ */ package org.hisp.dhis.category; +import java.util.Collection; import java.util.List; +import javax.annotation.Nonnull; import org.hisp.dhis.common.DataDimensionType; import org.hisp.dhis.common.IdentifiableObjectStore; +import org.hisp.dhis.common.UID; /** * @author Lars Helge Overland */ public interface CategoryComboStore extends IdentifiableObjectStore { List getCategoryCombosByDimensionType(DataDimensionType dataDimensionType); + + /** + * Retrieve all {@link CategoryCombo}s with {@link CategoryOptionCombo} {@link UID}s + * + * @param uids {@link CategoryOptionCombo} {@link UID}s + * @return {@link CategoryCombo}s with references to {@link CategoryOptionCombo} {@link UID}s + * passed in + */ + List getByCategoryOptionCombo(@Nonnull Collection uids); } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionStore.java index 48e546efa7cc..63097c630f88 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionStore.java @@ -27,8 +27,10 @@ */ package org.hisp.dhis.category; +import java.util.Collection; import java.util.List; import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; import org.hisp.dhis.common.IdentifiableObjectStore; import org.hisp.dhis.common.UID; import org.hisp.dhis.user.UserDetails; @@ -47,4 +49,13 @@ public interface CategoryOptionStore extends IdentifiableObjectStore getCategoryOptions(Category category); List getDataWriteCategoryOptions(Category category, UserDetails userDetails); + + /** + * Retrieve all {@link CategoryOption}s with {@link CategoryOptionCombo} {@link UID}s + * + * @param uids {@link CategoryOptionCombo} {@link UID}s + * @return {@link CategoryOption}s with references to {@link CategoryOptionCombo} {@link UID}s + * passed in + */ + List getByCategoryOptionCombo(@Nonnull Collection uids); } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryService.java index 01286d776196..260961bd8d79 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryService.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryService.java @@ -30,6 +30,7 @@ import java.util.Collection; import java.util.List; import java.util.Set; +import javax.annotation.Nonnull; import org.apache.commons.collections4.SetValuedMap; import org.hisp.dhis.common.IdScheme; import org.hisp.dhis.common.UID; @@ -110,13 +111,6 @@ public interface CategoryService { */ Category getCategoryByName(String name, UserDetails userDetails); - /** - * Returns all DataElementCategories. - * - * @return a list of all DataElementCategories. - */ - List getAllDataElementCategories(); - /** * Retrieves all DataElementCategories of dimension type disaggregation. * @@ -330,24 +324,6 @@ SetValuedMap getCategoryOptionOrganisationUnitsAssociations( */ List getAttributeCategoryCombos(); - /** - * Validates the category combo. Possible return values are: - * - *

- * - *

    - *
  • category_combo_is_null - *
  • category_combo_must_have_at_least_one_category - *
  • category_combo_cannot_have_duplicate_categories - *
  • categories_must_have_at_least_one_category_option - *
  • categories_cannot_share_category_options - *
- * - * @param categoryCombo the category combo to validate. - * @return null if valid, non-empty string if invalid. - */ - String validateCategoryCombo(CategoryCombo categoryCombo); - // ------------------------------------------------------------------------- // CategoryOptionCombo // ------------------------------------------------------------------------- @@ -478,7 +454,15 @@ CategoryOptionCombo getCategoryOptionCombo( * @return categoryOptionCombos with refs to categoryOptions */ List getCategoryOptionCombosByCategoryOption( - Collection categoryOptions); + @Nonnull Collection categoryOptions); + + /** + * Retrieves all CategoryOptionCombos by {@link UID}. + * + * @param uids {@link UID}s to search for + * @return categoryOptionCombos with refs to {@link UID}s + */ + List getCategoryOptionCombosByUid(@Nonnull Collection uids); // ------------------------------------------------------------------------- // DataElementOperand diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseIdentifiableObject.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseIdentifiableObject.java index 6eb4166a55c0..49dc780989af 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseIdentifiableObject.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseIdentifiableObject.java @@ -49,6 +49,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiFunction; import java.util.stream.Stream; +import javax.annotation.Nonnull; +import lombok.Setter; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.hibernate.annotations.Immutable; @@ -83,22 +85,22 @@ @JacksonXmlRootElement(localName = "identifiableObject", namespace = DxfNamespaces.DXF_2_0) public class BaseIdentifiableObject extends BaseLinkableObject implements IdentifiableObject { /** The database internal identifier for this Object. */ - protected long id; + @Setter protected long id; /** The unique identifier for this object. */ - @AuditAttribute protected String uid; + @Setter @AuditAttribute protected String uid; /** The unique code for this object. */ - @AuditAttribute protected String code; + @Setter @AuditAttribute protected String code; /** The name of this object. Required and unique. */ - protected String name; + @Setter protected String name; /** The date this object was created. */ - protected Date created; + @Setter protected Date created; /** The date this object was last updated. */ - protected Date lastUpdated; + @Setter protected Date lastUpdated; /** Set of the dynamic attributes values that belong to this data element. */ @AuditAttribute private AttributeValues attributeValues = AttributeValues.empty(); @@ -110,7 +112,7 @@ public class BaseIdentifiableObject extends BaseLinkableObject implements Identi * Cache for object translations, where the cache key is a combination of locale and translation * property, and value is the translated value. */ - private Map translationCache = new ConcurrentHashMap<>(); + private final Map translationCache = new ConcurrentHashMap<>(); /** User who created this object. This field is immutable and must not be updated. */ @Immutable protected User createdBy; @@ -119,13 +121,13 @@ public class BaseIdentifiableObject extends BaseLinkableObject implements Identi protected transient Access access; /** Users who have marked this object as a favorite. */ - protected Set favorites = new HashSet<>(); + @Setter protected Set favorites = new HashSet<>(); /** Last user updated this object. */ - protected User lastUpdatedBy; + @Setter protected User lastUpdatedBy; /** Object sharing (JSONB). */ - protected Sharing sharing = new Sharing(); + @Setter protected Sharing sharing = new Sharing(); // ------------------------------------------------------------------------- // Constructors @@ -162,7 +164,7 @@ public BaseIdentifiableObject(IdentifiableObject identifiableObject) { * name. */ @Override - public int compareTo(IdentifiableObject object) { + public int compareTo(@Nonnull IdentifiableObject object) { if (this.getDisplayName() == null) { return object.getDisplayName() == null ? 0 : 1; } @@ -182,10 +184,6 @@ public long getId() { return id; } - public void setId(long id) { - this.id = id; - } - @Override @JsonProperty(value = "id") @JacksonXmlProperty(localName = "id", isAttribute = true) @@ -196,10 +194,6 @@ public String getUid() { return uid; } - public void setUid(String uid) { - this.uid = uid; - } - @Override @JsonProperty @JacksonXmlProperty(isAttribute = true) @@ -209,10 +203,6 @@ public String getCode() { return code; } - public void setCode(String code) { - this.code = code; - } - @Override @JsonProperty @JacksonXmlProperty(isAttribute = true) @@ -222,11 +212,8 @@ public String getName() { return name; } - public void setName(String name) { - this.name = name; - } - @Override + @Sortable(whenPersisted = false) @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) @Translatable(propertyName = "name", key = "NAME") @@ -243,10 +230,6 @@ public Date getCreated() { return created; } - public void setCreated(Date created) { - this.created = created; - } - @Override @OpenApi.Property(UserPropertyTransformer.UserDto.class) @JsonProperty @@ -258,10 +241,6 @@ public User getLastUpdatedBy() { return lastUpdatedBy; } - public void setLastUpdatedBy(User lastUpdatedBy) { - this.lastUpdatedBy = lastUpdatedBy; - } - @Override @JsonProperty @JacksonXmlProperty(isAttribute = true) @@ -271,10 +250,6 @@ public Date getLastUpdated() { return lastUpdated; } - public void setLastUpdated(Date lastUpdated) { - this.lastUpdated = lastUpdated; - } - public record AttributeValue(@JsonProperty Attribute attribute, @JsonProperty String value) {} @Override @@ -308,6 +283,7 @@ public String getAttributeValue(String attributeUid) { @Gist(included = Include.FALSE) @Override + @Sortable(value = false) @JsonProperty @JacksonXmlElementWrapper(localName = "translations", namespace = DxfNamespaces.DXF_2_0) @JacksonXmlProperty(localName = "translation", namespace = DxfNamespaces.DXF_2_0) @@ -387,8 +363,9 @@ public void setOwner(String userId) { } @Override + @Sortable(value = false) @Gist(included = Include.FALSE) - @JsonProperty + @JsonProperty(access = JsonProperty.Access.READ_ONLY) @JacksonXmlProperty(localName = "access", namespace = DxfNamespaces.DXF_2_0) public Access getAccess() { return access; @@ -407,10 +384,6 @@ public Set getFavorites() { return favorites; } - public void setFavorites(Set favorites) { - this.favorites = favorites; - } - @Override @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) @@ -422,6 +395,7 @@ public boolean isFavorite() { } @Override + @Sortable(value = false) @Gist(included = Include.FALSE) @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) @@ -433,10 +407,6 @@ public Sharing getSharing() { return sharing; } - public void setSharing(Sharing sharing) { - this.sharing = sharing; - } - @Override public boolean setAsFavorite(UserDetails user) { if (this.favorites == null) { diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseLinkableObject.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseLinkableObject.java index 382a88fc72e3..df59ee82f663 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseLinkableObject.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseLinkableObject.java @@ -45,7 +45,8 @@ public class BaseLinkableObject implements LinkableObject { private transient String href; @Override - @JsonProperty + @Sortable(value = false) + @JsonProperty(access = JsonProperty.Access.READ_ONLY) @JacksonXmlProperty(isAttribute = true) @Property(PropertyType.URL) public String getHref() { diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseNameableObject.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseNameableObject.java index fbc0c690596a..33af9314ccb5 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseNameableObject.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/BaseNameableObject.java @@ -166,6 +166,7 @@ public String toString() { // ------------------------------------------------------------------------- @Override + @Sortable @JsonProperty @JacksonXmlProperty(isAttribute = true) @PropertyRange(min = 1) @@ -178,6 +179,7 @@ public void setShortName(String shortName) { } @Override + @Sortable(whenPersisted = false) @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) @Translatable(propertyName = "shortName", key = "SHORT_NAME") @@ -186,6 +188,7 @@ public String getDisplayShortName() { } @Override + @Sortable @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) @PropertyRange(min = 1) @@ -198,6 +201,7 @@ public void setDescription(String description) { } @Override + @Sortable(value = false) @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) @Translatable(propertyName = "description", key = "DESCRIPTION") @@ -206,6 +210,7 @@ public String getDisplayDescription() { } @JsonProperty + @Sortable(whenPersisted = false) @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) @Translatable(propertyName = "formName", key = "FORM_NAME") public String getDisplayFormName() { @@ -217,6 +222,7 @@ public String getFormNameFallback() { return formName != null && !formName.isEmpty() ? getFormName() : getDisplayName(); } + @Sortable @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public String getFormName() { diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/EventDataQueryRequest.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/EventDataQueryRequest.java index fbcdfd79dccb..8362fdf61e17 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/EventDataQueryRequest.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/EventDataQueryRequest.java @@ -40,9 +40,11 @@ import java.util.Set; import java.util.regex.Pattern; import java.util.stream.Collectors; +import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import org.hisp.dhis.analytics.AggregationType; import org.hisp.dhis.analytics.AnalyticsMetaDataKey; import org.hisp.dhis.analytics.EventOutputType; @@ -52,8 +54,9 @@ import org.hisp.dhis.event.EventStatus; import org.hisp.dhis.program.EnrollmentStatus; -@Builder @Getter +@Builder +@NoArgsConstructor(access = AccessLevel.PRIVATE) @AllArgsConstructor public class EventDataQueryRequest { private String program; diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/RequestTypeAware.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/RequestTypeAware.java index 722476f2d3d6..b8816f089ed7 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/RequestTypeAware.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/RequestTypeAware.java @@ -33,10 +33,7 @@ import lombok.Getter; -/** - * Encapsulates some information about the current request and endpoint invoked. They are needed - * because of some internal rules. - */ +/** Encapsulates some information about the current request and endpoint invoked. */ public class RequestTypeAware { @Getter private EndpointAction endpointAction = OTHER; diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/Sortable.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/Sortable.java new file mode 100644 index 000000000000..4ed4854e0479 --- /dev/null +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/Sortable.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.common; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface Sortable { + + boolean value() default true; + + boolean whenPersisted() default true; +} diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalAuditStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalAuditStore.java index 1a125f3881a5..625d909a1ac7 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalAuditStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalAuditStore.java @@ -28,6 +28,7 @@ package org.hisp.dhis.dataapproval; import java.util.List; +import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.common.GenericStore; import org.hisp.dhis.organisationunit.OrganisationUnit; @@ -46,6 +47,13 @@ public interface DataApprovalAuditStore extends GenericStore */ void deleteDataApprovalAudits(OrganisationUnit organisationUnit); + /** + * Deletes DataApprovalAudits for the given category option combo. + * + * @param coc the category option combo + */ + void deleteDataApprovalAudits(CategoryOptionCombo coc); + /** * Returns DataApprovalAudit objects for query parameters. * diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalStore.java index ff845f2ee79c..fb84b7bad794 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataapproval/DataApprovalStore.java @@ -31,8 +31,10 @@ import java.util.List; import java.util.Map; import java.util.Set; +import javax.annotation.Nonnull; import org.hisp.dhis.category.CategoryCombo; import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.common.UID; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.period.Period; @@ -162,4 +164,19 @@ List getDataApprovalStatuses( Set attributeOptionCombos, List userApprovalLevels, Map levelMap); + + /** + * Retrieve all {@link DataApproval}s with {@link CategoryOptionCombo} {@link UID}s + * + * @param uids {@link CategoryOptionCombo} {@link UID}s + * @return {@link DataApproval}s with {@link CategoryOptionCombo} {@link UID}s passed in + */ + List getByCategoryOptionCombo(@Nonnull Collection uids); + + /** + * Delete all {@link DataApproval}s with references to {@link CategoryOptionCombo} {@link UID}s + * + * @param uids {@link CategoryOptionCombo} {@link UID}s + */ + void deleteByCategoryOptionCombo(@Nonnull Collection uids); } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataelement/DataElementOperandStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataelement/DataElementOperandStore.java index 554a7283bf92..1014f8139676 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataelement/DataElementOperandStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataelement/DataElementOperandStore.java @@ -29,7 +29,10 @@ import java.util.Collection; import java.util.List; +import javax.annotation.Nonnull; +import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.common.IdentifiableObjectStore; +import org.hisp.dhis.common.UID; /** * @author Morten Olav Hansen @@ -37,5 +40,20 @@ public interface DataElementOperandStore extends IdentifiableObjectStore { String ID = DataElementOperand.class.getName(); + /** + * Retrieve all {@link DataElementOperand}s with {@link DataElement}s + * + * @param dataElements {@link DataElement}s + * @return {@link DataElementOperand}s with references to {@link DataElement}s passed in + */ List getByDataElement(Collection dataElements); + + /** + * Retrieve all {@link DataElementOperand}s with {@link CategoryOptionCombo} {@link UID}s + * + * @param uids {@link CategoryOptionCombo} {@link UID}s + * @return {@link DataElementOperand}s with references to {@link CategoryOptionCombo} {@link UID}s + * passed in + */ + List getByCategoryOptionCombo(@Nonnull Collection uids); } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataelement/DataElementService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataelement/DataElementService.java index 9d05b329c1a5..7dd807f2bc38 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataelement/DataElementService.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataelement/DataElementService.java @@ -30,12 +30,8 @@ import java.util.Collection; import java.util.List; import javax.annotation.Nonnull; -import org.hisp.dhis.category.CategoryCombo; import org.hisp.dhis.common.IllegalQueryException; -import org.hisp.dhis.common.UID; -import org.hisp.dhis.common.ValueType; import org.hisp.dhis.hierarchy.HierarchyViolationException; -import org.hisp.dhis.period.PeriodType; /** * Defines service functionality for DataElements and DataElementGroups. @@ -110,39 +106,6 @@ public interface DataElementService { */ List getAllDataElements(); - /** - * Returns all DataElements of a given type. - * - * @param valueType the value type restriction - * @return a list of all DataElements with the given value type, or an empty list if there are no - * DataElements. - */ - List getAllDataElementsByValueType(ValueType valueType); - - /** - * Returns all DataElements with the given domain type. - * - * @param domainType the DataElementDomainType. - * @return all DataElements with the given domainType. - */ - List getDataElementsByDomainType(DataElementDomain domainType); - - /** - * Returns the DataElements with the given PeriodType. - * - * @param periodType the PeriodType. - * @return a list of DataElements. - */ - List getDataElementsByPeriodType(PeriodType periodType); - - /** - * Returns all DataElements with the given category combo. - * - * @param categoryCombo the CategoryCombo. - * @return all DataElements with the given category combo. - */ - List getDataElementByCategoryCombo(CategoryCombo categoryCombo); - /** * Returns all DataElements which are not member of any DataElementGroups. * @@ -246,14 +209,6 @@ public interface DataElementService { */ List getAllDataElementGroups(); - /** - * Returns all DataElements which zeroIsSignificant property is true or false. - * - * @param zeroIsSignificant whether zero is significant is true for this query. - * @return a collection of DataElements. - */ - List getDataElementsByZeroIsSignificant(boolean zeroIsSignificant); - // ------------------------------------------------------------------------- // DataElementGroupSet // ------------------------------------------------------------------------- @@ -267,14 +222,4 @@ public interface DataElementService { DataElementGroupSet getDataElementGroupSet(long id); DataElementGroupSet getDataElementGroupSet(String uid); - - DataElementGroupSet getDataElementGroupSetByName(String name); - - List getAllDataElementGroupSets(); - - List getByAttributeAndValue(UID attribute, String value); - - List getByAttribute(UID attribute); - - DataElement getByUniqueAttributeValue(UID attribute, String value); } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataelement/DataElementStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataelement/DataElementStore.java index d82afe928b27..8065ad0a3f81 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataelement/DataElementStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataelement/DataElementStore.java @@ -30,7 +30,6 @@ import java.util.List; import org.hisp.dhis.category.CategoryCombo; import org.hisp.dhis.common.GenericDimensionalObjectStore; -import org.hisp.dhis.common.ValueType; import org.hisp.dhis.user.User; /** @@ -53,30 +52,6 @@ public interface DataElementStore extends GenericDimensionalObjectStore getDataElementByCategoryCombo(CategoryCombo categoryCombo); - /** - * Returns all DataElement which zeroIsSignificant property is true or false - * - * @param zeroIsSignificant is zeroIsSignificant property - * @return a collection of all DataElement - */ - List getDataElementsByZeroIsSignificant(boolean zeroIsSignificant); - - /** - * Returns all DataElements of the given domain type. - * - * @param domainType the domain type. - * @return all DataElements of the given domain type. - */ - List getDataElementsByDomainType(DataElementDomain domainType); - - /** - * Returns all DataElements of the given value type. - * - * @param valueType the value type. - * @return all DataElements of the given value type. - */ - List getDataElementsByValueType(ValueType valueType); - /** * Returns all DataElements which are not member of any DataElementGroups. * diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/CompleteDataSetRegistration.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/CompleteDataSetRegistration.java index c24d9e4f3fb0..bc1624f4dd9d 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/CompleteDataSetRegistration.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/CompleteDataSetRegistration.java @@ -35,6 +35,7 @@ import com.google.common.base.MoreObjects; import java.io.Serializable; import java.util.Date; +import javax.annotation.Nonnull; import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.common.BaseIdentifiableObject; import org.hisp.dhis.common.DxfNamespaces; @@ -323,4 +324,28 @@ public String toString() { .add("isCompleted", completed) .toString(); } + + /** + * Creates a copy of the passed in CompleteDataSetRegistration, using all old values except for + * attributeOptionCombo, which uses the param attributeOptionCombo passed in. + * + * @param old old CompleteDataSetRegistration to use values from + * @param attributeOptionCombo attributeOptionCombo to use as new value in new + * CompleteDataSetRegistration + * @return copy of old CompleteDataSetRegistration except with a new attributeOptionCombo + */ + public static CompleteDataSetRegistration copyWithNewAttributeOptionCombo( + @Nonnull CompleteDataSetRegistration old, @Nonnull CategoryOptionCombo attributeOptionCombo) { + CompleteDataSetRegistration newCopy = new CompleteDataSetRegistration(); + newCopy.setDataSet(old.getDataSet()); + newCopy.setPeriod(old.getPeriod()); + newCopy.setSource(old.getSource()); + newCopy.setAttributeOptionCombo(attributeOptionCombo); + newCopy.setDate(old.getDate()); + newCopy.setStoredBy(old.getStoredBy()); + newCopy.setLastUpdated(old.getLastUpdated()); + newCopy.setCompleted(old.getCompleted()); + newCopy.setPeriodName(old.getPeriodName()); + return newCopy; + } } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/CompleteDataSetRegistrationStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/CompleteDataSetRegistrationStore.java index 91e2340e4270..d7b72adbf917 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/CompleteDataSetRegistrationStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/CompleteDataSetRegistrationStore.java @@ -27,9 +27,12 @@ */ package org.hisp.dhis.dataset; +import java.util.Collection; import java.util.Date; import java.util.List; +import javax.annotation.Nonnull; import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.common.UID; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.period.Period; @@ -44,6 +47,13 @@ public interface CompleteDataSetRegistrationStore { */ void saveCompleteDataSetRegistration(CompleteDataSetRegistration registration); + /** + * Saves a CompleteDataSetRegistration without updating its lastUpdated value + * + * @param registration reg to update + */ + void saveWithoutUpdatingLastUpdated(@Nonnull CompleteDataSetRegistration registration); + /** * Updates a CompleteDataSetRegistration. * @@ -102,4 +112,20 @@ CompleteDataSetRegistration getCompleteDataSetRegistration( * @return the number of completed DataSets. */ int getCompleteDataSetCountLastUpdatedAfter(Date lastUpdated); + + /** + * Retrieve all {@link CompleteDataSetRegistration}s with {@link CategoryOptionCombo} {@link UID}s + * + * @param uids {@link CategoryOptionCombo} {@link UID}s + * @return {@link CompleteDataSetRegistration}s with references to {@link CategoryOptionCombo} + * {@link UID}s passed in + */ + List getAllByCategoryOptionCombo(@Nonnull Collection uids); + + /** + * Delete all {@link CompleteDataSetRegistration}s with references to {@link CategoryOptionCombo}s + * + * @param cocs {@link CategoryOptionCombo}s + */ + void deleteByCategoryOptionCombo(@Nonnull Collection cocs); } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/DataSetService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/DataSetService.java index b79d9218cd38..9cc80d7be245 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/DataSetService.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/DataSetService.java @@ -35,7 +35,6 @@ import org.hisp.dhis.dataentryform.DataEntryForm; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.period.Period; -import org.hisp.dhis.period.PeriodType; import org.hisp.dhis.user.User; import org.hisp.dhis.user.UserDetails; @@ -108,14 +107,6 @@ public interface DataSetService extends DataSetDataIntegrityProvider { */ List getAllDataSets(); - /** - * Gets all DataSets associated with the given PeriodType. - * - * @param periodType the PeriodType. - * @return a list of DataSets. - */ - List getDataSetsByPeriodType(PeriodType periodType); - /** * Returns the data sets which given user have READ access. If the current user has the ALL * authority then all data sets are returned. diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/DataSetStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/DataSetStore.java index b2f4de66224d..c777c3d592c0 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/DataSetStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/DataSetStore.java @@ -32,26 +32,12 @@ import org.hisp.dhis.common.IdentifiableObjectStore; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.dataentryform.DataEntryForm; -import org.hisp.dhis.period.PeriodType; /** * @author Kristian Nordal */ public interface DataSetStore extends IdentifiableObjectStore, DataSetDataIntegrityProvider { - String ID = DataSetStore.class.getName(); - - // ------------------------------------------------------------------------- - // DataSet - // ------------------------------------------------------------------------- - - /** - * Gets all DataSets associated with the given PeriodType. - * - * @param periodType the PeriodType. - * @return a list of DataSets. - */ - List getDataSetsByPeriodType(PeriodType periodType); /** * Gets all DataSets associated with the given DataEntryForm. diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValue.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValue.java index 337f1d3689b3..8d10d1dc4ffb 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValue.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValue.java @@ -272,34 +272,6 @@ public void mergeWith(DataValue other) { this.deleted = other.isDeleted(); } - /** - * Method that creates a new {@link DataValue}. All the old values are used from the supplied old - * {@link DataValue} except for the {@link DataElement} field, which uses the supplied {@link - * DataElement}. - * - * @param oldDv old {@link DataValue} whose values will be used in the new {@link DataValue} - * @param newDataElement {@link DataElement} to be used in the new {@link DataValue} - * @return new {@link DataValue} - */ - public static DataValue dataValueWithNewDataElement(DataValue oldDv, DataElement newDataElement) { - DataValue newValue = - DataValue.builder() - .dataElement(newDataElement) - .period(oldDv.getPeriod()) - .source(oldDv.getSource()) - .categoryOptionCombo(oldDv.getCategoryOptionCombo()) - .attributeOptionCombo(oldDv.getAttributeOptionCombo()) - .value(oldDv.getValue()) - .storedBy(oldDv.getStoredBy()) - .lastUpdated(oldDv.getLastUpdated()) - .comment(oldDv.getComment()) - .followup(oldDv.isFollowup()) - .deleted(oldDv.isDeleted()) - .build(); - newValue.setCreated(oldDv.getCreated()); - return newValue; - } - // ------------------------------------------------------------------------- // hashCode and equals // ------------------------------------------------------------------------- diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueAuditStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueAuditStore.java index 8ab537549896..7761b4af5990 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueAuditStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueAuditStore.java @@ -28,6 +28,8 @@ package org.hisp.dhis.datavalue; import java.util.List; +import javax.annotation.Nonnull; +import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.organisationunit.OrganisationUnit; @@ -68,6 +70,14 @@ public interface DataValueAuditStore { */ void deleteDataValueAudits(DataElement dataElement); + /** + * Deletes all data value audits for the given category option combo. Both properties: + * categoryOptionCombo & attributeOptionCombo are checked for a match. + * + * @param categoryOptionCombo the categoryOptionCombo. + */ + void deleteDataValueAudits(@Nonnull CategoryOptionCombo categoryOptionCombo); + /** * Returns data value audits for the given query. * diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueStore.java index 61bd3550f9a2..74433a5c45f9 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueStore.java @@ -27,9 +27,11 @@ */ package org.hisp.dhis.datavalue; +import java.util.Collection; import java.util.Date; import java.util.List; import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; import org.hisp.dhis.category.CategoryCombo; import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.common.UID; @@ -92,6 +94,29 @@ public interface DataValueStore { */ void deleteDataValues(DataElement dataElement); + /** + * Deletes all data values for the given data element. + * + * @param dataElement the dataElement. + */ + void deleteDataValues(@Nonnull Collection dataElement); + + /** + * Deletes all data values for the given category option combos. + * + * @param categoryOptionCombos the categoryOptionCombos. + */ + void deleteDataValuesByCategoryOptionCombo( + @Nonnull Collection categoryOptionCombos); + + /** + * Deletes all data values for the given attribute option combos. + * + * @param attributeOptionCombos the attributeOptionCombos. + */ + void deleteDataValuesByAttributeOptionCombo( + @Nonnull Collection attributeOptionCombos); + void deleteDataValue(DataValue dataValue); /** @@ -165,6 +190,12 @@ DataValue getDataValue( */ List getDeflatedDataValues(DataExportParams params); + /** + * Retrieve all {@link DataValue}s with references to {@link DataElement}s + * + * @param dataElements {@link DataElement}s + * @return {@link DataValue}s with references to {@link DataElement}s passed in + */ List getAllDataValuesByDataElement(List dataElements); /** @@ -193,4 +224,54 @@ DataValue getDataValue( * @return true, if any values exist, otherwise false */ boolean dataValueExistsForDataElement(String uid); + + List getAllDataValuesByCatOptCombo(@Nonnull Collection uids); + + List getAllDataValuesByAttrOptCombo(@Nonnull Collection uids); + + /** + * SQL for handling merging {@link DataValue}s. There may be multiple potential {@link DataValue} + * duplicates. Duplicate {@link DataValue}s with the latest {@link DataValue#lastUpdated} values + * are kept, the rest are deleted. Only one of these entries can exist due to the composite key + * constraint.
+ * The 3 execution paths are: + * + *

1. If the source {@link DataValue} is not a duplicate, it simply gets its {@link + * DataValue#categoryOptionCombo} updated to that of the target. + * + *

2. If the source {@link DataValue} is a duplicate and has an earlier {@link + * DataValue#lastUpdated} value, it is deleted. + * + *

3. If the source {@link DataValue} is a duplicate and has a later {@link + * DataValue#lastUpdated} value, the target {@link DataValue} is deleted. The source is kept and + * has its {@link DataValue#categoryOptionCombo} updated to that of the target. + * + * @param target target {@link CategoryOptionCombo} + * @param sources source {@link CategoryOptionCombo}s + */ + void mergeDataValuesWithCategoryOptionCombos( + @Nonnull CategoryOptionCombo target, @Nonnull Collection sources); + + /** + * SQL for handling merging {@link DataValue}s. There may be multiple potential {@link DataValue} + * duplicates. Duplicate {@link DataValue}s with the latest {@link DataValue#lastUpdated} values + * are kept, the rest are deleted. Only one of these entries can exist due to the composite key + * constraint.
+ * The 3 execution paths are: + * + *

1. If the source {@link DataValue} is not a duplicate, it simply gets its {@link + * DataValue#attributeOptionCombo} updated to that of the target. + * + *

2. If the source {@link DataValue} is a duplicate and has an earlier {@link + * DataValue#lastUpdated} value, it is deleted. + * + *

3. If the source {@link DataValue} is a duplicate and has a later {@link + * DataValue#lastUpdated} value, the target {@link DataValue} is deleted. The source is kept and + * has its {@link DataValue#attributeOptionCombo} updated to that of the target. + * + * @param target target {@link CategoryOptionCombo} + * @param sources source {@link CategoryOptionCombo}s + */ + void mergeDataValuesWithAttributeOptionCombos( + @Nonnull CategoryOptionCombo target, @Nonnull Collection sources); } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DeflatedDataValue.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DeflatedDataValue.java index 80dfadd0b7fe..a24053cdf35a 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DeflatedDataValue.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DeflatedDataValue.java @@ -95,7 +95,7 @@ public DeflatedDataValue(DataValue dataValue) { this.dataElementId = dataValue.getDataElement().getId(); this.periodId = dataValue.getPeriod().getId(); this.sourceId = dataValue.getSource().getId(); - this.sourcePath = dataValue.getSource().getPath(); + this.sourcePath = dataValue.getSource().getStoredPath(); this.categoryOptionComboId = dataValue.getCategoryOptionCombo().getId(); this.attributeOptionComboId = dataValue.getAttributeOptionCombo().getId(); this.value = dataValue.getValue(); diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/maintenance/MaintenanceStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/maintenance/MaintenanceStore.java index 895414497b93..bff0a8527785 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/maintenance/MaintenanceStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/maintenance/MaintenanceStore.java @@ -27,6 +27,8 @@ */ package org.hisp.dhis.maintenance; +import java.util.List; + /** * @author Lars Helge Overland */ @@ -55,6 +57,8 @@ public interface MaintenanceStore { */ int deleteSoftDeletedEvents(); + int hardDeleteEvents(List eventsToDelete, String eventSelect, String eventDeleteQuery); + /** * Permanently deletes relationships which have been soft deleted, i.e. relationships where the * deleted property is true. @@ -81,4 +85,6 @@ public interface MaintenanceStore { /** Deletes periods which are not associated with any other table. */ void prunePeriods(); + + List getDeletionEntities(String entitySql); } 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 index a19697ace91b..ea2ac54ea215 100644 --- 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 @@ -29,6 +29,7 @@ import com.fasterxml.jackson.annotation.JsonValue; import org.hisp.dhis.category.CategoryOption; +import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.common.IdentifiableObject; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.indicator.Indicator; @@ -43,7 +44,8 @@ public enum MergeType { INDICATOR_TYPE(IndicatorType.class), INDICATOR(Indicator.class), DATA_ELEMENT(DataElement.class), - CATEGORY_OPTION(CategoryOption.class); + CATEGORY_OPTION(CategoryOption.class), + CATEGORY_OPTION_COMBO(CategoryOptionCombo.class); private final Class clazz; private final String name; diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementStore.java index db1aa923f63f..038dbb572554 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementStore.java @@ -29,8 +29,10 @@ import java.util.Collection; import java.util.List; +import javax.annotation.Nonnull; import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.common.GenericStore; +import org.hisp.dhis.common.UID; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.organisationunit.OrganisationUnit; @@ -58,4 +60,14 @@ MinMaxDataElement get( void delete(Collection dataElements, OrganisationUnit parent); List getByDataElement(Collection dataElements); + + /** + * Retrieve all {@link MinMaxDataElement}s with references to {@link CategoryOptionCombo} {@link + * UID}s + * + * @param uids {@link CategoryOptionCombo} {@link UID}s + * @return {@link MinMaxDataElement}s with references to {@link CategoryOptionCombo} {@link UID} + * passed in + */ + List getByCategoryOptionCombo(@Nonnull Collection uids); } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/organisationunit/OrganisationUnit.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/organisationunit/OrganisationUnit.java index b64446845041..6e5c21b42c75 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/organisationunit/OrganisationUnit.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/organisationunit/OrganisationUnit.java @@ -28,7 +28,9 @@ package org.hisp.dhis.organisationunit; import static org.apache.commons.collections4.CollectionUtils.isEmpty; +import static org.apache.commons.lang3.StringUtils.isNotEmpty; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; @@ -136,7 +138,7 @@ public class OrganisationUnit extends BaseDimensionalItemObject private Geometry geometry; - /** A reference to the Image file associated with this OrganisationUnit. */ + /** A reference to the image file resource associated with this {@link OrganisationUnit}. */ private FileResource image; // ------------------------------------------------------------------------- @@ -555,7 +557,6 @@ public void updateParent(OrganisationUnit newParent) { } this.parent = newParent; - newParent.getChildren().add(this); } @@ -574,7 +575,7 @@ public boolean isDescendant(Collection ancestors) { return ancestors.stream() .filter(Objects::nonNull) .map(OrganisationUnit::getUid) - .anyMatch(uid -> StringUtils.contains(this.getPath(), uid)); + .anyMatch(uid -> StringUtils.contains(this.getStoredPath(), uid)); } /** @@ -588,7 +589,7 @@ public boolean isDescendant(OrganisationUnit ancestor) { return false; } - return StringUtils.contains(this.getPath(), ancestor.getUid()); + return StringUtils.contains(this.getStoredPath(), ancestor.getUid()); } public Set getChildrenThisIfEmpty() { @@ -754,6 +755,13 @@ public void setChildren(Set children) { this.children = children; } + /** + * Note that the {@code path} property is mapped with the "property access" mode. This method will + * calculate and return the path property value based on the org unit ancestors. To access the + * {@code path} property directly, use {@link OrganisationUnit#getStoredPath}. + * + * @return the path. + */ @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public String getPath() { @@ -775,16 +783,39 @@ public String getPath() { Collections.reverse(pathList); - this.path = PATH_SEP + StringUtils.join(pathList, PATH_SEP); + return PATH_SEP + StringUtils.join(pathList, PATH_SEP); + } - return this.path; + /** + * Note that the {@code path} property is mapped with the "property access" mode. This method will + * return the persisted {@code path} property value directly. If the path is not defined, + * typically as part of an integration test where the state is not yet flushed to the database, + * the calculated path based on the org unit ancestors is returned. To get the calculated path + * value explicitly, use {@link OrganisationUnit#getPath}. + * + * @return the path. + */ + @JsonIgnore + public String getStoredPath() { + return isNotEmpty(path) ? path : getPath(); } - /** Do not set directly, managed by persistence layer. */ + /** + * Note that the {@code path} property is mapped with the "property access" mode. Do not set + * directly, this property is managed by the persistence layer. + */ public void setPath(String path) { this.path = path; } + /** + * Note that the {@code path} property is mapped with the "property access" mode. This method is + * for unit testing purposes only. + */ + public void updatePath() { + setPath(getPath()); + } + /** * Used by persistence layer. Purpose is to have a column for use in database queries. For * application use see {@link OrganisationUnit#getLevel()} which has better performance. diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/organisationunit/OrganisationUnitService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/organisationunit/OrganisationUnitService.java index 5b3dde26e685..07a740575b23 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/organisationunit/OrganisationUnitService.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/organisationunit/OrganisationUnitService.java @@ -307,8 +307,6 @@ List getOrganisationUnitsAtLevels( Long getOrganisationUnitHierarchyMemberCount( OrganisationUnit parent, Object member, String collectionName) throws BadRequestException; - OrganisationUnitDataSetAssociationSet getOrganisationUnitDataSetAssociationSet(User user); - /** * Returns the level of the given org unit level. The level parameter string can either represent * a numerical level, or a UID referring to an {@link OrganisationUnitLevel} object. @@ -385,19 +383,6 @@ List getOrganisationUnitByCoordinate( @Deprecated(forRemoval = true) boolean isInUserDataViewHierarchy(User user, OrganisationUnit organisationUnit); - /** - * Equal to {@link OrganisationUnitService#isInUserSearchHierarchy(User, OrganisationUnit)} except - * adds a caching layer on top. Use this method when performance is imperative and the risk of a - * stale result is tolerable. - * - * @param user the user to check for. - * @param organisationUnit the organisation unit. - * @return true if the given organisation unit is part of the hierarchy. - * @deprecated Use {@link org.hisp.dhis.user.UserDetails#isInUserSearchHierarchy(String)} instead - */ - @Deprecated(forRemoval = true) - boolean isInUserSearchHierarchyCached(User user, OrganisationUnit organisationUnit); - /** * @deprecated Use {@link org.hisp.dhis.user.UserDetails#isInUserSearchHierarchy(String)} instead */ diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/organisationunit/OrganisationUnitStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/organisationunit/OrganisationUnitStore.java index f35af4770f09..fa0df1fcecd5 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/organisationunit/OrganisationUnitStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/organisationunit/OrganisationUnitStore.java @@ -30,8 +30,6 @@ import java.util.Collection; import java.util.Date; import java.util.List; -import java.util.Map; -import java.util.Set; import org.hisp.dhis.common.IdentifiableObjectStore; import org.hisp.dhis.dataset.DataSet; import org.hisp.dhis.program.Program; @@ -130,18 +128,6 @@ Long getOrganisationUnitHierarchyMemberCount( */ List getOrganisationUnits(OrganisationUnitQueryParams params); - /** - * Creates a mapping between organisation unit UID and set of data set UIDs being assigned to the - * organisation unit. - * - * @param organisationUnits the parent organisation units of the hierarchy to include, ignored if - * null. - * @param dataSets the data set to include, ignored if null. - * @return a map of sets. - */ - Map> getOrganisationUnitDataSetAssocationMap( - Collection organisationUnits, Collection dataSets); - /** * Retrieves the objects where its coordinate is within the 4 area points. 4 area points are Index * 0: Maximum latitude (north edge of box shape) Index 1: Maxium longitude (east edge of box @@ -153,8 +139,10 @@ Map> getOrganisationUnitDataSetAssocationMap( */ List getWithinCoordinateArea(double[] box); + /** Updates the path property of all org units. */ void updatePaths(); + /** Explicitly updates the path property of all org units. */ void forceUpdatePaths(); /** @@ -166,25 +154,10 @@ Map> getOrganisationUnitDataSetAssocationMap( int getMaxLevel(); /** - * Check if the number of orgunits that satisfies the conditions in the queryParams is greater - * than the threshold provided. Note: groups, maxLevels and levels are not supported yet. + * Sets the geometry field for org units to null. * - * @param params The Org unit query params - * @param threshold the threshold count to check against - * @return true if the org units satisfying the params criteria is above the threshold, false - * otherwise. + * @return then number of affected org units. */ - boolean isOrgUnitCountAboveThreshold(OrganisationUnitQueryParams params, int threshold); - - /** - * Get list of organisation unit uids satisfying the query params. Note: groups, maxLevels and - * levels are not supported yet. - * - * @param params The Org unit query params - * @return the list of org unit uids satisfying the params criteria - */ - List getOrganisationUnitUids(OrganisationUnitQueryParams params); - int updateAllOrganisationUnitsGeometryToNull(); /** diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/predictor/PredictorStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/predictor/PredictorStore.java index 848f5b73fc5e..e505d07cfb40 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/predictor/PredictorStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/predictor/PredictorStore.java @@ -30,7 +30,9 @@ import java.util.Collection; import java.util.List; import javax.annotation.Nonnull; +import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.common.IdentifiableObjectStore; +import org.hisp.dhis.common.UID; import org.hisp.dhis.dataelement.DataElement; /** @@ -45,4 +47,12 @@ public interface PredictorStore extends IdentifiableObjectStore { List getAllWithSampleSkipTestContainingDataElement( @Nonnull List dataElementUids); + + /** + * Retrieve all {@link Predictor}s with references to {@link CategoryOptionCombo} {@link UID}s + * + * @param uids {@link CategoryOptionCombo} {@link UID}s + * @return {@link Predictor}s with references to {@link CategoryOptionCombo} {@link UID} passed in + */ + List getByCategoryOptionCombo(@Nonnull Collection uids); } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/program/EventStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/program/EventStore.java index 051747d98536..841ac8c02e39 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/program/EventStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/program/EventStore.java @@ -28,6 +28,8 @@ package org.hisp.dhis.program; import java.util.List; +import java.util.Set; +import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.common.IdentifiableObjectStore; /** @@ -36,4 +38,13 @@ public interface EventStore extends IdentifiableObjectStore { List getAllWithEventDataValuesRootKeysContainingAnyOf(List searchStrings); + + /** + * Updates all {@link Event}s with references to {@link CategoryOptionCombo}s, to use the coc + * reference. + * + * @param cocs {@link CategoryOptionCombo}s to update + * @param coc {@link CategoryOptionCombo} to use as the new value + */ + void setAttributeOptionCombo(Set cocs, long coc); } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobConfigurationService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobConfigurationService.java index dec50e0c1a39..80a0834f65a3 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobConfigurationService.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobConfigurationService.java @@ -32,7 +32,6 @@ import java.util.Map; import javax.annotation.Nonnull; import org.hisp.dhis.feedback.ConflictException; -import org.hisp.dhis.feedback.NotFoundException; import org.hisp.dhis.schema.Property; import org.hisp.dhis.user.UserDetails; import org.springframework.util.MimeType; @@ -62,13 +61,6 @@ String create(JobConfiguration config, MimeType contentType, InputStream content void createDefaultJob(JobType type, UserDetails actingUser); - String createInTransaction( - JobConfiguration jobConfiguration, MimeType contentType, InputStream content) - throws ConflictException, NotFoundException; - - String createInTransaction(JobConfiguration jobConfiguration) - throws ConflictException, NotFoundException; - /** * Updates all {@link JobConfiguration}s that are not {@link JobConfiguration#isEnabled()} to * state {@link JobStatus#DISABLED} in case they are in state {@link JobStatus#SCHEDULED}. diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobConfigurationStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobConfigurationStore.java index 0d6de0b6e91e..5094901949d9 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobConfigurationStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobConfigurationStore.java @@ -175,8 +175,6 @@ public interface JobConfigurationStore extends GenericDimensionalObjectStore - */ -@Slf4j -@RequiredArgsConstructor -@Service -@Profile("test") -public class JobCreationHelperForTests implements JobCreationHelper { - - private final JobConfigurationStore jobConfigurationStore; - private final FileResourceService fileResourceService; +public interface JobExecutionService { - @Transactional - public String create(JobConfiguration config) throws ConflictException { - return createFromConfig(config, jobConfigurationStore); - } + /** + * Creates and runs a new job for one-off operations executed via the scheduler. + * + * @param config a new job that does not exist yet + * @param contentType of the provided content data + * @param content the data that should be processed by the job which is stored as file + * @throws ConflictException in case the config belongs to an existing job or when the job isn't + * configured correctly + */ + void executeOnceNow( + @Nonnull JobConfiguration config, @Nonnull MimeType contentType, @Nonnull InputStream content) + throws ConflictException; - @Transactional - public String create(JobConfiguration config, MimeType contentType, InputStream content) - throws ConflictException { - return createFromConfigAndInputStream( - config, contentType, content, jobConfigurationStore, fileResourceService); - } + /** + * Creates and runs a new job for one-off operations executed via the scheduler. + * + * @param config a new job that does not exist yet + * @throws ConflictException in case the config belongs to an existing job or when the job isn't + * configured correctly + */ + void executeOnceNow(@Nonnull JobConfiguration config) throws ConflictException; } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobSchedulerService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobSchedulerService.java index 8324a21fff0f..708989cad5a1 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobSchedulerService.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobSchedulerService.java @@ -27,18 +27,15 @@ */ package org.hisp.dhis.scheduling; -import java.io.InputStream; import java.util.List; import java.util.Set; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; -import org.hisp.dhis.common.NonTransactional; import org.hisp.dhis.common.UID; import org.hisp.dhis.feedback.ConflictException; import org.hisp.dhis.feedback.ForbiddenException; import org.hisp.dhis.feedback.NotFoundException; import org.hisp.dhis.scheduling.JobProgress.Progress; -import org.springframework.util.MimeType; /** * This is the external API (called by users via controller API) for the scheduling. @@ -49,7 +46,9 @@ public interface JobSchedulerService { /** - * Attempts to switch the {@link JobConfiguration#getSchedulingType()} to {@link + * Ad-hoc execution of existing jobs. + * + *

Attempts to switch the {@link JobConfiguration#getSchedulingType()} to {@link * SchedulingType#ONCE_ASAP} for the given job. * *

A job with a {@link JobConfiguration#getCronExpression()} switches back to {@link @@ -63,22 +62,6 @@ public interface JobSchedulerService { */ void executeNow(@Nonnull String jobId) throws ConflictException, NotFoundException; - /** - * Executes a job configuration in a separate transaction. - * - * @param jobId the job id to execute - * @throws NotFoundException - * @throws ConflictException - */ - void runInTransaction(String jobId) throws NotFoundException, ConflictException; - - @NonTransactional - void createThenExecute(JobConfiguration config, MimeType contentType, InputStream content) - throws ConflictException, NotFoundException; - - @NonTransactional - void createThenExecute(JobConfiguration config) throws ConflictException, NotFoundException; - /** * Reverts the {@link JobStatus} of the job from {@link JobStatus#RUNNING} to the appropriate * status after a failed execution. For an ad-hoc job this is {@link JobStatus#DISABLED}, for a diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/parameters/TestJobParameters.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/parameters/TestJobParameters.java index 4cb9d19d29b1..47df72b298eb 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/parameters/TestJobParameters.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/parameters/TestJobParameters.java @@ -66,6 +66,8 @@ public class TestJobParameters implements JobParameters { /** When true, an exception is used to fail, otherwise the progress tracking api is used */ @JsonProperty private boolean failWithException; + @JsonProperty private boolean failWithPostCondition; + /** Stage failure policy to use, when {@code null} it is the default policy */ @JsonProperty private FailurePolicy failWithPolicy; diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/schema/Property.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/schema/Property.java index 4dabd7abd1e7..56ae50fce9b7 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/schema/Property.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/schema/Property.java @@ -38,10 +38,13 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import lombok.Getter; +import lombok.Setter; import org.hisp.dhis.common.DxfNamespaces; import org.hisp.dhis.common.EmbeddedObject; import org.hisp.dhis.common.IdentifiableObject; import org.hisp.dhis.common.NameableObject; +import org.hisp.dhis.common.Sortable; import org.hisp.dhis.hibernate.HibernateProxyUtils; import org.springframework.core.Ordered; @@ -54,56 +57,56 @@ public class Property implements Ordered, Klass { private Class klass; /** Normalized type of this property */ - private PropertyType propertyType; + @Setter private PropertyType propertyType; /** If this property is a collection, this is the class of the items inside the collection. */ - private Class itemKlass; + @Setter private Class itemKlass; /** * If this property is a collection, this is the normalized type of the items inside the * collection. */ - private PropertyType itemPropertyType; + @Setter private PropertyType itemPropertyType; /** Direct link to getter for this property. */ - private Method getterMethod; + @Getter @Setter private Method getterMethod; /** Direct link to setter for this property. */ - private Method setterMethod; + @Getter @Setter private Method setterMethod; /** * Name for this property, if this class is a collection, it is the name of the items -inside- the * collection and not the collection wrapper itself. */ - private String name; + @Setter private String name; /** Name for actual field, used to persistence operations and getting setter/getter. */ - private String fieldName; + @Setter private String fieldName; /** * Is this property persisted somewhere. This property will be used to create criteria queries on * demand (default: false) */ - private boolean persisted; + @Setter private boolean persisted; /** Name of collection wrapper. */ - private String collectionName; + @Setter private String collectionName; /** If this Property is a collection, should it be wrapped with collectionName? */ - private Boolean collectionWrapping; + @Setter private Boolean collectionWrapping; /** * Description if provided, will be fetched from @Description annotation. * * @see org.hisp.dhis.common.annotation.Description */ - private String description; + @Setter private String description; /** Namespace used for this property. */ - private String namespace; + @Setter private String namespace; /** Usually only used for XML. Is this property considered an attribute. */ - private boolean attribute; + @Setter private boolean attribute; /** * This property is true if the type pointed to does not export any properties itself, it is then @@ -111,120 +114,121 @@ public class Property implements Ordered, Klass { * type of the collection, e.g. List would set simple to be true, but List * would set it to false. */ - private boolean simple; + @Setter private boolean simple; /** * This property is true if the type of this property is a sub-class of Collection. * * @see java.util.Collection */ - private boolean collection; + @Setter private boolean collection; /** * This property is true if collection=true and klass points to a implementation with a stable * order (i.e. List). */ - private boolean ordered; + @Setter private boolean ordered; /** * If this property is a complex object or a collection, is this property considered the owner of * that relationship (important for imports etc). */ - private boolean owner; + @Setter private boolean owner; /** * Is this class a sub-class of IdentifiableObject * * @see org.hisp.dhis.common.IdentifiableObject */ - private boolean identifiableObject; + @Setter private boolean identifiableObject; /** * Is this class a sub-class of NameableObject * * @see org.hisp.dhis.common.NameableObject */ - private boolean nameableObject; + @Setter private boolean nameableObject; /** Does this class implement {@link EmbeddedObject} ? */ - private boolean embeddedObject; + @Setter private boolean embeddedObject; /** Does this class implement {@link EmbeddedObject} ? */ - private boolean analyticalObject; + @Setter private boolean analyticalObject; /** Can this property be read. */ - private boolean readable; + @Setter private boolean readable; /** Can this property be written to. */ - private boolean writable; + @Setter private boolean writable; /** Are the values for this property required to be unique? */ - private boolean unique; + @Setter private boolean unique; /** Nullability of this property. */ - private boolean required; + @Setter private boolean required; /** Maximum length/size/value of this property. */ - private Integer length; + @Setter private Integer length; /** Minimum size/length of this property. */ - private Double max; + @Setter private Double max; /** Minimum size/length of this property. */ - private Double min; + @Setter private Double min; /** Cascading used when doing CRUD operations. */ - private String cascade; + @Setter private String cascade; /** Is property many-to-many. */ - private boolean manyToMany; + @Setter private boolean manyToMany; /** Is property one-to-one. */ - private boolean oneToOne; + @Setter private boolean oneToOne; /** Is property many-to-one. */ - private boolean manyToOne; + @Setter private boolean manyToOne; /** Is property one-to-many. */ - private boolean oneToMany; + @Setter private boolean oneToMany; /** The hibernate role of the owning side. */ - private String owningRole; + @Setter private String owningRole; /** The hibernate role of the inverse side (if many-to-many). */ - private String inverseRole; + @Setter private String inverseRole; /** If property type is enum, this is the list of valid options. */ - private List constants; + @Setter private List constants; /** Used by LinkService to link to the Schema describing this type (if reference). */ - private String href; + @Setter private String href; /** Points to relative Web-API endpoint (if exposed). */ - private String relativeApiEndpoint; + @Setter private String relativeApiEndpoint; /** Used by LinkService to link to the API endpoint containing this type. */ - private String apiEndpoint; + @Setter private String apiEndpoint; /** PropertyTransformer to apply to this property before and field filtering is applied. */ - private Class propertyTransformer; + @Getter @Setter private Class propertyTransformer; /** Default value of the Property */ private Object defaultValue; - private boolean translatable; + @Setter private boolean translatable; - private String translationKey; + @Setter private String translationKey; /** * The translation key use for retrieving I18n translation of this property's name. The key * follows snake_case naming convention. */ - private String i18nTranslationKey; + @Setter private String i18nTranslationKey; private GistPreferences gistPreferences = GistPreferences.DEFAULT; /** All annotations present on this property (either through field or method) */ + @Getter @Setter private Map, Annotation> annotations = new HashMap<>(); public Property() {} @@ -259,144 +263,85 @@ public PropertyType getPropertyType() { return propertyType; } - public void setPropertyType(PropertyType propertyType) { - this.propertyType = propertyType; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public Class getItemKlass() { return itemKlass; } - public void setItemKlass(Class itemKlass) { - this.itemKlass = itemKlass; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public PropertyType getItemPropertyType() { return itemPropertyType; } - public void setItemPropertyType(PropertyType itemPropertyType) { - this.itemPropertyType = itemPropertyType; - } - - public Method getGetterMethod() { - return getterMethod; - } - - public void setGetterMethod(Method getterMethod) { - this.getterMethod = getterMethod; - } - - public Method getSetterMethod() { - return setterMethod; - } - - public void setSetterMethod(Method setterMethod) { - this.setterMethod = setterMethod; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public String getName() { return name; } - public void setName(String name) { - this.name = name; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public String getFieldName() { return fieldName; } - public void setFieldName(String fieldName) { - this.fieldName = fieldName; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public boolean isPersisted() { return persisted; } - public void setPersisted(boolean persisted) { - this.persisted = persisted; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public String getCollectionName() { return collectionName != null ? collectionName : (isCollection() ? name : null); } - public void setCollectionName(String collectionName) { - this.collectionName = collectionName; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public Boolean isCollectionWrapping() { return collectionWrapping; } - public void setCollectionWrapping(Boolean collectionWrapping) { - this.collectionWrapping = collectionWrapping; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public String getDescription() { return description; } - public void setDescription(String description) { - this.description = description; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public String getNamespace() { return namespace; } - public void setNamespace(String namespace) { - this.namespace = namespace; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public boolean isAttribute() { return attribute; } - public void setAttribute(boolean attribute) { - this.attribute = attribute; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public boolean isSimple() { return simple; } - public void setSimple(boolean simple) { - this.simple = simple; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public boolean isCollection() { return collection; } - public void setCollection(boolean collection) { - this.collection = collection; + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) + public boolean isSortable() { + Sortable sortable = getterMethod == null ? null : getterMethod.getAnnotation(Sortable.class); + return sortable != null + ? sortable.value() && (!sortable.whenPersisted() || isPersisted()) + : !isCollection() && isSimple() && isPersisted(); } @JsonProperty @@ -405,220 +350,132 @@ public boolean isOrdered() { return ordered; } - public void setOrdered(boolean ordered) { - this.ordered = ordered; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public boolean isOwner() { return owner; } - public void setOwner(boolean owner) { - this.owner = owner; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public boolean isIdentifiableObject() { return identifiableObject; } - public void setIdentifiableObject(boolean identifiableObject) { - this.identifiableObject = identifiableObject; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public boolean isNameableObject() { return nameableObject; } - public void setNameableObject(boolean nameableObject) { - this.nameableObject = nameableObject; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public boolean isEmbeddedObject() { return embeddedObject; } - public void setEmbeddedObject(boolean embeddedObject) { - this.embeddedObject = embeddedObject; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public boolean isAnalyticalObject() { return analyticalObject; } - public void setAnalyticalObject(boolean analyticalObject) { - this.analyticalObject = analyticalObject; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public boolean isReadable() { return readable; } - public void setReadable(boolean readable) { - this.readable = readable; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public boolean isWritable() { return writable; } - public void setWritable(boolean writable) { - this.writable = writable; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public boolean isUnique() { return unique; } - public void setUnique(boolean unique) { - this.unique = unique; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public boolean isRequired() { return required; } - public void setRequired(boolean required) { - this.required = required; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public Integer getLength() { return length; } - public void setLength(Integer length) { - this.length = length; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public Double getMax() { return max; } - public void setMax(Double max) { - this.max = max; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public Double getMin() { return min; } - public void setMin(Double min) { - this.min = min; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public String getCascade() { return cascade; } - public void setCascade(String cascade) { - this.cascade = cascade; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public boolean isManyToMany() { return manyToMany; } - public void setManyToMany(boolean manyToMany) { - this.manyToMany = manyToMany; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public boolean isOneToOne() { return oneToOne; } - public void setOneToOne(boolean oneToOne) { - this.oneToOne = oneToOne; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public boolean isManyToOne() { return manyToOne; } - public void setManyToOne(boolean manyToOne) { - this.manyToOne = manyToOne; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public boolean isOneToMany() { return oneToMany; } - public void setOneToMany(boolean oneToMany) { - this.oneToMany = oneToMany; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public String getOwningRole() { return owningRole; } - public void setOwningRole(String owningRole) { - this.owningRole = owningRole; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public String getInverseRole() { return inverseRole; } - public void setInverseRole(String inverseRole) { - this.inverseRole = inverseRole; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public String getTranslationKey() { return this.translationKey; } - public void setTranslationKey(String translationKey) { - this.translationKey = translationKey; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public String getI18nTranslationKey() { return i18nTranslationKey; } - public void setI18nTranslationKey(String i18nTranslationKey) { - this.i18nTranslationKey = i18nTranslationKey; - } - @JsonProperty @JacksonXmlElementWrapper(localName = "constants", namespace = DxfNamespaces.DXF_2_0) @JacksonXmlProperty(localName = "constant", namespace = DxfNamespaces.DXF_2_0) @@ -626,54 +483,30 @@ public List getConstants() { return constants; } - public void setConstants(List constants) { - this.constants = constants; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public String getHref() { return href; } - public void setHref(String href) { - this.href = href; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public String getRelativeApiEndpoint() { return relativeApiEndpoint; } - public void setRelativeApiEndpoint(String relativeApiEndpoint) { - this.relativeApiEndpoint = relativeApiEndpoint; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public String getApiEndpoint() { return apiEndpoint; } - public void setApiEndpoint(String apiEndpoint) { - this.apiEndpoint = apiEndpoint; - } - @JsonProperty("propertyTransformer") @JacksonXmlProperty(localName = "propertyTransformer", namespace = DxfNamespaces.DXF_2_0) public boolean hasPropertyTransformer() { return propertyTransformer != null; } - public Class getPropertyTransformer() { - return propertyTransformer; - } - - public void setPropertyTransformer(Class propertyTransformer) { - this.propertyTransformer = propertyTransformer; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public Object getDefaultValue() { @@ -695,10 +528,6 @@ public boolean isTranslatable() { return this.translatable; } - public void setTranslatable(boolean translatable) { - this.translatable = translatable; - } - @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) public GistPreferences getGistPreferences() { @@ -709,14 +538,6 @@ public void setGistPreferences(GistPreferences gistPreferences) { this.gistPreferences = gistPreferences == null ? GistPreferences.DEFAULT : gistPreferences; } - public Map, Annotation> getAnnotations() { - return annotations; - } - - public void setAnnotations(Map, Annotation> annotations) { - this.annotations = annotations; - } - @SuppressWarnings("unchecked") public A getAnnotation(Class annotationType) { return (A) annotations.get(annotationType); 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 1ad8ebf72978..e6d4666c49fb 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 @@ -87,6 +87,7 @@ public enum Authorities { F_INDICATOR_MERGE, F_DATA_ELEMENT_MERGE, F_CATEGORY_OPTION_MERGE, + F_CATEGORY_OPTION_COMBO_MERGE, F_INSERT_CUSTOM_JS_CSS, F_VIEW_UNAPPROVED_DATA, F_USER_VIEW, diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/util/PeriodCriteriaUtils.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/util/PeriodCriteriaUtils.java index 36ea52ec5a81..c675e8a00c01 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/util/PeriodCriteriaUtils.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/util/PeriodCriteriaUtils.java @@ -40,12 +40,12 @@ @NoArgsConstructor(access = PRIVATE) public class PeriodCriteriaUtils { /** - * Defines a default period for the given criteria, if none is present. + * Add a default period for the given criteria, if none is present. * * @param criteria {@link EventsAnalyticsQueryCriteria} query criteria. * @param defaultPeriod the default period to set, based on {@link RelativePeriodEnum}. */ - public static void defineDefaultPeriodForCriteria( + public static void addDefaultPeriodIfAbsent( EventsAnalyticsQueryCriteria criteria, RelativePeriodEnum defaultPeriod) { if (!hasPeriod(criteria)) { criteria.getDimension().add(PERIOD_DIM_ID + ":" + defaultPeriod.name()); @@ -53,12 +53,12 @@ public static void defineDefaultPeriodForCriteria( } /** - * Defines a default period for the given criteria, if none is present. + * Adds a default period for the given criteria, if none is present. * * @param criteria {@link EnrollmentAnalyticsQueryCriteria} query criteria. * @param defaultPeriod the default period to set, based on {@link RelativePeriodEnum}. */ - public static void defineDefaultPeriodForCriteria( + public static void addDefaultPeriodIfAbsent( EnrollmentAnalyticsQueryCriteria criteria, RelativePeriodEnum defaultPeriod) { if (!hasPeriod(criteria)) { criteria.getDimension().add(PERIOD_DIM_ID + ":" + defaultPeriod.name()); diff --git a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/util/PeriodCriteriaUtilsTest.java b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/util/PeriodCriteriaUtilsTest.java index 8c23f70ca46d..ec02487a1366 100644 --- a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/util/PeriodCriteriaUtilsTest.java +++ b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/util/PeriodCriteriaUtilsTest.java @@ -50,7 +50,7 @@ void testDefineDefaultPeriodDimensionCriteriaWithOrderBy_Event_forRelativePeriod configureEventsAnalyticsQueryCriteriaWithPeriod(LAST_5_YEARS.name()); // when - PeriodCriteriaUtils.defineDefaultPeriodForCriteria(eventsAnalyticsQueryCriteria, LAST_5_YEARS); + PeriodCriteriaUtils.addDefaultPeriodIfAbsent(eventsAnalyticsQueryCriteria, LAST_5_YEARS); // then assertTrue(eventsAnalyticsQueryCriteria.getDimension().stream().findFirst().isPresent()); @@ -67,7 +67,7 @@ void testDefineDefaultPeriodDimensionCriteriaWithOrderBy_Event_forNoRelativePeri configureEventsAnalyticsQueryCriteriaWithPeriod(null); // when - PeriodCriteriaUtils.defineDefaultPeriodForCriteria(eventsAnalyticsQueryCriteria, LAST_5_YEARS); + PeriodCriteriaUtils.addDefaultPeriodIfAbsent(eventsAnalyticsQueryCriteria, LAST_5_YEARS); // then assertTrue(eventsAnalyticsQueryCriteria.getDimension().stream().findFirst().isPresent()); @@ -116,7 +116,7 @@ void testDefineDefaultPeriodDimensionCriteriaWithOrderBy_Event_forStartAndEndDat configureEventsAnalyticsQueryCriteriaWithDateRange(new Date(), new Date()); // when - PeriodCriteriaUtils.defineDefaultPeriodForCriteria(eventsAnalyticsQueryCriteria, LAST_5_YEARS); + PeriodCriteriaUtils.addDefaultPeriodIfAbsent(eventsAnalyticsQueryCriteria, LAST_5_YEARS); // then assertFalse(eventsAnalyticsQueryCriteria.getDimension().stream().findFirst().isPresent()); @@ -130,7 +130,7 @@ void testDefineDefaultPeriodDimensionCriteriaWithOrderBy_Event_forEventDate() { configureEventsAnalyticsQueryCriteriaWithEventDate(); // when - PeriodCriteriaUtils.defineDefaultPeriodForCriteria(eventsAnalyticsQueryCriteria, LAST_5_YEARS); + PeriodCriteriaUtils.addDefaultPeriodIfAbsent(eventsAnalyticsQueryCriteria, LAST_5_YEARS); // then assertFalse(eventsAnalyticsQueryCriteria.getDimension().stream().findFirst().isPresent()); @@ -144,8 +144,7 @@ void testDefineDefaultPeriodDimensionCriteriaWithOrderBy_Enrollment_forRelativeP configureEnrollmentAnalyticsQueryCriteriaWithPeriod(LAST_5_YEARS.name()); // when - PeriodCriteriaUtils.defineDefaultPeriodForCriteria( - enrollmentsAnalyticsQueryCriteria, LAST_5_YEARS); + PeriodCriteriaUtils.addDefaultPeriodIfAbsent(enrollmentsAnalyticsQueryCriteria, LAST_5_YEARS); // then assertTrue(enrollmentsAnalyticsQueryCriteria.getDimension().stream().findFirst().isPresent()); @@ -162,8 +161,7 @@ void testDefineDefaultPeriodDimensionCriteriaWithOrderBy_Enrollment_forNoRelativ configureEnrollmentAnalyticsQueryCriteriaWithPeriod(null); // when - PeriodCriteriaUtils.defineDefaultPeriodForCriteria( - enrollmentAnalyticsQueryCriteria, LAST_5_YEARS); + PeriodCriteriaUtils.addDefaultPeriodIfAbsent(enrollmentAnalyticsQueryCriteria, LAST_5_YEARS); // then assertTrue(enrollmentAnalyticsQueryCriteria.getDimension().stream().findFirst().isPresent()); @@ -180,8 +178,7 @@ void testDefineDefaultPeriodDimensionCriteriaWithOrderBy_Enrollment_forStartAndE configureEnrollmentsAnalyticsQueryCriteriaWithDateRange(new Date(), new Date()); // when - PeriodCriteriaUtils.defineDefaultPeriodForCriteria( - enrollmentAnalyticsQueryCriteria, LAST_5_YEARS); + PeriodCriteriaUtils.addDefaultPeriodIfAbsent(enrollmentAnalyticsQueryCriteria, LAST_5_YEARS); // then assertFalse(enrollmentAnalyticsQueryCriteria.getDimension().stream().findFirst().isPresent()); @@ -195,8 +192,7 @@ void testDefineDefaultPeriodDimensionCriteriaWithOrderBy_Enrollment_forEventDate configureEnrollmentAnalyticsQueryCriteriaWithEventDate(); // when - PeriodCriteriaUtils.defineDefaultPeriodForCriteria( - enrollmentsAnalyticsQueryCriteria, LAST_5_YEARS); + PeriodCriteriaUtils.addDefaultPeriodIfAbsent(enrollmentsAnalyticsQueryCriteria, LAST_5_YEARS); // then assertFalse(enrollmentsAnalyticsQueryCriteria.getDimension().stream().findFirst().isPresent()); diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/dataintegrity/hibernate/HibernateDataIntegrityStore.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/dataintegrity/hibernate/HibernateDataIntegrityStore.java index f59b413fd85b..4e008cab873b 100644 --- a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/dataintegrity/hibernate/HibernateDataIntegrityStore.java +++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/dataintegrity/hibernate/HibernateDataIntegrityStore.java @@ -58,7 +58,8 @@ public class HibernateDataIntegrityStore implements DataIntegrityStore { public DataIntegritySummary querySummary(DataIntegrityCheck check, String sql) { Date startTime = new Date(); // Note! that the SQL here can be touching any table so we cannot sync it - Object summary = entityManager.createNativeQuery(sql).getSingleResult(); + Object summary = + entityManager.createNativeQuery(sql).getResultStream().findFirst().orElse(null); return new DataIntegritySummary( check, startTime, new Date(), null, parseCount(summary), parsePercentage(summary)); } diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/maintenance/jdbc/JdbcMaintenanceStore.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/maintenance/jdbc/JdbcMaintenanceStore.java index b24dc776eccc..7b70b07553cc 100644 --- a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/maintenance/jdbc/JdbcMaintenanceStore.java +++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/maintenance/jdbc/JdbcMaintenanceStore.java @@ -101,6 +101,11 @@ public int deleteSoftDeletedEvents() { String eventSelect = "(select eventid from event where deleted is true)"; + return hardDeleteEvents(deletedEvents, eventSelect, "delete from event where deleted is true"); + } + + @Override + public int hardDeleteEvents(List eventsToDelete, String eventSelect, String eventDelete) { String pmSelect = "(select id from programmessage where eventid in " + eventSelect + " )"; /* @@ -126,15 +131,14 @@ public int deleteSoftDeletedEvents() { "delete from programmessage where eventid in " + eventSelect, "delete from programnotificationinstance where eventid in " + eventSelect, // finally delete the events - "delete from event where deleted is true" + eventDelete }; int result = jdbcTemplate.batchUpdate(sqlStmts)[sqlStmts.length - 1]; - if (result > 0 && !deletedEvents.isEmpty()) { - auditHardDeletedEntity(deletedEvents, Event.class); + if (result > 0 && !eventsToDelete.isEmpty()) { + auditHardDeletedEntity(eventsToDelete, Event.class); } - return result; } @@ -352,7 +356,8 @@ public void prunePeriods() { jdbcTemplate.batchUpdate(sql); } - private List getDeletionEntities(String entitySql) { + @Override + public List getDeletionEntities(String entitySql) { /* * Get all soft deleted entities before they are hard deleted from * database diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/CommonDataMergeHandler.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/CommonDataMergeHandler.java new file mode 100644 index 000000000000..03713a2e2b45 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/CommonDataMergeHandler.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.merge; + +import jakarta.persistence.EntityManager; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.common.BaseIdentifiableObject; +import org.hisp.dhis.dataelement.DataElement; +import org.hisp.dhis.datavalue.DataValue; +import org.hisp.dhis.datavalue.DataValueStore; +import org.springframework.stereotype.Component; + +/** + * Common Merge handler for data entities. The merge operations here are shared by multiple merge + * use cases (e.g. CategoryOptionCombo & DataElement merge), hence the need for common handlers, to + * reuse code and avoid duplication. This keeps merges consistent, for better or for worse. + * + * @author david mackessy + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class CommonDataMergeHandler { + + private final DataValueStore dataValueStore; + private final EntityManager entityManager; + + /** + * Groups {@link DataValue}s into duplicates & non-duplicates. It uses the duplicate predicate + * param value to decide whether the {@link DataValue} is considered a duplicate or not. Once + * grouped, they are then passed on to be handled accordingly. + * + * @param merge {@link DataValueMergeParams} to perform merge + * @param {@link BaseIdentifiableObject} type + */ + public void handleDataValues( + @Nonnull DataValueMergeParams merge) { + Map> sourceDuplicateList = + merge.sourceDataValues.stream() + .collect( + Collectors.partitioningBy( + dv -> merge.dvDuplicatePredicate.test(dv, merge.targetDataValues))); + + if (!sourceDuplicateList.get(false).isEmpty()) + handleNonDuplicates(sourceDuplicateList.get(false), merge); + if (!sourceDuplicateList.get(true).isEmpty()) + handleDuplicates(sourceDuplicateList.get(true), merge); + } + + /** + * Handle merging duplicate {@link DataValue}s. There may be multiple potential {@link DataValue} + * duplicates. The {@link DataValue} with the latest `lastUpdated` value is filtered out, the rest + * are then deleted at the end of the process (We can only have one of these entries due to the + * composite key constraint). The filtered-out {@link DataValue} will be compared with the target + * {@link DataValue} lastUpdated date. + * + *

If the target date is later, no action is required. + * + *

If the source date is later, then A new {@link DataValue} will be created from the old + * {@link DataValue} values and it will use the target {@link DataElement} ref. This new {@link + * DataValue} will be saved to the database. This sequence is required as a {@link DataValue} has + * a composite primary key. This prohibits updating the ref in a source {@link DataValue}. The + * matching target {@link DataValue}s will then be deleted. + * + * @param sourceDataValueDuplicates {@link DataValue}s to merge + * @param dvMergeParams {@link DataValueMergeParams} + */ + private void handleDuplicates( + @Nonnull Collection sourceDataValueDuplicates, + @Nonnull DataValueMergeParams dvMergeParams) { + log.info( + "Handling " + + sourceDataValueDuplicates.size() + + " duplicate data values, keeping later lastUpdated value"); + + // Group Data values by key, so we can deal with each duplicate correctly + Map> sourceDataValuesGroupedByKey = + sourceDataValueDuplicates.stream() + .collect(Collectors.groupingBy(dvMergeParams.dataValueKey)); + + // Filter groups down to single DV with latest date + List filtered = + sourceDataValuesGroupedByKey.values().stream() + .map(ls -> Collections.max(ls, Comparator.comparing(DataValue::getLastUpdated))) + .toList(); + + for (DataValue source : filtered) { + DataValue matchingTargetDataValue = + dvMergeParams.targetDataValues.get(dvMergeParams.dataValueKey.apply(source)); + + if (matchingTargetDataValue.getLastUpdated().before(source.getLastUpdated())) { + dataValueStore.deleteDataValue(matchingTargetDataValue); + + // Detaching is required here as it's not possible to add a new DataValue with essentially + // the same composite primary key - Throws `NonUniqueObjectException: A different object + // with the same identifier value was already associated with the session` + entityManager.detach(matchingTargetDataValue); + DataValue copyWithNewRef = + dvMergeParams.newDataValueFromOld.apply(source, dvMergeParams.target); + dataValueStore.addDataValue(copyWithNewRef); + } + } + + // Delete the rest of the source data values after handling the last update duplicate + dvMergeParams.dvStoreDelete.accept(dvMergeParams.sources); + } + + /** + * Method to handle merging non-duplicate {@link DataValue}s. A new {@link DataValue} will be + * created from the old {@link DataValue} values, and it will use the target {@link + * CategoryOptionCombo} ref. This new {@link DataValue} will be saved to the database. This + * sequence is required as a {@link DataValue} has a composite primary key. This prohibits + * updating the ref in a source {@link DataValue}. + * + *

All source {@link DataValue}s will then be deleted. + * + * @param dataValues {@link DataValue}s to merge + * @param dvMergeParams {@link DataValueMergeParams} + */ + private void handleNonDuplicates( + @Nonnull List dataValues, @Nonnull DataValueMergeParams dvMergeParams) { + log.info( + "Handling " + + dataValues.size() + + " non duplicate data values. Add new DataValue entry (using target ref) and delete old source entry"); + + dataValues.forEach( + sourceDataValue -> { + DataValue copyWithNewCocRef = + dvMergeParams.newDataValueFromOld.apply(sourceDataValue, dvMergeParams.target); + dataValueStore.addDataValue(copyWithNewCocRef); + }); + + log.info("Deleting all data values referencing source CategoryOptionCombos"); + dvMergeParams.dvStoreDelete.accept(dvMergeParams.sources); + } + + public record DataValueMergeParams( + @Nonnull MergeRequest mergeRequest, + @Nonnull List sources, + @Nonnull T target, + @Nonnull List sourceDataValues, + @Nonnull Map targetDataValues, + @Nonnull Consumer> dvStoreDelete, + @Nonnull BiPredicate> dvDuplicatePredicate, + @Nonnull BiFunction newDataValueFromOld, + @Nonnull Function dataValueKey) {} +} diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/option/CategoryOptionMergeService.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/option/CategoryOptionMergeService.java index 3e2ccf59c655..5607511e3942 100644 --- a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/option/CategoryOptionMergeService.java +++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/option/CategoryOptionMergeService.java @@ -36,7 +36,6 @@ import org.hisp.dhis.category.CategoryOption; import org.hisp.dhis.category.CategoryService; import org.hisp.dhis.common.UID; -import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.feedback.MergeReport; import org.hisp.dhis.merge.MergeParams; import org.hisp.dhis.merge.MergeRequest; @@ -109,7 +108,7 @@ private void initMergeHandlers() { } /** - * Functional interface representing a {@link DataElement} data merge operation. + * Functional interface representing a {@link CategoryOption} data merge operation. * * @author david mackessy */ diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/CategoryOptionComboMergeService.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/CategoryOptionComboMergeService.java new file mode 100644 index 000000000000..bbaa4928b09f --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/CategoryOptionComboMergeService.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.merge.category.optioncombo; + +import jakarta.persistence.EntityManager; +import java.util.List; +import javax.annotation.Nonnull; +import javax.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.category.CategoryService; +import org.hisp.dhis.feedback.ErrorCode; +import org.hisp.dhis.feedback.ErrorMessage; +import org.hisp.dhis.feedback.MergeReport; +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.merge.MergeValidator; +import org.springframework.stereotype.Service; + +/** + * Main class for a {@link CategoryOptionCombo} merge. + * + * @author david mackessy + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class CategoryOptionComboMergeService implements MergeService { + + private final CategoryService categoryService; + private final MetadataCategoryOptionComboMergeHandler metadataMergeHandler; + private final DataCategoryOptionComboMergeHandler dataMergeHandler; + private final MergeValidator validator; + private final EntityManager entityManager; + private List metadataMergeHandlers; + private List dataMergeHandlers; + private List auditMergeHandlers; + + @Override + public MergeRequest validate(@Nonnull MergeParams params, @Nonnull MergeReport mergeReport) { + MergeRequest request = + validator.validateUIDs(params, mergeReport, MergeType.CATEGORY_OPTION_COMBO); + + // merge-specific validation + if (params.getDataMergeStrategy() == null) { + mergeReport.addErrorMessage(new ErrorMessage(ErrorCode.E1534)); + } + return request; + } + + @Override + public MergeReport merge(@Nonnull MergeRequest request, @Nonnull MergeReport mergeReport) { + log.info("Performing CategoryOptionCombo merge"); + + List sources = + categoryService.getCategoryOptionCombosByUid(request.getSources()); + CategoryOptionCombo target = + categoryService.getCategoryOptionCombo(request.getTarget().getValue()); + + // merge metadata + log.info("Handling CategoryOptionCombo reference associations and merges"); + metadataMergeHandlers.forEach(h -> h.merge(sources, target)); + dataMergeHandlers.forEach(h -> h.merge(sources, target, request)); + auditMergeHandlers.forEach(h -> h.merge(sources, request)); + + // a flush is required here to bring the system into a consistent state. This is required so + // that the deletion handler hooks, which are usually done using JDBC (non-Hibernate), can + // see the most up-to-date state, including merges done using Hibernate. + entityManager.flush(); + + // handle deletes + if (request.isDeleteSources()) handleDeleteSources(sources, mergeReport); + + return mergeReport; + } + + private void handleDeleteSources(List sources, MergeReport mergeReport) { + log.info("Deleting source CategoryOptionCombos"); + for (CategoryOptionCombo source : sources) { + mergeReport.addDeletedSource(source.getUid()); + categoryService.deleteCategoryOptionCombo(source); + } + } + + @PostConstruct + private void initMergeHandlers() { + metadataMergeHandlers = + List.of( + metadataMergeHandler::handleCategoryOptions, + metadataMergeHandler::handleCategoryCombos, + metadataMergeHandler::handlePredictors, + metadataMergeHandler::handleDataElementOperands, + metadataMergeHandler::handleMinMaxDataElements, + metadataMergeHandler::handleSmsCodes); + + dataMergeHandlers = + List.of( + dataMergeHandler::handleDataValues, + dataMergeHandler::handleDataApprovals, + dataMergeHandler::handleEvents, + dataMergeHandler::handleCompleteDataSetRegistrations); + + auditMergeHandlers = + List.of( + dataMergeHandler::handleDataValueAudits, dataMergeHandler::handleDataApprovalAudits); + } + + /** + * Functional interface representing a {@link CategoryOptionCombo} data merge operation. + * + * @author david mackessy + */ + @FunctionalInterface + public interface MetadataMergeHandler { + void merge(@Nonnull List sources, @Nonnull CategoryOptionCombo target); + } + + @FunctionalInterface + public interface DataMergeHandler { + void merge( + @Nonnull List sources, + @Nonnull CategoryOptionCombo target, + @Nonnull MergeRequest request); + } + + @FunctionalInterface + public interface DataMergeHandlerNoTarget { + void merge(@Nonnull List sources, @Nonnull MergeRequest request); + } +} diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/DataCategoryOptionComboMergeHandler.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/DataCategoryOptionComboMergeHandler.java new file mode 100644 index 000000000000..81c144e7c59e --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/DataCategoryOptionComboMergeHandler.java @@ -0,0 +1,411 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.merge.category.optioncombo; + +import static org.hisp.dhis.merge.DataMergeStrategy.DISCARD; +import static org.hisp.dhis.merge.DataMergeStrategy.LAST_UPDATED; + +import jakarta.persistence.EntityManager; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.function.BiPredicate; +import java.util.function.Function; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.common.BaseIdentifiableObject; +import org.hisp.dhis.common.UID; +import org.hisp.dhis.dataapproval.DataApproval; +import org.hisp.dhis.dataapproval.DataApprovalAudit; +import org.hisp.dhis.dataapproval.DataApprovalAuditStore; +import org.hisp.dhis.dataapproval.DataApprovalStore; +import org.hisp.dhis.dataset.CompleteDataSetRegistration; +import org.hisp.dhis.dataset.CompleteDataSetRegistrationStore; +import org.hisp.dhis.datavalue.DataValue; +import org.hisp.dhis.datavalue.DataValueAudit; +import org.hisp.dhis.datavalue.DataValueAuditStore; +import org.hisp.dhis.datavalue.DataValueStore; +import org.hisp.dhis.maintenance.MaintenanceStore; +import org.hisp.dhis.merge.DataMergeStrategy; +import org.hisp.dhis.merge.MergeRequest; +import org.hisp.dhis.program.Event; +import org.hisp.dhis.program.EventStore; +import org.springframework.stereotype.Component; + +/** + * Merge handler for data types. + * + * @author david mackessy + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class DataCategoryOptionComboMergeHandler { + + private final DataValueStore dataValueStore; + private final DataValueAuditStore dataValueAuditStore; + private final DataApprovalAuditStore dataApprovalAuditStore; + private final DataApprovalStore dataApprovalStore; + private final EventStore eventStore; + private final MaintenanceStore maintenanceStore; + private final CompleteDataSetRegistrationStore completeDataSetRegistrationStore; + private final EntityManager entityManager; + + public void handleDataValues( + @Nonnull List sources, + @Nonnull CategoryOptionCombo target, + @Nonnull MergeRequest mergeRequest) { + if (DISCARD == mergeRequest.getDataMergeStrategy()) { + log.info("Deleting source data values as dataMergeStrategy is DISCARD"); + dataValueStore.deleteDataValuesByCategoryOptionCombo(sources); + dataValueStore.deleteDataValuesByAttributeOptionCombo(sources); + } else { + log.info("Merging source data values as dataMergeStrategy is LAST_UPDATED"); + dataValueStore.mergeDataValuesWithCategoryOptionCombos(target, sources); + dataValueStore.mergeDataValuesWithAttributeOptionCombos(target, sources); + } + } + + /** + * All {@link DataValueAudit}s will either be deleted or left as is, based on whether the source + * {@link CategoryOptionCombo}s are being deleted or not. + */ + public void handleDataValueAudits( + @Nonnull List sources, @Nonnull MergeRequest mergeRequest) { + if (mergeRequest.isDeleteSources()) { + log.info( + "Deleting source data value audits as source CategoryOptionCombos are being deleted"); + sources.forEach(dataValueAuditStore::deleteDataValueAudits); + } else { + log.info( + "Leaving source data value audit records as is, source CategoryOptionCombos are not being deleted"); + } + } + + public void handleDataApprovals( + @Nonnull List sources, + @Nonnull CategoryOptionCombo target, + @Nonnull MergeRequest mergeRequest) { + if (DISCARD == mergeRequest.getDataMergeStrategy()) { + log.info("Deleting source data approvals as dataMergeStrategy is DISCARD"); + dataApprovalStore.deleteByCategoryOptionCombo( + UID.of(sources.stream().map(BaseIdentifiableObject::getUid).toList())); + } else { + log.info("Merging source data approvals as dataMergeStrategy is LAST_UPDATED"); + List sourceDas = + dataApprovalStore.getByCategoryOptionCombo( + UID.of(sources.stream().map(BaseIdentifiableObject::getUid).toList())); + + // sort into duplicate & non-duplicates + // get map of target data approvals, using the duplicate key constraints as the key + Map targetDas = + dataApprovalStore.getByCategoryOptionCombo(List.of(UID.of(target.getUid()))).stream() + .collect(Collectors.toMap(getDataApprovalKey, da -> da)); + log.info("{} data approvals retrieved for target categoryOptionCombo", targetDas.size()); + + Map> sourceDuplicateList = + sourceDas.stream() + .collect(Collectors.partitioningBy(dv -> dataApprovalDuplicates.test(dv, targetDas))); + + if (!sourceDuplicateList.get(false).isEmpty()) + handleDaNonDuplicates(sourceDuplicateList.get(false), target); + if (!sourceDuplicateList.get(true).isEmpty()) + handleDaDuplicates(sourceDuplicateList.get(true), targetDas, target, sources); + } + } + + /** + * Deletes {@link DataApprovalAudit}s if the source {@link CategoryOptionCombo}s are being + * deleted. Otherwise, no other action taken. + */ + public void handleDataApprovalAudits( + @Nonnull List sources, @Nonnull MergeRequest mergeRequest) { + if (mergeRequest.isDeleteSources()) { + log.info( + "Deleting source data approval audits as source CategoryOptionCombos are being deleted"); + sources.forEach(dataApprovalAuditStore::deleteDataApprovalAudits); + } else { + log.info( + "Leaving source data approval audit records as is, source CategoryOptionCombos are not being deleted"); + } + } + + /** + * Deletes {@link Event}s if the {@link DataMergeStrategy}s is {@link DataMergeStrategy#DISCARD}. + * Otherwise, reassigns source {@link Event}s attributeOptionCombos to the target {@link + * CategoryOptionCombo} if the {@link DataMergeStrategy}s is {@link + * DataMergeStrategy#LAST_UPDATED}. + */ + public void handleEvents( + @Nonnull List sources, + @Nonnull CategoryOptionCombo target, + @Nonnull MergeRequest mergeRequest) { + if (DISCARD == mergeRequest.getDataMergeStrategy()) { + log.info("Deleting source events as dataMergeStrategy is DISCARD"); + String aocIds = + sources.stream().map(s -> String.valueOf(s.getId())).collect(Collectors.joining(",")); + + List eventsToDelete = + maintenanceStore.getDeletionEntities( + "(select distinct uid from event where attributeoptioncomboid in (%s))" + .formatted(aocIds)); + + String eventSelect = + "(select distinct eventid from event where attributeoptioncomboid in (%s))" + .formatted(aocIds); + + maintenanceStore.hardDeleteEvents( + eventsToDelete, + eventSelect, + "delete from event where attributeoptioncomboid in (%s)".formatted(aocIds)); + } else { + log.info("Merging source events as dataMergeStrategy is LAST_UPDATED"); + + eventStore.setAttributeOptionCombo( + sources.stream().map(BaseIdentifiableObject::getId).collect(Collectors.toSet()), + target.getId()); + } + } + + /** + * Deletes {@link CompleteDataSetRegistration}s if the {@link DataMergeStrategy}s is {@link + * DataMergeStrategy#DISCARD}. Otherwise, if the {@link DataMergeStrategy}s is {@link + * DataMergeStrategy#LAST_UPDATED}, it groups source {@link CompleteDataSetRegistration}s into + * duplicates and non-duplicates for further processing. + */ + public void handleCompleteDataSetRegistrations( + @Nonnull List sources, + @Nonnull CategoryOptionCombo target, + @Nonnull MergeRequest mergeRequest) { + if (DISCARD == mergeRequest.getDataMergeStrategy()) { + log.info("Deleting source complete data set registrations as dataMergeStrategy is DISCARD"); + completeDataSetRegistrationStore.deleteByCategoryOptionCombo(sources); + } else if (LAST_UPDATED == mergeRequest.getDataMergeStrategy()) { + log.info( + "Merging source complete data set registrations as dataMergeStrategy is LAST_UPDATED"); + // get CDSRs from sources + List sourceCdsr = + completeDataSetRegistrationStore.getAllByCategoryOptionCombo( + UID.of(sources.stream().map(BaseIdentifiableObject::getUid).toList())); + + // get map of target cdsr, using the duplicate key constraints as the key + Map targetcdsr = + completeDataSetRegistrationStore + .getAllByCategoryOptionCombo(UID.of(List.of(target.getUid()))) + .stream() + .collect(Collectors.toMap(getCdsrKey, cdsr -> cdsr)); + + Map> sourceDuplicateList = + sourceCdsr.stream() + .collect(Collectors.partitioningBy(cdsr -> cdsrDuplicates.test(cdsr, targetcdsr))); + + if (!sourceDuplicateList.get(false).isEmpty()) { + handleCdsrNonDuplicates(sourceDuplicateList.get(false), target); + } + if (!sourceDuplicateList.get(true).isEmpty()) { + handleCdsrDuplicates(sourceDuplicateList.get(true), targetcdsr, target, sources); + } + } + } + + private void handleCdsrDuplicates( + @Nonnull List sourceCdsrDuplicates, + @Nonnull Map targetCdsr, + @Nonnull CategoryOptionCombo target, + @Nonnull List sources) { + log.info("Merging source complete data set registration duplicates"); + // group CompleteDataSetRegistration by key, so we can deal with each duplicate correctly + Map> sourceCdsrGroupedByKey = + sourceCdsrDuplicates.stream().collect(Collectors.groupingBy(getCdsrKey)); + + // filter groups down to single CDSR with latest date + List filtered = + sourceCdsrGroupedByKey.values().stream() + .map( + ls -> + Collections.max( + ls, Comparator.comparing(CompleteDataSetRegistration::getLastUpdated))) + .toList(); + + for (CompleteDataSetRegistration source : filtered) { + CompleteDataSetRegistration matchingTargetCdsr = targetCdsr.get(getCdsrKey.apply(source)); + + if (matchingTargetCdsr.getLastUpdated().before(source.getLastUpdated())) { + completeDataSetRegistrationStore.deleteCompleteDataSetRegistration(matchingTargetCdsr); + + CompleteDataSetRegistration copyWithNewRef = + CompleteDataSetRegistration.copyWithNewAttributeOptionCombo(source, target); + completeDataSetRegistrationStore.saveWithoutUpdatingLastUpdated(copyWithNewRef); + } + } + + // delete the rest of the source CDSRs after handling the last update duplicate + completeDataSetRegistrationStore.deleteByCategoryOptionCombo(sources); + } + + /** + * Handles non duplicate CompleteDataSetRegistrations. As CompleteDataSetRegistration has a + * composite primary key which includes CategoryOptionCombo, this cannot be updated. A new copy of + * the CompleteDataSetRegistration is required, which uses the target CompleteDataSetRegistration + * as the new ref. + * + * @param sourceCdsr sources to handle + * @param target target to use as new ref in copy + */ + private void handleCdsrNonDuplicates( + @Nonnull List sourceCdsr, @Nonnull CategoryOptionCombo target) { + log.info("Merging source complete data set registration non-duplicates"); + sourceCdsr.forEach( + cdsr -> { + CompleteDataSetRegistration copyWithNewAoc = + CompleteDataSetRegistration.copyWithNewAttributeOptionCombo(cdsr, target); + completeDataSetRegistrationStore.saveWithoutUpdatingLastUpdated(copyWithNewAoc); + }); + + sourceCdsr.forEach(completeDataSetRegistrationStore::deleteCompleteDataSetRegistration); + } + + /** + * Method to handle merging duplicate {@link DataApproval}s. There may be multiple potential + * {@link DataApproval} duplicates. The {@link DataApproval} with the latest `lastUpdated` value + * is filtered out, the rest are then deleted at the end of the process (We can only have one of + * these entries due to the unique key constraint). The filtered-out {@link DataApproval} will be + * compared with the target {@link DataApproval} lastUpdated date. + * + *

If the target date is later, no action is required. + * + *

If the source date is later, the source {@link DataApproval} has its {@link + * CategoryOptionCombo} set as the target. The matching target {@link DataApproval}s will then be + * deleted. + * + * @param sourceDaDuplicates {@link DataApproval}s to merge + * @param targetDaMap target map of {@link DataApproval}s to check duplicates against + * @param target target {@link CategoryOptionCombo} + */ + private void handleDaDuplicates( + @Nonnull Collection sourceDaDuplicates, + @Nonnull Map targetDaMap, + @Nonnull CategoryOptionCombo target, + @Nonnull List sources) { + log.info( + "Handling " + + sourceDaDuplicates.size() + + " duplicate data approvals, keeping later lastUpdated value"); + + // group Data approvals by key, so we can deal with each duplicate correctly + Map> sourceDataApprovalsGroupedByKey = + sourceDaDuplicates.stream().collect(Collectors.groupingBy(getDataApprovalKey)); + + // filter groups down to single DA with latest date + List filtered = + sourceDataApprovalsGroupedByKey.values().stream() + .map(ls -> Collections.max(ls, Comparator.comparing(DataApproval::getLastUpdated))) + .toList(); + + for (DataApproval source : filtered) { + DataApproval matchingTargetDataApproval = targetDaMap.get(getDataApprovalKey.apply(source)); + + if (matchingTargetDataApproval.getLastUpdated().before(source.getLastUpdated())) { + dataApprovalStore.deleteDataApproval(matchingTargetDataApproval); + // flush is required here as it's not possible to update a source DataApproval with + // essentially the same unique constraint key as the target DataApproval until it is removed + // from the Hibernate session + entityManager.flush(); + + source.setAttributeOptionCombo(target); + } + } + + // delete the rest of the source data values after handling the last update duplicate + dataApprovalStore.deleteByCategoryOptionCombo( + UID.of(sources.stream().map(BaseIdentifiableObject::getUid).toList())); + } + + /** + * Method to handle merging non-duplicate {@link DataApproval}s. Source {@link DataApproval}s will + * be assigned the target {@link CategoryOptionCombo} ref. + * + * @param dataApprovals {@link DataApproval}s to merge + * @param target target {@link CategoryOptionCombo} + */ + private void handleDaNonDuplicates( + @Nonnull List dataApprovals, @Nonnull CategoryOptionCombo target) { + log.info( + "Handling " + + dataApprovals.size() + + " non duplicate data approvals. Each will have their attribute option combo set as the target"); + + dataApprovals.forEach(sourceDataApproval -> sourceDataApproval.setAttributeOptionCombo(target)); + } + + private static final Function getCocDataValueKey = + dv -> + String.valueOf(dv.getPeriod().getId()) + + dv.getSource().getId() + + dv.getDataElement().getId() + + dv.getAttributeOptionCombo().getId(); + + private static final Function getDataApprovalKey = + da -> + String.valueOf(da.getPeriod().getId()) + + da.getDataApprovalLevel().getId() + + da.getWorkflow().getId() + + da.getOrganisationUnit().getId(); + + private static final BiPredicate> cocDataValueDuplicates = + (sourceDv, targetDvs) -> targetDvs.containsKey(getCocDataValueKey.apply(sourceDv)); + + private static final BiPredicate> dataApprovalDuplicates = + (sourceDa, targetDas) -> targetDas.containsKey(getDataApprovalKey.apply(sourceDa)); + + private static final Function getAocDataValueKey = + dv -> + String.valueOf(dv.getPeriod().getId()) + + dv.getSource().getId() + + dv.getDataElement().getId() + + dv.getCategoryOptionCombo().getId(); + + private static final BiPredicate> aocDataValueDuplicates = + (sourceDv, targetDvs) -> targetDvs.containsKey(getAocDataValueKey.apply(sourceDv)); + + private static final Function getCdsrKey = + cdsr -> + String.valueOf(cdsr.getPeriod().getId()) + + cdsr.getSource().getId() + + cdsr.getDataSet().getId(); + + private static final BiPredicate< + CompleteDataSetRegistration, Map> + cdsrDuplicates = + (sourceCdsr, targetCdsr) -> targetCdsr.containsKey(getCdsrKey.apply(sourceCdsr)); +} diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/MetadataCategoryOptionComboMergeHandler.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/MetadataCategoryOptionComboMergeHandler.java new file mode 100644 index 000000000000..0c9bf1c40a5a --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/optioncombo/MetadataCategoryOptionComboMergeHandler.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.merge.category.optioncombo; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.hisp.dhis.category.CategoryCombo; +import org.hisp.dhis.category.CategoryComboStore; +import org.hisp.dhis.category.CategoryOption; +import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.category.CategoryOptionStore; +import org.hisp.dhis.common.BaseIdentifiableObject; +import org.hisp.dhis.common.UID; +import org.hisp.dhis.dataelement.DataElementOperand; +import org.hisp.dhis.dataelement.DataElementOperandStore; +import org.hisp.dhis.minmax.MinMaxDataElement; +import org.hisp.dhis.minmax.MinMaxDataElementStore; +import org.hisp.dhis.predictor.Predictor; +import org.hisp.dhis.predictor.PredictorStore; +import org.hisp.dhis.sms.command.code.SMSCode; +import org.hisp.dhis.sms.command.hibernate.SMSCommandStore; +import org.springframework.stereotype.Component; + +/** + * Merge handler for metadata entities. + * + * @author david mackessy + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class MetadataCategoryOptionComboMergeHandler { + + private final CategoryOptionStore categoryOptionStore; + private final CategoryComboStore categoryComboStore; + private final DataElementOperandStore dataElementOperandStore; + private final MinMaxDataElementStore minMaxDataElementStore; + private final PredictorStore predictorStore; + private final SMSCommandStore smsCommandStore; + + /** + * Remove sources from {@link CategoryOption} and add target to {@link CategoryOption} + * + * @param sources to be removed + * @param target to add + */ + public void handleCategoryOptions(List sources, CategoryOptionCombo target) { + log.info("Merging source category options"); + List categoryOptions = + categoryOptionStore.getByCategoryOptionCombo( + UID.of(sources.stream().map(BaseIdentifiableObject::getUid).toList())); + + categoryOptions.forEach( + co -> { + co.addCategoryOptionCombo(target); + co.removeCategoryOptionCombos(sources); + }); + } + + /** + * Remove sources from {@link CategoryCombo} and add target to {@link CategoryCombo} + * + * @param sources to be removed + * @param target to add + */ + public void handleCategoryCombos(List sources, CategoryOptionCombo target) { + log.info("Merging source category combos"); + List categoryCombos = + categoryComboStore.getByCategoryOptionCombo( + UID.of(sources.stream().map(BaseIdentifiableObject::getUid).toList())); + + categoryCombos.forEach( + cc -> { + cc.addCategoryOptionCombo(target); + cc.removeCategoryOptionCombos(sources); + }); + } + + /** + * Set target to {@link DataElementOperand} + * + * @param sources to be actioned + * @param target to add + */ + public void handleDataElementOperands( + List sources, CategoryOptionCombo target) { + log.info("Merging source data element operands"); + List dataElementOperands = + dataElementOperandStore.getByCategoryOptionCombo( + UID.of(sources.stream().map(BaseIdentifiableObject::getUid).toList())); + + dataElementOperands.forEach(deo -> deo.setCategoryOptionCombo(target)); + } + + /** + * Set target to {@link MinMaxDataElement} + * + * @param sources to be actioned + * @param target to add + */ + public void handleMinMaxDataElements( + List sources, CategoryOptionCombo target) { + log.info("Merging source min max data elements"); + List minMaxDataElements = + minMaxDataElementStore.getByCategoryOptionCombo( + UID.of(sources.stream().map(BaseIdentifiableObject::getUid).toList())); + + minMaxDataElements.forEach(mmde -> mmde.setOptionCombo(target)); + } + + /** + * Set target to {@link Predictor} + * + * @param sources to be actioned + * @param target to add + */ + public void handlePredictors(List sources, CategoryOptionCombo target) { + log.info("Merging source predictors"); + List predictors = + predictorStore.getByCategoryOptionCombo( + UID.of(sources.stream().map(BaseIdentifiableObject::getUid).toList())); + + predictors.forEach(p -> p.setOutputCombo(target)); + } + + /** + * Set target to {@link SMSCode} + * + * @param sources to be removed + * @param target to add + */ + public void handleSmsCodes(List sources, CategoryOptionCombo target) { + log.info("Merging source SMS codes"); + List smsCodes = + smsCommandStore.getCodesByCategoryOptionCombo( + UID.of(sources.stream().map(BaseIdentifiableObject::getUid).toList())); + + smsCodes.forEach(smsCode -> smsCode.setOptionId(target)); + } +} diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/dataelement/handler/DataDataElementMergeHandler.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/dataelement/handler/DataDataElementMergeHandler.java index 28f8c1fc2d92..97cae9fa5c7a 100644 --- a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/dataelement/handler/DataDataElementMergeHandler.java +++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/dataelement/handler/DataDataElementMergeHandler.java @@ -27,13 +27,12 @@ */ package org.hisp.dhis.merge.dataelement.handler; -import jakarta.persistence.EntityManager; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; +import static org.hisp.dhis.datavalue.DataValueUtil.dataValueWithNewDataElement; + import java.util.List; import java.util.Map; import java.util.function.BiPredicate; +import java.util.function.Function; import java.util.stream.Collectors; import javax.annotation.Nonnull; import lombok.RequiredArgsConstructor; @@ -43,6 +42,8 @@ import org.hisp.dhis.datavalue.DataValueAudit; import org.hisp.dhis.datavalue.DataValueAuditStore; import org.hisp.dhis.datavalue.DataValueStore; +import org.hisp.dhis.merge.CommonDataMergeHandler; +import org.hisp.dhis.merge.CommonDataMergeHandler.DataValueMergeParams; import org.hisp.dhis.merge.DataMergeStrategy; import org.hisp.dhis.merge.MergeRequest; import org.springframework.stereotype.Component; @@ -59,7 +60,7 @@ public class DataDataElementMergeHandler { private final DataValueStore dataValueStore; private final DataValueAuditStore dataValueAuditStore; - private final EntityManager entityManager; + private final CommonDataMergeHandler commonDataMergeHandler; /** * Method retrieving {@link DataValue}s by source {@link DataElement} references. All retrieved @@ -74,32 +75,33 @@ public void handleDataValueDataElement( @Nonnull List sources, @Nonnull DataElement target, @Nonnull MergeRequest mergeRequest) { - // get DVs from sources - List sourceDataValues = dataValueStore.getAllDataValuesByDataElement(sources); - log.info(sourceDataValues.size() + " source data values retrieved"); - - // get map of target data values, using the duplicate key constraints as the key - Map targetDataValues = - dataValueStore.getAllDataValuesByDataElement(List.of(target)).stream() - .collect(Collectors.toMap(DataDataElementMergeHandler::getDataValueKey, dv -> dv)); - log.info(targetDataValues.size() + " target data values retrieved"); - - // merge based on chosen strategy - DataMergeStrategy dataMergeStrategy = mergeRequest.getDataMergeStrategy(); - if (dataMergeStrategy == DataMergeStrategy.DISCARD) { - log.info(dataMergeStrategy + " dataMergeStrategy being used, deleting source data values"); - sources.forEach(dataValueStore::deleteDataValues); - } else if (dataMergeStrategy == DataMergeStrategy.LAST_UPDATED) { - log.info(dataMergeStrategy + " dataMergeStrategy being used"); - Map> sourceDuplicateList = - sourceDataValues.stream() - .collect( - Collectors.partitioningBy(dv -> dataValueDuplicates.test(dv, targetDataValues))); - - if (!sourceDuplicateList.get(false).isEmpty()) - handleNonDuplicates(sourceDuplicateList.get(false), sources, target); - if (!sourceDuplicateList.get(true).isEmpty()) - handleDuplicates(sourceDuplicateList.get(true), targetDataValues, sources, target); + if (DataMergeStrategy.DISCARD == mergeRequest.getDataMergeStrategy()) { + log.info( + mergeRequest.getDataMergeStrategy() + + " dataMergeStrategy being used, deleting source data values"); + dataValueStore.deleteDataValues(sources); + } else { + // get DVs from sources + List sourceDataValues = dataValueStore.getAllDataValuesByDataElement(sources); + log.info("{} data values retrieved for source DataElements", sourceDataValues.size()); + + // get map of target data values, using the duplicate key constraints as the key + Map targetDataValues = + dataValueStore.getAllDataValuesByDataElement(List.of(target)).stream() + .collect(Collectors.toMap(getDataValueKey, dv -> dv)); + log.info("{} data values retrieved for target DataElement", targetDataValues.size()); + + commonDataMergeHandler.handleDataValues( + new DataValueMergeParams<>( + mergeRequest, + sources, + target, + sourceDataValues, + targetDataValues, + dataValueStore::deleteDataValues, + dataValueDuplicates, + dataValueWithNewDataElement, + getDataValueKey)); } } @@ -121,108 +123,13 @@ public void handleDataValueAuditDataElement( } } - /** - * Method to handle merging duplicate {@link DataValue}s. There may be multiple potential {@link - * DataValue} duplicates. The {@link DataValue} with the latest `lastUpdated` value is filtered - * out, the rest are then deleted at the end of the process (We can only have one of these entries - * due to the composite key constraint). The filtered-out {@link DataValue} will be compared with - * the target {@link DataValue} lastUpdated date. - * - *

If the target date is later, no action is required. - * - *

If the source date is later, then A new {@link DataValue} will be created from the old - * {@link DataValue} values and it will use the target {@link DataElement} ref. This new {@link - * DataValue} will be saved to the database. This sequence is required as a {@link DataValue} has - * a composite primary key which includes {@link DataElement}. This prohibits updating the {@link - * DataElement} ref in a source {@link DataValue}. The matching target {@link DataValue}s will - * then be deleted. - * - * @param sourceDataValuesDuplicates {@link DataValue}s to merge - * @param targetDataValueMap target {@link DataValue}s - * @param sources source {@link DataElement}s - * @param target target {@link DataElement} - */ - private void handleDuplicates( - @Nonnull Collection sourceDataValuesDuplicates, - @Nonnull Map targetDataValueMap, - @Nonnull List sources, - @Nonnull DataElement target) { - log.info( - "Handling " - + sourceDataValuesDuplicates.size() - + " duplicate data values, keeping later lastUpdated value"); - - // group Data values by key so we can deal with each duplicate correctly - Map> sourceDataValuesGroupedByKey = - sourceDataValuesDuplicates.stream() - .collect(Collectors.groupingBy(DataDataElementMergeHandler::getDataValueKey)); - - // filter groups down to single DV with latest date - List filtered = - sourceDataValuesGroupedByKey.values().stream() - .map(ls -> Collections.max(ls, Comparator.comparing(DataValue::getLastUpdated))) - .toList(); - - for (DataValue source : filtered) { - DataValue matchingTargetDataValue = targetDataValueMap.get(getDataValueKey(source)); - - if (matchingTargetDataValue.getLastUpdated().before(source.getLastUpdated())) { - dataValueStore.deleteDataValue(matchingTargetDataValue); - - // detaching is required here as it's not possible to add a new DataValue with essentially - // the same composite primary key - Throws `NonUniqueObjectException: A different object - // with the same identifier value was already associated with the session` - entityManager.detach(matchingTargetDataValue); - DataValue copyWithNewDataElementRef = DataValue.dataValueWithNewDataElement(source, target); - dataValueStore.addDataValue(copyWithNewDataElementRef); - } - } - - // delete the rest of the source data values after handling the last update duplicate - sources.forEach(dataValueStore::deleteDataValues); - } - - /** - * Method to handle merging non-duplicate {@link DataValue}s. A new {@link DataValue} will be - * created from the old {@link DataValue} values, and it will use the target {@link DataElement} - * ref. This new {@link DataValue} will be saved to the database. This sequence is required as a - * {@link DataValue} has a composite primary key which includes {@link DataElement}. This - * prohibits updating the {@link DataElement} ref in a source {@link DataValue}. - * - *

All source {@link DataValue}s will then be deleted. - * - * @param dataValues {@link DataValue}s to merge - * @param sources source {@link DataElement}s - * @param target target {@link DataElement} - */ - private void handleNonDuplicates( - @Nonnull List dataValues, - @Nonnull List sources, - @Nonnull DataElement target) { - log.info( - "Handling " - + dataValues.size() - + " non duplicate data values. Add new DataValue entry (using target DataElement ref) and delete old source entry"); - - dataValues.forEach( - sourceDataValue -> { - DataValue copyWithNewDataElementRef = - DataValue.dataValueWithNewDataElement(sourceDataValue, target); - - dataValueStore.addDataValue(copyWithNewDataElementRef); - }); - - log.info("Deleting all data values referencing source data elements"); - sources.forEach(dataValueStore::deleteDataValues); - } + private static final Function getDataValueKey = + dv -> + String.valueOf(dv.getPeriod().getId()) + + dv.getSource().getId() + + dv.getCategoryOptionCombo().getId() + + dv.getAttributeOptionCombo().getId(); public static final BiPredicate> dataValueDuplicates = - (sourceDv, targetDvs) -> targetDvs.containsKey(getDataValueKey(sourceDv)); - - private static String getDataValueKey(DataValue dv) { - return String.valueOf(dv.getPeriod().getId()) - + dv.getSource().getId() - + dv.getCategoryOptionCombo().getId() - + dv.getAttributeOptionCombo().getId(); - } + (sourceDv, targetDvs) -> targetDvs.containsKey(getDataValueKey.apply(sourceDv)); } diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/resources/data-integrity-checks.yaml b/dhis-2/dhis-services/dhis-service-administration/src/main/resources/data-integrity-checks.yaml index 209893b659a8..53d9e07d9c4a 100644 --- a/dhis-2/dhis-services/dhis-service-administration/src/main/resources/data-integrity-checks.yaml +++ b/dhis-2/dhis-services/dhis-service-administration/src/main/resources/data-integrity-checks.yaml @@ -84,4 +84,5 @@ checks: - users/user_roles_no_authorities.yaml - users/user_roles_with_no_users.yaml - users/users_with_invalid_usernames.yaml + - users/users_with_all_authority.yaml - file_resources/file_resources_no_icon.yaml diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/resources/data-integrity-checks/users/users_with_all_authority.yaml b/dhis-2/dhis-services/dhis-service-administration/src/main/resources/data-integrity-checks/users/users_with_all_authority.yaml new file mode 100644 index 000000000000..c67587c49ae1 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-administration/src/main/resources/data-integrity-checks/users/users_with_all_authority.yaml @@ -0,0 +1,62 @@ +# Copyright (c) 2004-2025, University of Oslo +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# Neither the name of the HISP project nor the names of its contributors may +# be used to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +--- +name: users_with_all_authority +description: Users who have ALL authority assigned. +section: Users +section_order: 6 +summary_sql: >- + SELECT COUNT(*) as value, + 100.0 * COUNT(*) / NULLIF((SELECT COUNT(*) FROM userinfo), 0) as percent + from userinfo where userinfoid IN ( + SELECT userid from userrolemembers where userroleid IN ( + SELECT DISTINCT userroleid from userroleauthorities + where authority = 'ALL' + ) + ); +details_sql: > + SELECT c.uid, + c.username as name, + CASE WHEN c.disabled THEN 'Disabled' ELSE 'Active' END as comment, + array_agg(b.role_name) as refs + from userrolemembers a + INNER JOIN ( + SELECT userroleid, name as role_name from userrole where userroleid in ( + SELECT DISTINCT userroleid from userroleauthorities + where authority = 'ALL' + ) + ) b on a.userroleid = b.userroleid + INNER JOIN userinfo c on a.userid = c.userinfoid + GROUP BY c.uid,c.username,c.disabled +details_id_type: users +severity: INFO +introduction: > + Users with the ALL authority have unrestricted access to all data and functionality in the system. The total + number of users with this authority should be kept to a minimum. +recommendation: > + Using the list of users provided by the details query, consider whether these users should have the ALL authority + assigned. If they should not have the ALL authority, consider removing it from their user roles. \ No newline at end of file diff --git a/dhis-2/dhis-services/dhis-service-administration/src/test/java/org/hisp/dhis/dataintegrity/DataIntegrityYamlReaderTest.java b/dhis-2/dhis-services/dhis-service-administration/src/test/java/org/hisp/dhis/dataintegrity/DataIntegrityYamlReaderTest.java index 77fb67fdd3ba..071db0a83e21 100644 --- a/dhis-2/dhis-services/dhis-service-administration/src/test/java/org/hisp/dhis/dataintegrity/DataIntegrityYamlReaderTest.java +++ b/dhis-2/dhis-services/dhis-service-administration/src/test/java/org/hisp/dhis/dataintegrity/DataIntegrityYamlReaderTest.java @@ -57,7 +57,7 @@ void testReadDataIntegrityYaml() { List checks = new ArrayList<>(); readYaml(checks, "data-integrity-checks.yaml", "data-integrity-checks", CLASS_PATH); - assertEquals(86, checks.size()); + assertEquals(87, checks.size()); // Names should be unique List allNames = checks.stream().map(DataIntegrityCheck::getName).toList(); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/JdbcRawAnalyticsManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/JdbcRawAnalyticsManager.java index 5c364d231b50..9f6263bfae80 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/JdbcRawAnalyticsManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/JdbcRawAnalyticsManager.java @@ -167,7 +167,7 @@ private String getSelectStatement(DataQueryParams params, List getCoordinateFields( - String program, - String coordinateField, - String fallbackCoordinateField, - boolean defaultCoordinateFallback); + List getCoordinateFields(EventDataQueryRequest request); /** * Returns a {@link QueryItem}. diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/AbstractJdbcEventAnalyticsManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/AbstractJdbcEventAnalyticsManager.java index 55a58883bd63..47952b35a9d4 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/AbstractJdbcEventAnalyticsManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/AbstractJdbcEventAnalyticsManager.java @@ -1384,11 +1384,13 @@ protected String getCoalesce(List fields, String defaultColumnName) { String args = fields.stream() - .filter(f -> f != null && !f.isBlank()) + .filter(StringUtils::isNotBlank) .map(f -> sqlBuilder.quoteAx(f)) .collect(Collectors.joining(",")); - return args.isEmpty() ? defaultColumnName : "coalesce(" + args + ")"; + String sql = String.format("coalesce(%s)", args); + + return args.isEmpty() ? defaultColumnName : sql; } /** diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEventCoordinateService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEventCoordinateService.java index 10c66873f3e9..39740ebe917a 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEventCoordinateService.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEventCoordinateService.java @@ -31,7 +31,6 @@ import java.util.ArrayList; import java.util.List; -import javax.annotation.Nonnull; import lombok.RequiredArgsConstructor; import org.hisp.dhis.common.IllegalQueryException; import org.hisp.dhis.common.ValueType; @@ -69,11 +68,11 @@ public class DefaultEventCoordinateService implements EventCoordinateService { public static final List COL_NAME_PROGRAM_NO_REGISTRATION_GEOMETRY_LIST = List.of(COL_NAME_EVENT_GEOMETRY, COL_NAME_ENROLLMENT_GEOMETRY, COL_NAME_OU_GEOMETRY); - @Nonnull private final ProgramService programService; + private final ProgramService programService; - @Nonnull private final DataElementService dataElementService; + private final DataElementService dataElementService; - @Nonnull private final TrackedEntityAttributeService attributeService; + private final TrackedEntityAttributeService attributeService; @Override public boolean isFallbackCoordinateFieldValid(boolean isRegistration, String coordinateField) { @@ -115,23 +114,21 @@ public List getFallbackCoordinateFields( } fallbackCoordinateFields.add(fallbackCoordinateField); - } else { - if (defaultCoordinateFallback) { - List items = - new ArrayList<>( - pr.isRegistration() - ? COL_NAME_GEOMETRY_LIST - : COL_NAME_PROGRAM_NO_REGISTRATION_GEOMETRY_LIST); - - fallbackCoordinateFields.addAll(items); - } + } else if (defaultCoordinateFallback) { + List items = + new ArrayList<>( + pr.isRegistration() + ? COL_NAME_GEOMETRY_LIST + : COL_NAME_PROGRAM_NO_REGISTRATION_GEOMETRY_LIST); + + fallbackCoordinateFields.addAll(items); } return fallbackCoordinateFields; } @Override - public String getCoordinateField(ValueType valueType, String field, ErrorCode errorCode) { + public String validateCoordinateField(ValueType valueType, String field, ErrorCode errorCode) { if (ValueType.COORDINATE != valueType && ValueType.ORGANISATION_UNIT != valueType) { throwIllegalQueryEx(errorCode, field); } @@ -140,13 +137,13 @@ public String getCoordinateField(ValueType valueType, String field, ErrorCode er } @Override - public String getCoordinateField(String program, String coordinateField, ErrorCode errorCode) { + public String validateCoordinateField(String program, String field, ErrorCode errorCode) { Program pr = programService.getProgram(program); - if (COL_NAME_TRACKED_ENTITY_GEOMETRY.equals(coordinateField) && !pr.isRegistration()) { - throwIllegalQueryEx(errorCode, coordinateField); + if (COL_NAME_TRACKED_ENTITY_GEOMETRY.equals(field) && !pr.isRegistration()) { + throwIllegalQueryEx(errorCode, field); } - return coordinateField; + return field; } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEventDataQueryService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEventDataQueryService.java index b08af886985a..533bd5c3229e 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEventDataQueryService.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/DefaultEventDataQueryService.java @@ -139,13 +139,6 @@ public EventQueryParams getFromRequest(EventDataQueryRequest request, boolean an Program pr = programService.getProgram(request.getProgram()); - List coordinateFields = - getCoordinateFields( - request.getProgram(), - request.getCoordinateField(), - request.getFallbackCoordinateField(), - request.isDefaultCoordinateFallback()); - if (pr == null) { throwIllegalQueryEx(ErrorCode.E7129, request.getProgram()); } @@ -156,6 +149,8 @@ public EventQueryParams getFromRequest(EventDataQueryRequest request, boolean an throwIllegalQueryEx(ErrorCode.E7130, request.getStage()); } + List coordinateFields = getCoordinateFields(request); + addDimensionsToParams(params, request, userOrgUnits, pr, idScheme); addFiltersToParams(params, request, userOrgUnits, pr, idScheme); @@ -216,6 +211,7 @@ public EventQueryParams getFromRequest(EventDataQueryRequest request, boolean an EventQueryParams eventQueryParams = builder.build(); // Partitioning applies only when default period is specified + // Empty period dimension means default period if (hasPeriodDimension(eventQueryParams) && hasNotDefaultPeriod(eventQueryParams)) { @@ -419,33 +415,32 @@ public EventQueryParams getFromAnalyticalObject(EventAnalyticalObject object) { * @return the coordinate column list. */ @Override - public List getCoordinateFields( - String program, - String coordinateField, - String fallbackCoordinateField, - boolean defaultCoordinateFallback) { - List coordinateFields = new ArrayList<>(); + public List getCoordinateFields(EventDataQueryRequest request) { + final String program = request.getProgram(); + // TODO Remove when all web apps stop using old names of coordinate fields + final String coordinateField = mapCoordinateField(request.getCoordinateField()); + final boolean defaultCoordinateFallback = request.isDefaultCoordinateFallback(); + final String fallbackCoordinateField = mapCoordinateField(request.getFallbackCoordinateField()); - // TODO!!! remove when all fe apps stop using old names of coordinate fields - coordinateField = mapCoordinateField(coordinateField); - fallbackCoordinateField = mapCoordinateField(fallbackCoordinateField); + List coordinateFields = new ArrayList<>(); if (coordinateField == null) { coordinateFields.add(StringUtils.EMPTY); } else if (COL_NAME_GEOMETRY_LIST.contains(coordinateField)) { coordinateFields.add( - eventCoordinateService.getCoordinateField(program, coordinateField, ErrorCode.E7221)); + eventCoordinateService.validateCoordinateField( + program, coordinateField, ErrorCode.E7221)); } else if (EventQueryParams.EVENT_COORDINATE_FIELD.equals(coordinateField)) { coordinateFields.add( - eventCoordinateService.getCoordinateField( + eventCoordinateService.validateCoordinateField( program, COL_NAME_EVENT_GEOMETRY, ErrorCode.E7221)); } else if (EventQueryParams.ENROLLMENT_COORDINATE_FIELD.equals(coordinateField)) { coordinateFields.add( - eventCoordinateService.getCoordinateField( + eventCoordinateService.validateCoordinateField( program, COL_NAME_ENROLLMENT_GEOMETRY, ErrorCode.E7221)); } else if (EventQueryParams.TRACKER_COORDINATE_FIELD.equals(coordinateField)) { coordinateFields.add( - eventCoordinateService.getCoordinateField( + eventCoordinateService.validateCoordinateField( program, COL_NAME_TRACKED_ENTITY_GEOMETRY, ErrorCode.E7221)); } @@ -453,7 +448,7 @@ public List getCoordinateFields( if (dataElement != null) { coordinateFields.add( - eventCoordinateService.getCoordinateField( + eventCoordinateService.validateCoordinateField( dataElement.getValueType(), coordinateField, ErrorCode.E7219)); } @@ -461,7 +456,7 @@ public List getCoordinateFields( if (attribute != null) { coordinateFields.add( - eventCoordinateService.getCoordinateField( + eventCoordinateService.validateCoordinateField( attribute.getValueType(), coordinateField, ErrorCode.E7220)); } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EnrollmentQueryService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EnrollmentQueryService.java index be80b3566ddb..0f9f1ba769af 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EnrollmentQueryService.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EnrollmentQueryService.java @@ -68,11 +68,12 @@ import org.hisp.dhis.common.DimensionItemKeywords.Keyword; import org.hisp.dhis.common.Grid; import org.hisp.dhis.common.GridHeader; +import org.hisp.dhis.db.sql.SqlBuilder; import org.hisp.dhis.system.grid.ListGrid; import org.hisp.dhis.util.Timer; import org.springframework.stereotype.Service; -/** This service is responsible for querying enrollments. */ +/** Service responsible for querying enrollments. */ @Service @RequiredArgsConstructor public class EnrollmentQueryService { @@ -89,6 +90,8 @@ public class EnrollmentQueryService { private final SchemeIdHandler schemeIdHandler; + private final SqlBuilder sqlBuilder; + /** * Returns a list of enrollments matching the given query. * @@ -96,33 +99,39 @@ public class EnrollmentQueryService { * @return enrollments data as a {@link Grid} object. */ public Grid getEnrollments(EventQueryParams params) { - // Check access/constraints. + // Security + securityManager.decideAccessEventQuery(params); params = securityManager.withUserConstraints(params); - // Validate request. + // Validation + queryValidator.validate(params); List keywords = getDimensionsKeywords(params); - // Set periods. params = new EventQueryParams.Builder(params).withStartEndDatesForPeriods().build(); - // Populate headers. + // Headers + Grid grid = createGridWithHeaders(params); addCommonHeaders(grid, params, List.of()); - // Add data. + // Data + long count = 0; if (!params.isSkipData() || params.analyzeOnly()) { count = addData(grid, params); } - // Set response info. + // Metadata + metadataHandler.addMetadata(grid, params, keywords); schemeIdHandler.applyScheme(grid, params); + // Paging + addPaging(params, count, grid); applyHeaders(grid, params); setRowContextColumns(grid); @@ -134,11 +143,12 @@ public Grid getEnrollments(EventQueryParams params) { * Creates a {@link Grid} object with default headers. * * @param params the {@link EventQueryParams}. - * @return the {@link Grid} with initial headers. + * @return the {@link Grid} with headers. */ private Grid createGridWithHeaders(EventQueryParams params) { - return new ListGrid() - .addHeader( + Grid grid = new ListGrid(); + + grid.addHeader( new GridHeader( ENROLLMENT.getItem(), getEnrollmentLabel(params.getProgram(), ENROLLMENT.getName()), @@ -177,11 +187,15 @@ private Grid createGridWithHeaders(EventQueryParams params) { false, true)) .addHeader( - new GridHeader(LAST_UPDATED.getItem(), LAST_UPDATED.getName(), DATETIME, false, true)) - .addHeader(new GridHeader(GEOMETRY.getItem(), GEOMETRY.getName(), TEXT, false, true)) - .addHeader(new GridHeader(LONGITUDE.getItem(), LONGITUDE.getName(), NUMBER, false, true)) - .addHeader(new GridHeader(LATITUDE.getItem(), LATITUDE.getName(), NUMBER, false, true)) - .addHeader( + new GridHeader(LAST_UPDATED.getItem(), LAST_UPDATED.getName(), DATETIME, false, true)); + + if (sqlBuilder.supportsGeospatialData()) { + grid.addHeader(new GridHeader(GEOMETRY.getItem(), GEOMETRY.getName(), TEXT, false, true)) + .addHeader(new GridHeader(LONGITUDE.getItem(), LONGITUDE.getName(), NUMBER, false, true)) + .addHeader(new GridHeader(LATITUDE.getItem(), LATITUDE.getName(), NUMBER, false, true)); + } + + grid.addHeader( new GridHeader( ORG_UNIT_NAME.getItem(), getOrgUnitLabel(params.getProgram(), ORG_UNIT_NAME.getName()), @@ -199,14 +213,15 @@ private Grid createGridWithHeaders(EventQueryParams params) { new GridHeader(ORG_UNIT_CODE.getItem(), ORG_UNIT_CODE.getName(), TEXT, false, true)) .addHeader( new GridHeader(PROGRAM_STATUS.getItem(), PROGRAM_STATUS.getName(), TEXT, false, true)); + + return grid; } /** * Adds data into the given grid, based on the given params. * * @param grid {@link Grid}. - * @param params the {@link EventQueryParams}. @@param maxLimit the max number of records to - * retrieve. + * @param params the {@link EventQueryParams}. */ private long addData(Grid grid, EventQueryParams params) { Timer timer = new Timer().start().disablePrint(); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EventAggregateService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EventAggregateService.java index 580cedbd69fe..1ca2f0eb043b 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EventAggregateService.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EventAggregateService.java @@ -210,12 +210,12 @@ private Grid getAggregatedDataGrid(EventQueryParams params) { addHeaders(params, grid); addData(grid, params, maxLimit); - // Sort, done again due to potential multiple partitions. + // Sort grid, done again due to potential multiple partitions if (params.hasSortOrder() && grid.getHeight() > 0) { grid.sortGrid(1, params.getSortOrderAsInt()); } - // Limit the grid, if asked for. + // Limit grid if (params.hasLimit() && grid.getHeight() > params.getLimit()) { grid.limitGrid(params.getLimit()); } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EventCoordinateService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EventCoordinateService.java index 4f89b6319ab1..f765760483f1 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EventCoordinateService.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EventCoordinateService.java @@ -39,10 +39,10 @@ public interface EventCoordinateService { /** * Verifies the validity of fallback coordinate field. * - * @param isRegistration true when underlying program is registration - * @param coordinateField the name of coordinate field (uid or name) - * @return returns true if valid. - * @throws IllegalQueryException + * @param isRegistration true when program is registration. + * @param coordinateField the name of coordinate field (identifier or name). + * @return true if valid. + * @throws IllegalQueryException if validation failed. */ boolean isFallbackCoordinateFieldValid(boolean isRegistration, String coordinateField) throws IllegalQueryException; @@ -50,36 +50,36 @@ boolean isFallbackCoordinateFieldValid(boolean isRegistration, String coordinate /** * Provides list of coordinate fields. * - * @param program underlying program - * @param fallbackCoordinateField fallback - * @param defaultCoordinateFallback fallback cascade should be applied when true - * @return list of coordinate fields - * @throws IllegalQueryException + * @param program the program identifier. + * @param fallbackCoordinateField the fallback coordinate field. + * @param defaultCoordinateFallback fallback cascade should be applied when true. + * @return a list of coordinate fields. + * @throws IllegalQueryException if validation failed. */ List getFallbackCoordinateFields( String program, String fallbackCoordinateField, boolean defaultCoordinateFallback); /** - * Provides verified geometry. + * Validates the given coordinate field. * - * @param valueType value type - * @param field geometry + * @param valueType the {@link ValueType}. + * @param field the coordinate field. * @param errorCode code for standard error message * @return the coordinate field. - * @throws IllegalQueryException + * @throws IllegalQueryException if validation failed. */ - String getCoordinateField(ValueType valueType, String field, ErrorCode errorCode) + String validateCoordinateField(ValueType valueType, String field, ErrorCode errorCode) throws IllegalQueryException; /** - * Provides verified geometry. + * Validates the given coordinate field. * - * @param program underlying program - * @param coordinateField geometry - * @param errorCode code for standard error message + * @param program the program identifier. + * @param field the coordinate field. + * @param errorCode the {@link ErrorCode}. * @return the coordinate field. - * @throws IllegalQueryException + * @throws IllegalQueryException if validation failed. */ - public String getCoordinateField(String program, String coordinateField, ErrorCode errorCode) + public String validateCoordinateField(String program, String field, ErrorCode errorCode) throws IllegalQueryException; } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EventQueryService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EventQueryService.java index 1bef2394a06a..09fb1b9c0578 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EventQueryService.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/EventQueryService.java @@ -70,7 +70,6 @@ import static org.hisp.dhis.feedback.ErrorCode.E7218; import java.util.List; -import javax.annotation.PostConstruct; import lombok.RequiredArgsConstructor; import org.hisp.dhis.analytics.AnalyticsSecurityManager; import org.hisp.dhis.analytics.Rectangle; @@ -83,12 +82,12 @@ import org.hisp.dhis.common.DimensionItemKeywords.Keyword; import org.hisp.dhis.common.Grid; import org.hisp.dhis.common.GridHeader; -import org.hisp.dhis.system.database.DatabaseInfoProvider; +import org.hisp.dhis.db.sql.SqlBuilder; import org.hisp.dhis.system.grid.ListGrid; import org.hisp.dhis.util.Timer; import org.springframework.stereotype.Service; -/** This service is responsible for querying events. */ +/** Service responsible for querying events. */ @Service @RequiredArgsConstructor public class EventQueryService { @@ -101,18 +100,11 @@ public class EventQueryService { private final EventQueryPlanner queryPlanner; - private final DatabaseInfoProvider databaseInfoProvider; - private final MetadataItemsHandler metadataHandler; private final SchemeIdHandler schemeIdHandler; - private boolean spatialSupport; - - @PostConstruct - void init() { - this.spatialSupport = databaseInfoProvider.getDatabaseInfo().isSpatialSupport(); - } + private final SqlBuilder sqlBuilder; /** * Returns a list of events matching the given query. @@ -121,33 +113,39 @@ void init() { * @return events as a {@Grid} object. */ public Grid getEvents(EventQueryParams params) { - // Check access/constraints. + // Security + securityManager.decideAccessEventQuery(params); params = securityManager.withUserConstraints(params); - // Validate request. + // Validation + queryValidator.validate(params); List keywords = getDimensionsKeywords(params); - // Set periods. params = new EventQueryParams.Builder(params).withStartEndDatesForPeriods().build(); - // Populate headers. + // Headers + Grid grid = createGridWithHeaders(params); addCommonHeaders(grid, params, List.of()); - // Add data. + // Data + long count = 0; if (!params.isSkipData() || params.analyzeOnly()) { count = addData(grid, params); } - // Set response info. + // Metadata + metadataHandler.addMetadata(grid, params, keywords); schemeIdHandler.applyScheme(grid, params); + // Paging + addPaging(params, count, grid); applyHeaders(grid, params); setRowContextColumns(grid); @@ -162,7 +160,7 @@ public Grid getEvents(EventQueryParams params) { * @return event clusters as a {@link Grid} object. */ public Grid getEventClusters(EventQueryParams params) { - if (!spatialSupport) { + if (!isGeospatialSupport()) { throwIllegalQueryEx(E7218); } @@ -176,11 +174,12 @@ public Grid getEventClusters(EventQueryParams params) { queryValidator.validate(params); - Grid grid = new ListGrid(); - grid.addHeader(new GridHeader(COUNT.getItem(), COUNT.getName(), NUMBER, false, false)) - .addHeader(new GridHeader(CENTER.getItem(), CENTER.getName(), TEXT, false, false)) - .addHeader(new GridHeader(EXTENT.getItem(), EXTENT.getName(), TEXT, false, false)) - .addHeader(new GridHeader(POINTS.getItem(), POINTS.getName(), TEXT, false, false)); + Grid grid = + new ListGrid() + .addHeader(new GridHeader(COUNT.getItem(), COUNT.getName(), NUMBER, false, false)) + .addHeader(new GridHeader(CENTER.getItem(), CENTER.getName(), TEXT, false, false)) + .addHeader(new GridHeader(EXTENT.getItem(), EXTENT.getName(), TEXT, false, false)) + .addHeader(new GridHeader(POINTS.getItem(), POINTS.getName(), TEXT, false, false)); params = queryPlanner.planEventQuery(params); @@ -197,7 +196,7 @@ public Grid getEventClusters(EventQueryParams params) { * @return event clusters as a {@link Grid} object. */ public Rectangle getRectangle(EventQueryParams params) { - if (!spatialSupport) { + if (!isGeospatialSupport()) { throwIllegalQueryEx(E7218); } @@ -292,10 +291,13 @@ private Grid createGridWithHeaders(EventQueryParams params) { PROGRAM_INSTANCE.getItem(), PROGRAM_INSTANCE.getName(), TEXT, false, true)); } - grid.addHeader(new GridHeader(GEOMETRY.getItem(), GEOMETRY.getName(), TEXT, false, true)) - .addHeader(new GridHeader(LONGITUDE.getItem(), LONGITUDE.getName(), NUMBER, false, true)) - .addHeader(new GridHeader(LATITUDE.getItem(), LATITUDE.getName(), NUMBER, false, true)) - .addHeader( + if (isGeospatialSupport()) { + grid.addHeader(new GridHeader(GEOMETRY.getItem(), GEOMETRY.getName(), TEXT, false, true)) + .addHeader(new GridHeader(LONGITUDE.getItem(), LONGITUDE.getName(), NUMBER, false, true)) + .addHeader(new GridHeader(LATITUDE.getItem(), LATITUDE.getName(), NUMBER, false, true)); + } + + grid.addHeader( new GridHeader( ORG_UNIT_NAME.getItem(), getOrgUnitLabel(params.getProgram(), ORG_UNIT_NAME.getName()), @@ -348,4 +350,13 @@ private long addData(Grid grid, EventQueryParams params) { return count; } + + /** + * Indicates whether the DBMS supports geospatial data types and functions. + * + * @return true if the DBMS supports geospatial data types and functions. + */ + private boolean isGeospatialSupport() { + return sqlBuilder.supportsGeospatialData(); + } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java index 914007f42909..da2129f70c2f 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEnrollmentAnalyticsManager.java @@ -75,6 +75,7 @@ import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.program.AnalyticsType; import org.hisp.dhis.program.ProgramIndicatorService; +import org.hisp.dhis.system.util.ListBuilder; import org.locationtech.jts.util.Assert; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.jdbc.InvalidResultSetAccessException; @@ -102,23 +103,9 @@ public class JdbcEnrollmentAnalyticsManager extends AbstractJdbcEventAnalyticsMa private static final String IS_NOT_NULL = " is not null "; - private static final List COLUMNS = - List.of( - EnrollmentAnalyticsColumnName.ENROLLMENT_COLUMN_NAME, - EnrollmentAnalyticsColumnName.TRACKED_ENTITY_COLUMN_NAME, - EnrollmentAnalyticsColumnName.ENROLLMENT_DATE_COLUMN_NAME, - EnrollmentAnalyticsColumnName.OCCURRED_DATE_COLUMN_NAME, - EnrollmentAnalyticsColumnName.STORED_BY_COLUMN_NAME, - EnrollmentAnalyticsColumnName.CREATED_BY_DISPLAY_NAME_COLUMN_NAME, - EnrollmentAnalyticsColumnName.LAST_UPDATED_BY_DISPLAY_NAME_COLUMN_NAME, - EnrollmentAnalyticsColumnName.LAST_UPDATED_COLUMN_NAME, - "ST_AsGeoJSON(" + EnrollmentAnalyticsColumnName.ENROLLMENT_GEOMETRY_COLUMN_NAME + ")", - EnrollmentAnalyticsColumnName.LONGITUDE_COLUMN_NAME, - EnrollmentAnalyticsColumnName.LATITUDE_COLUMN_NAME, - EnrollmentAnalyticsColumnName.OU_NAME_COLUMN_NAME, - AbstractJdbcTableManager.OU_NAME_HIERARCHY_COLUMN_NAME, - EnrollmentAnalyticsColumnName.OU_CODE_COLUMN_NAME, - EnrollmentAnalyticsColumnName.ENROLLMENT_STATUS_COLUMN_NAME); + private static final String COLUMN_ENROLLMENT_GEOMETRY_GEOJSON = + String.format( + "ST_AsGeoJSON(%s)", EnrollmentAnalyticsColumnName.ENROLLMENT_GEOMETRY_COLUMN_NAME); public JdbcEnrollmentAnalyticsManager( @Qualifier("analyticsJdbcTemplate") JdbcTemplate jdbcTemplate, @@ -197,11 +184,11 @@ private void getEnrollments( } /** - * The method retrieves the amount of the supportive columns in database result set + * Retrieves the amount of the supportive columns in database result set. * * @param rowSet {@link SqlRowSet}. * @param columnName The name of the investigated column. - * @return If the investigated column has some supportive columns lie .exists or .status, the + * @return if the investigated column has some supportive columns like .exists or .status, the * count of the columns is returned. */ private long getRowSetOriginItems(SqlRowSet rowSet, String columnName) { @@ -214,7 +201,7 @@ private long getRowSetOriginItems(SqlRowSet rowSet, String columnName) { } /** - * Add value meta info into the grid. Value meta info is information about origin of the + * Adds value meta info into the grid. Value meta info is information about origin of the * repeatable stage value. * * @param grid the {@link Grid}. @@ -493,7 +480,7 @@ protected String getWhereClause(EventQueryParams params) { protected String getSelectClause(EventQueryParams params) { List selectCols = ListUtils.distinctUnion( - params.isAggregatedEnrollments() ? List.of("enrollment") : COLUMNS, + params.isAggregatedEnrollments() ? List.of("enrollment") : getStandardColumns(), getSelectColumns(params, false)); return "select " + StringUtils.join(selectCols, ",") + " "; @@ -661,6 +648,40 @@ protected String getColumn(QueryItem item, String suffix) { } } + /** + * Returns a list of names of standard columns. + * + * @return a list of names of standard columns. + */ + private List getStandardColumns() { + ListBuilder columns = new ListBuilder<>(); + + columns.add( + EnrollmentAnalyticsColumnName.ENROLLMENT_COLUMN_NAME, + EnrollmentAnalyticsColumnName.TRACKED_ENTITY_COLUMN_NAME, + EnrollmentAnalyticsColumnName.ENROLLMENT_DATE_COLUMN_NAME, + EnrollmentAnalyticsColumnName.OCCURRED_DATE_COLUMN_NAME, + EnrollmentAnalyticsColumnName.STORED_BY_COLUMN_NAME, + EnrollmentAnalyticsColumnName.CREATED_BY_DISPLAY_NAME_COLUMN_NAME, + EnrollmentAnalyticsColumnName.LAST_UPDATED_BY_DISPLAY_NAME_COLUMN_NAME, + EnrollmentAnalyticsColumnName.LAST_UPDATED_COLUMN_NAME); + + if (sqlBuilder.supportsGeospatialData()) { + columns.add( + COLUMN_ENROLLMENT_GEOMETRY_GEOJSON, + EnrollmentAnalyticsColumnName.LONGITUDE_COLUMN_NAME, + EnrollmentAnalyticsColumnName.LATITUDE_COLUMN_NAME); + } + + columns.add( + EnrollmentAnalyticsColumnName.OU_NAME_COLUMN_NAME, + AbstractJdbcTableManager.OU_NAME_HIERARCHY_COLUMN_NAME, + EnrollmentAnalyticsColumnName.OU_CODE_COLUMN_NAME, + EnrollmentAnalyticsColumnName.ENROLLMENT_STATUS_COLUMN_NAME); + + return columns.build(); + } + /** * Returns true if the item is a program attribute and the value type is an organizational unit. * diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEventAnalyticsManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEventAnalyticsManager.java index 667a139a2209..23da08181fd1 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEventAnalyticsManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/event/data/JdbcEventAnalyticsManager.java @@ -37,7 +37,7 @@ import static org.hisp.dhis.analytics.common.ColumnHeader.LATITUDE; import static org.hisp.dhis.analytics.common.ColumnHeader.LONGITUDE; import static org.hisp.dhis.analytics.event.data.OrgUnitTableJoiner.joinOrgUnitTables; -import static org.hisp.dhis.analytics.table.JdbcEventAnalyticsTableManager.OU_GEOMETRY_COL_SUFFIX; +import static org.hisp.dhis.analytics.table.AbstractEventJdbcTableManager.OU_GEOMETRY_COL_SUFFIX; import static org.hisp.dhis.analytics.util.AnalyticsUtils.withExceptionHandling; import static org.hisp.dhis.common.DimensionalObject.ORGUNIT_DIM_ID; import static org.hisp.dhis.common.IdentifiableObjectUtils.getUids; @@ -47,7 +47,6 @@ import static org.hisp.dhis.util.DateUtils.toMediumDate; import static org.postgresql.util.PSQLState.DIVISION_BY_ZERO; -import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import java.util.ArrayList; import java.util.Date; @@ -86,6 +85,7 @@ import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.program.AnalyticsType; import org.hisp.dhis.program.ProgramIndicatorService; +import org.hisp.dhis.system.util.ListBuilder; import org.postgresql.util.PSQLException; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.dao.DataAccessResourceFailureException; @@ -263,12 +263,15 @@ public long getEventCount(EventQueryParams params) { @Override public Rectangle getRectangle(EventQueryParams params) { + String coalesceClause = + getCoalesce( + params.getCoordinateFields(), FallbackCoordinateFieldType.EVENT_GEOMETRY.getValue()); + String sql = "select count(event) as " + COL_COUNT + ", ST_Extent(" - + getCoalesce( - params.getCoordinateFields(), FallbackCoordinateFieldType.EVENT_GEOMETRY.getValue()) + + coalesceClause + ") as " + COL_EXTENT + " "; @@ -306,6 +309,11 @@ private SqlRowSet queryForRows(String sql) { } } + @Override + protected AnalyticsType getAnalyticsType() { + return AnalyticsType.EVENT; + } + // ------------------------------------------------------------------------- // Supportive methods // ------------------------------------------------------------------------- @@ -317,44 +325,70 @@ private SqlRowSet queryForRows(String sql) { */ @Override protected String getSelectClause(EventQueryParams params) { - ImmutableList.Builder cols = - new ImmutableList.Builder() - .add( - EventAnalyticsColumnName.EVENT_COLUMN_NAME, - EventAnalyticsColumnName.PS_COLUMN_NAME, - EventAnalyticsColumnName.OCCURRED_DATE_COLUMN_NAME, - EventAnalyticsColumnName.STORED_BY_COLUMN_NAME, - EventAnalyticsColumnName.CREATED_BY_DISPLAYNAME_COLUMN_NAME, - EventAnalyticsColumnName.LAST_UPDATED_BY_DISPLAYNAME_COLUMN_NAME, - EventAnalyticsColumnName.LAST_UPDATED_COLUMN_NAME, - EventAnalyticsColumnName.SCHEDULED_DATE_COLUMN_NAME); + List standardColumns = getStandardColumns(params); + + List selectCols = + ListUtils.distinctUnion(standardColumns, getSelectColumns(params, false)); + + return "select " + StringUtils.join(selectCols, ",") + " "; + } + + /** + * Returns a list of names of standard columns. + * + * @param params the {@link EventQueryParams}. + * @return a list of names of standard columns. + */ + private List getStandardColumns(EventQueryParams params) { + ListBuilder columns = new ListBuilder<>(); + + columns.add( + EventAnalyticsColumnName.EVENT_COLUMN_NAME, + EventAnalyticsColumnName.PS_COLUMN_NAME, + EventAnalyticsColumnName.OCCURRED_DATE_COLUMN_NAME, + EventAnalyticsColumnName.STORED_BY_COLUMN_NAME, + EventAnalyticsColumnName.CREATED_BY_DISPLAYNAME_COLUMN_NAME, + EventAnalyticsColumnName.LAST_UPDATED_BY_DISPLAYNAME_COLUMN_NAME, + EventAnalyticsColumnName.LAST_UPDATED_COLUMN_NAME, + EventAnalyticsColumnName.SCHEDULED_DATE_COLUMN_NAME); if (params.getProgram().isRegistration()) { - cols.add( + columns.add( EventAnalyticsColumnName.ENROLLMENT_DATE_COLUMN_NAME, EventAnalyticsColumnName.ENROLLMENT_OCCURRED_DATE_COLUMN_NAME, EventAnalyticsColumnName.TRACKED_ENTITY_COLUMN_NAME, EventAnalyticsColumnName.ENROLLMENT_COLUMN_NAME); } - String coordinatesFieldsSnippet = - getCoalesce( - params.getCoordinateFields(), FallbackCoordinateFieldType.EVENT_GEOMETRY.getValue()); + if (sqlBuilder.supportsGeospatialData()) { + columns.add( + getCoordinateSelectExpression(params), + EventAnalyticsColumnName.LONGITUDE_COLUMN_NAME, + EventAnalyticsColumnName.LATITUDE_COLUMN_NAME); + } - cols.add( - "ST_AsGeoJSON(" + coordinatesFieldsSnippet + ", 6) as geometry", - EventAnalyticsColumnName.LONGITUDE_COLUMN_NAME, - EventAnalyticsColumnName.LATITUDE_COLUMN_NAME, + columns.add( EventAnalyticsColumnName.OU_NAME_COLUMN_NAME, AbstractJdbcTableManager.OU_NAME_HIERARCHY_COLUMN_NAME, EventAnalyticsColumnName.OU_CODE_COLUMN_NAME, EventAnalyticsColumnName.ENROLLMENT_STATUS_COLUMN_NAME, EventAnalyticsColumnName.EVENT_STATUS_COLUMN_NAME); - List selectCols = - ListUtils.distinctUnion(cols.build(), getSelectColumns(params, false)); + return columns.build(); + } - return "select " + StringUtils.join(selectCols, ",") + " "; + /** + * Returns a coordinate coalesce select expression. + * + * @param params the {@link EventQueryParams}. + * @return a coordinate coalesce select expression. + */ + private String getCoordinateSelectExpression(EventQueryParams params) { + String field = + getCoalesce( + params.getCoordinateFields(), FallbackCoordinateFieldType.EVENT_GEOMETRY.getValue()); + + return String.format("ST_AsGeoJSON(%s, 6) as geometry", field); } /** @@ -797,11 +831,6 @@ private String getProgramIndicatorSql(EventQueryParams params) { params.getLatestEndDate()); } - @Override - protected AnalyticsType getAnalyticsType() { - return AnalyticsType.EVENT; - } - /** * If the coordinateField points to an Item of type ORG UNIT, add the "_geom" suffix to the field * name. diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/outlier/OutlierHelper.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/outlier/OutlierHelper.java index 1215f61233de..5492631c8e21 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/outlier/OutlierHelper.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/outlier/OutlierHelper.java @@ -75,7 +75,10 @@ public static String getOrgUnitPathClause( StringBuilder sql = new StringBuilder(relation + " ("); orgUnits.forEach( ou -> - sql.append(pathAlias).append(".\"path\" like '").append(ou.getPath()).append("%' or ")); + sql.append(pathAlias) + .append(".\"path\" like '") + .append(ou.getStoredPath()) + .append("%' or ")); return StringUtils.trim(TextUtils.removeLastOr(sql.toString())) + ")"; } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractEventJdbcTableManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractEventJdbcTableManager.java index f805967cbae5..092b97c7b657 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractEventJdbcTableManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractEventJdbcTableManager.java @@ -124,7 +124,7 @@ protected String getColumnExpression(ValueType valueType, String columnExpressio columnExpression + " = 'true'", "1", columnExpression + " = 'false'", "0", "null"); } else if (valueType.isDate()) { return getCastExpression(columnExpression, DATE_REGEXP, sqlBuilder.dataTypeTimestamp()); - } else if (valueType.isGeo() && isSpatialSupport()) { + } else if (valueType.isGeo() && isGeospatialSupport()) { return String.format( """ ST_GeomFromGeoJSON('{"type":"Point", "coordinates":' || (%s) || \ @@ -185,7 +185,7 @@ protected List getColumnForAttribute(TrackedEntityAttribut List columns = new ArrayList<>(); String valueColumn = getValueColumn(attribute); - DataType dataType = getColumnType(attribute.getValueType(), isSpatialSupport()); + DataType dataType = getColumnType(attribute.getValueType(), isGeospatialSupport()); String selectExpression = getColumnExpression(attribute.getValueType(), valueColumn); Skip skipIndex = skipIndex(attribute.getValueType(), attribute.hasOptionSet()); @@ -220,7 +220,7 @@ private List getColumnForOrgUnitAttribute( Validate.isTrue(attribute.getValueType().isOrganisationUnit()); List columns = new ArrayList<>(); - if (isSpatialSupport()) { + if (isGeospatialSupport()) { columns.add( AnalyticsTableColumn.builder() .name((attribute.getUid() + OU_GEOMETRY_COL_SUFFIX)) diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractJdbcTableManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractJdbcTableManager.java index 732c93737af6..0507ab88dd38 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractJdbcTableManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractJdbcTableManager.java @@ -349,15 +349,19 @@ private boolean tableExists(String name) { // ------------------------------------------------------------------------- /** - * Indicates whether spatial support is available. + * Indicates whether the DBMS supports geospatial data types and functions. * - * @return true if spatial support is available. + * @return true if the DBMS supports geospatial data types and functions. */ - protected boolean isSpatialSupport() { - return analyticsTableSettings.isSpatialSupport() && sqlBuilder.supportsGeospatialData(); + protected boolean isGeospatialSupport() { + return sqlBuilder.supportsGeospatialData(); } - /** Returns the analytics table name. */ + /** + * Returns the analytics table name. + * + * @return the analytics table name. + */ protected String getTableName() { return getAnalyticsTableType().getTableName(); } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManager.java index 2a07e0b69b89..6fa5118509ca 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManager.java @@ -140,7 +140,7 @@ public List getAnalyticsTables(AnalyticsTableUpdateParams params log.info( "Get tables using earliest: {}, spatial support: {}", params.getFromDate(), - isSpatialSupport()); + isGeospatialSupport()); List availableDataYears = periodDataProvider.getAvailableYears(analyticsTableSettings.getPeriodSource()); @@ -487,7 +487,7 @@ private List getColumnForDataElement( DataElement dataElement, boolean withLegendSet) { List columns = new ArrayList<>(); - DataType dataType = getColumnType(dataElement.getValueType(), isSpatialSupport()); + DataType dataType = getColumnType(dataElement.getValueType(), isGeospatialSupport()); String jsonExpression = sqlBuilder.jsonExtract("eventdatavalues", dataElement.getUid(), "value"); String columnExpression = getColumnExpression(dataElement.getValueType(), jsonExpression); @@ -540,7 +540,7 @@ private List getColumnForOrgUnitDataElement(DataElement da List columns = new ArrayList<>(); - if (isSpatialSupport()) { + if (isGeospatialSupport()) { columns.add( AnalyticsTableColumn.builder() .name((dataElement.getUid() + OU_GEOMETRY_COL_SUFFIX)) diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityAnalyticsTableManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityAnalyticsTableManager.java index ceecc38220db..778a38f997e4 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityAnalyticsTableManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityAnalyticsTableManager.java @@ -213,7 +213,7 @@ private List getColumns( tea -> AnalyticsTableColumn.builder() .name(tea.getUid()) - .dataType(getColumnType(tea.getValueType(), isSpatialSupport())) + .dataType(getColumnType(tea.getValueType(), isGeospatialSupport())) .selectExpression( getColumnExpression(tea.getValueType(), quote(tea.getUid()) + ".value")) .build()) diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/setting/AnalyticsTableSettings.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/setting/AnalyticsTableSettings.java index e77f44a4ca82..ed8746dabae7 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/setting/AnalyticsTableSettings.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/setting/AnalyticsTableSettings.java @@ -28,33 +28,25 @@ package org.hisp.dhis.analytics.table.setting; import static org.apache.commons.lang3.StringUtils.isBlank; -import static org.hisp.dhis.commons.util.TextUtils.format; import static org.hisp.dhis.db.model.Logged.LOGGED; import static org.hisp.dhis.db.model.Logged.UNLOGGED; -import static org.hisp.dhis.external.conf.ConfigurationKey.ANALYTICS_DATABASE; import static org.hisp.dhis.external.conf.ConfigurationKey.ANALYTICS_TABLE_SKIP_COLUMN; import static org.hisp.dhis.external.conf.ConfigurationKey.ANALYTICS_TABLE_SKIP_INDEX; import static org.hisp.dhis.external.conf.ConfigurationKey.ANALYTICS_TABLE_UNLOGGED; import static org.hisp.dhis.period.PeriodDataProvider.PeriodSource.DATABASE; import static org.hisp.dhis.period.PeriodDataProvider.PeriodSource.SYSTEM_DEFINED; -import static org.hisp.dhis.util.ObjectUtils.isNull; import com.google.common.collect.Lists; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.EnumUtils; -import org.apache.commons.lang3.StringUtils; import org.hisp.dhis.analytics.table.model.Skip; -import org.hisp.dhis.db.model.Database; import org.hisp.dhis.db.model.Logged; import org.hisp.dhis.external.conf.DhisConfigurationProvider; import org.hisp.dhis.period.PeriodDataProvider.PeriodSource; import org.hisp.dhis.setting.SystemSettings; import org.hisp.dhis.setting.SystemSettingsProvider; -import org.hisp.dhis.system.database.DatabaseInfo; -import org.hisp.dhis.system.database.DatabaseInfoProvider; import org.springframework.stereotype.Component; /** @@ -70,8 +62,6 @@ public class AnalyticsTableSettings { private final SystemSettingsProvider settingsProvider; - private final DatabaseInfoProvider databaseInfoProvider; - /** * Returns the setting indicating whether resource and analytics tables should be logged or * unlogged. @@ -134,39 +124,6 @@ public Set getSkipColumnDimensions() { return toSet(config.getProperty(ANALYTICS_TABLE_SKIP_COLUMN)); } - /** - * Indicates whether spatial database support is available. - * - * @return true if spatial database support is available. - */ - public boolean isSpatialSupport() { - DatabaseInfo info = databaseInfoProvider.getDatabaseInfo(); - return info != null && info.isSpatialSupport(); - } - - /** - * Returns the {@link Database} matching the given value. - * - * @param value the string value. - * @return the {@link Database}. - * @throws IllegalArgumentException if the value does not match a valid option. - */ - Database getAndValidateDatabase(String value) { - Database database = EnumUtils.getEnum(Database.class, value); - - if (isNull(database)) { - String message = - format( - "Property '{}' has illegal value: '{}', allowed options: {}", - ANALYTICS_DATABASE.getKey(), - value, - StringUtils.join(Database.values(), ',')); - throw new IllegalArgumentException(message); - } - - return database; - } - /** * Splits the given value on comma, and returns the values as a set. * diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/resourcetable/table/OrganisationUnitStructureResourceTable.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/resourcetable/table/OrganisationUnitStructureResourceTable.java index 8b9e14844c19..1a924b5fe302 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/resourcetable/table/OrganisationUnitStructureResourceTable.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/resourcetable/table/OrganisationUnitStructureResourceTable.java @@ -156,7 +156,7 @@ List createBatchObjects(List units, int level) { values.add(unit.getOpeningDate()); values.add(unit.getClosedDate()); values.add(level); - values.add(unit.getPath()); + values.add(unit.getStoredPath()); Map identifiers = new HashMap<>(); Map uids = new HashMap<>(); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DataQueryServiceDimensionItemKeywordTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DataQueryServiceDimensionItemKeywordTest.java index 622f2b45975f..51ebc5aa1ccd 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DataQueryServiceDimensionItemKeywordTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DataQueryServiceDimensionItemKeywordTest.java @@ -32,6 +32,7 @@ import static org.hamcrest.Matchers.is; import static org.hisp.dhis.common.DimensionalObject.PERIOD_DIM_ID; import static org.hisp.dhis.common.IdScheme.UID; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -142,7 +143,7 @@ public void setUp() { lenient().when(i18nManager.getI18n()).thenReturn(i18n); lenient().when(i18n.getString("LAST_12_MONTHS")).thenReturn("Last 12 months"); - rootOu = new OrganisationUnit("Sierra Leone"); + rootOu = createOrganisationUnit('A'); rootOu.setUid(CodeGenerator.generateUid()); rootOu.setCode("OU_525"); } @@ -164,7 +165,7 @@ void convertAnalyticsRequestWithOuLevelToDataQueryParam() { .thenReturn(getOrgUnitLevel(2, "level2UID", "District", null)); when(organisationUnitService.getOrganisationUnitLevelByLevelOrUid("2")).thenReturn(2); when(organisationUnitService.getOrganisationUnitsAtLevels(Mockito.anyList(), Mockito.anyList())) - .thenReturn(Lists.newArrayList(new OrganisationUnit(), new OrganisationUnit())); + .thenReturn(Lists.newArrayList(createOrganisationUnit('A'), createOrganisationUnit('B'))); rb.addOuFilter("LEVEL-2;ImspTQPwCqd"); rb.addDimension(concatenateUuid(DATA_ELEMENT_1, DATA_ELEMENT_2, DATA_ELEMENT_3)); @@ -199,7 +200,7 @@ void convertAnalyticsRequestWithMultipleOuLevelToDataQueryParam() { when(organisationUnitService.getOrganisationUnitLevelByLevelOrUid("3")).thenReturn(3); when(organisationUnitService.getOrganisationUnitLevelByLevelOrUid("2")).thenReturn(2); when(organisationUnitService.getOrganisationUnitsAtLevels(Mockito.anyList(), Mockito.anyList())) - .thenReturn(Lists.newArrayList(new OrganisationUnit(), new OrganisationUnit())); + .thenReturn(Lists.newArrayList(createOrganisationUnit('A'), createOrganisationUnit('B'))); rb.addOuFilter("LEVEL-2;LEVEL-3;ImspTQPwCqd"); rb.addDimension(concatenateUuid(DATA_ELEMENT_1, DATA_ELEMENT_2, DATA_ELEMENT_3)); @@ -240,9 +241,9 @@ void convertAnalyticsRequestWithIndicatorGroup() { when(idObjectManager.getObject(IndicatorGroup.class, UID, INDICATOR_GROUP_UID)) .thenReturn(indicatorGroup); when(idObjectManager.getObject(OrganisationUnit.class, UID, "goRUwCHPg1M")) - .thenReturn(new OrganisationUnit("aaa")); + .thenReturn(createOrganisationUnit('A')); when(idObjectManager.getObject(OrganisationUnit.class, UID, "fdc6uOvgoji")) - .thenReturn(new OrganisationUnit("bbb")); + .thenReturn(createOrganisationUnit('B')); rb.addOuFilter("goRUwCHPg1M;fdc6uOvgoji"); rb.addDimension("IN_GROUP-" + INDICATOR_GROUP_UID + ";cYeuwXTCPkU;Jtf34kNZhz"); @@ -343,7 +344,8 @@ void convertAnalyticsRequestWithOrgUnitLevelAsFilter() { assertNull(keywords.getKeyword("level2UID").getMetadataItem().getCode()); assertNotNull(keywords.getKeyword(rootOu.getUid())); - assertEquals("Sierra Leone", keywords.getKeyword(rootOu.getUid()).getMetadataItem().getName()); + assertEquals( + "OrganisationUnitA", keywords.getKeyword(rootOu.getUid()).getMetadataItem().getName()); assertEquals( rootOu.getCode(), keywords.getKeyword(rootOu.getUid()).getMetadataItem().getCode()); } @@ -403,7 +405,8 @@ void convertAnalyticsRequestWithOrgUnitLevelAndOrgUnitGroupAsFilter() { groupOu.getCode(), keywords.getKeyword(groupOu.getUid()).getMetadataItem().getCode()); assertNotNull(keywords.getKeyword(rootOu.getUid())); - assertEquals("Sierra Leone", keywords.getKeyword(rootOu.getUid()).getMetadataItem().getName()); + assertEquals( + "OrganisationUnitA", keywords.getKeyword(rootOu.getUid()).getMetadataItem().getName()); assertEquals( rootOu.getCode(), keywords.getKeyword(rootOu.getUid()).getMetadataItem().getCode()); } @@ -632,7 +635,7 @@ private void initOrgUnitGroup(String ouGroupUID) { when(idObjectManager.getObject(OrganisationUnit.class, UID, this.rootOu.getUid())) .thenReturn(rootOu); when(organisationUnitService.getOrganisationUnits(Mockito.anyList(), Mockito.anyList())) - .thenReturn(Lists.newArrayList(new OrganisationUnit(), new OrganisationUnit())); + .thenReturn(Lists.newArrayList(createOrganisationUnit('A'), createOrganisationUnit('B'))); } private void assertOrgUnitGroup(String ouGroupUID, DimensionalObject dimension) { @@ -644,7 +647,8 @@ private void assertOrgUnitGroup(String ouGroupUID, DimensionalObject dimension) assertEquals("CODE_001", keywords.getKeyword(ouGroupUID).getMetadataItem().getCode()); assertNotNull(keywords.getKeyword(rootOu.getUid())); - assertEquals("Sierra Leone", keywords.getKeyword(rootOu.getUid()).getMetadataItem().getName()); + assertEquals( + "OrganisationUnitA", keywords.getKeyword(rootOu.getUid()).getMetadataItem().getName()); assertEquals( rootOu.getCode(), keywords.getKeyword(rootOu.getUid()).getMetadataItem().getCode()); } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DimensionalObjectProviderTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DimensionalObjectProviderTest.java index d0277d6514a5..24358bd39c2c 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DimensionalObjectProviderTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DimensionalObjectProviderTest.java @@ -216,8 +216,8 @@ void testGetDimensionWhenDataDimensionsAreNotFound() { @Test void testGetOrgUnitDimensionWithNoLevelsNoGroup() { - OrganisationUnit level2Ou1 = createOrganisationUnit("Bo"); - OrganisationUnit level2Ou2 = createOrganisationUnit("Bombali"); + OrganisationUnit level2Ou1 = createOrganisationUnit('A'); + OrganisationUnit level2Ou2 = createOrganisationUnit('B'); OrganisationUnit ou1 = createOrganisationUnit('A'); OrganisationUnit ou2 = createOrganisationUnit('B'); List organisationUnits = @@ -253,10 +253,10 @@ void testGetOrgUnitDimensionWithNoLevelsNoGroup() { @Test void testGetOrgUnitDimensionWithWithLevelAndGroup() { OrganisationUnitGroup organisationUnitGroup = createOrganisationUnitGroup('A'); - OrganisationUnit level2Ou1 = createOrganisationUnit("Bo"); - OrganisationUnit level2Ou2 = createOrganisationUnit("Bombali"); - OrganisationUnit ou1 = createOrganisationUnit('A'); - OrganisationUnit ou2 = createOrganisationUnit('B'); + OrganisationUnit level2Ou1 = createOrganisationUnit('A'); + OrganisationUnit level2Ou2 = createOrganisationUnit('B'); + OrganisationUnit ou1 = createOrganisationUnit('C'); + OrganisationUnit ou2 = createOrganisationUnit('D'); List organisationUnits = new ArrayList<>(asList(level2Ou1, level2Ou2, ou1, ou2)); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/handler/SchemeIdResponseMapperTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/handler/SchemeIdResponseMapperTest.java index edd44fbee2e6..d4c3abcdab71 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/handler/SchemeIdResponseMapperTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/handler/SchemeIdResponseMapperTest.java @@ -28,7 +28,6 @@ package org.hisp.dhis.analytics.data.handler; import static com.google.common.collect.Lists.newArrayList; -import static java.lang.String.valueOf; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.emptyOrNullString; import static org.hamcrest.Matchers.equalTo; @@ -42,6 +41,7 @@ import static org.hisp.dhis.common.IdScheme.UUID; import static org.hisp.dhis.common.ValueType.TEXT; import static org.hisp.dhis.period.PeriodType.getPeriodFromIsoString; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; import static org.hisp.dhis.test.TestBase.createProgram; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -83,12 +83,12 @@ class SchemeIdResponseMapperTest { @Test void testGetSchemeIdResponseMapWhenOutputIdSchemeIsSetToName() { List dataElementOperands = stubDataElementOperands(); - OrganisationUnit organisationUnit = stubOrgUnit(); + OrganisationUnit organisationUnit = createOrganisationUnit('A'); Period period = stubPeriod(); Data schemeData = Data.builder() - .organizationUnits(List.of(stubOrgUnit())) + .organizationUnits(List.of(organisationUnit)) .dataElementOperands(List.of(dataElementOperands.get(0), dataElementOperands.get(1))) .dimensionalItemObjects(Set.of(period, organisationUnit)) .build(); @@ -121,7 +121,7 @@ void testGetSchemeIdResponseMapWhenOutputIdSchemeIsSetToName() { @Test void testGetSchemeIdResponseMapWhenOutputIdSchemeIsSetToCode() { List dataElementOperands = stubDataElementOperands(); - OrganisationUnit organisationUnit = stubOrgUnit(); + OrganisationUnit organisationUnit = createOrganisationUnit('A'); Period period = stubPeriod(); Data schemeData = @@ -158,12 +158,12 @@ void testGetSchemeIdResponseMapWhenOutputIdSchemeIsSetToCode() { @Test void testGetSchemeIdResponseMapWhenOutputIdSchemeIsSetToUuid() { List dataElementOperands = stubDataElementOperands(); - OrganisationUnit organisationUnit = stubOrgUnit(); + OrganisationUnit organisationUnit = createOrganisationUnit('A'); Period period = stubPeriod(); Data schemeData = Data.builder() - .organizationUnits(List.of(stubOrgUnit())) + .organizationUnits(List.of(createOrganisationUnit('A'))) .dataElementOperands(List.of(dataElementOperands.get(0), dataElementOperands.get(1))) .dimensionalItemObjects(Set.of(period, organisationUnit)) .build(); @@ -192,7 +192,7 @@ void testGetSchemeIdResponseMapWhenOutputIdSchemeIsSetToUuid() { @Test void testGetSchemeIdResponseMapWhenOutputIdSchemeIsSetToUid() { List dataElementOperands = stubDataElementOperands(); - OrganisationUnit organisationUnit = stubOrgUnit(); + OrganisationUnit organisationUnit = createOrganisationUnit('A'); Period period = stubPeriod(); Data schemeData = @@ -226,7 +226,7 @@ void testGetSchemeIdResponseMapWhenOutputIdSchemeIsSetToUid() { @Test void testGetSchemeIdResponseMapWhenOutputDataElementIdSchemeIsSetToName() { List dataElements = stubDataElements(); - OrganisationUnit organisationUnit = stubOrgUnit(); + OrganisationUnit organisationUnit = createOrganisationUnit('A'); Period period = stubPeriod(); Data schemeData = @@ -257,7 +257,7 @@ void testGetSchemeIdResponseMapWhenOutputDataElementIdSchemeIsSetToName() { @Test void testGetSchemeIdResponseMapWhenOutputDataElementIdSchemeIsSetToCode() { List dataElementOperands = stubDataElementOperands(); - OrganisationUnit organisationUnit = stubOrgUnit(); + OrganisationUnit organisationUnit = createOrganisationUnit('A'); Period period = stubPeriod(); Data schemeData = @@ -295,7 +295,7 @@ void testGetSchemeIdResponseMapWhenOutputDataElementIdSchemeIsSetToCode() { @Test void testGetSchemeIdResponseMapWhenOutputDataElementIdSchemeIsSetToUuid() { List dataElementOperands = stubDataElementOperands(); - OrganisationUnit organisationUnit = stubOrgUnit(); + OrganisationUnit organisationUnit = createOrganisationUnit('A'); Period period = stubPeriod(); Data schemeData = @@ -330,7 +330,7 @@ void testGetSchemeIdResponseMapWhenOutputDataElementIdSchemeIsSetToUuid() { void testGetSchemeIdResponseMapWhenOutputDataElementIdSchemeIsSetToUid() { List dataElementOperands = stubDataElementOperands(); DataElement dataElement = stubDataElement(); - OrganisationUnit organisationUnit = stubOrgUnit(); + OrganisationUnit organisationUnit = createOrganisationUnit('A'); Period period = stubPeriod(); Data schemeData = @@ -365,7 +365,7 @@ void testGetSchemeIdResponseMapWhenOutputDataElementIdSchemeIsSetToUid() { @Test void testGetSchemeIdResponseMapWhenOutputOrgUnitIdSchemeIsSetToName() { List dataElementOperands = stubDataElementOperands(); - OrganisationUnit organisationUnit = stubOrgUnit(); + OrganisationUnit organisationUnit = createOrganisationUnit('A'); Period period = stubPeriod(); Data schemeData = @@ -399,7 +399,7 @@ void testGetSchemeIdResponseMapWhenOutputOrgUnitIdSchemeIsSetToName() { @Test void testGetSchemeIdResponseMapWhenOutputOrgUnitIdSchemeIsSetToCode() { List dataElementOperands = stubDataElementOperands(); - OrganisationUnit organisationUnit = stubOrgUnit(); + OrganisationUnit organisationUnit = createOrganisationUnit('A'); Period period = stubPeriod(); Data schemeData = @@ -433,7 +433,7 @@ void testGetSchemeIdResponseMapWhenOutputOrgUnitIdSchemeIsSetToCode() { @Test void testGetSchemeIdResponseMapWhenOutputOrgUnitIdSchemeIsSetToUuid() { List dataElementOperands = stubDataElementOperands(); - OrganisationUnit organisationUnit = stubOrgUnit(); + OrganisationUnit organisationUnit = createOrganisationUnit('A'); Period period = stubPeriod(); Data schemeData = @@ -467,7 +467,7 @@ void testGetSchemeIdResponseMapWhenOutputOrgUnitIdSchemeIsSetToUuid() { @Test void testGetSchemeIdResponseMapWhenOutputOrgUnitIdSchemeIsSetToUid() { List dataElementOperands = stubDataElementOperands(); - OrganisationUnit organisationUnit = stubOrgUnit(); + OrganisationUnit organisationUnit = createOrganisationUnit('A'); Period period = stubPeriod(); Data schemeData = @@ -501,7 +501,7 @@ void testGetSchemeIdResponseMapWhenOutputOrgUnitIdSchemeIsSetToUid() { @Test void testGetSchemeIdResponseMapWhenOutputOrgUnitIdSchemeOverridesOutputIdScheme() { List dataElementOperands = stubDataElementOperands(); - OrganisationUnit organisationUnit = stubOrgUnit(); + OrganisationUnit organisationUnit = createOrganisationUnit('A'); Period period = stubPeriod(); Data schemeData = @@ -543,7 +543,7 @@ void testGetSchemeIdResponseMapWhenOutputOrgUnitIdSchemeOverridesOutputIdScheme( @Test void testGetSchemeIdResponseMapWhenOutputDataElementIdSchemeOverridesOutputOrgUnitIdScheme() { List dataElementOperands = stubDataElementOperands(); - OrganisationUnit organisationUnit = stubOrgUnit(); + OrganisationUnit organisationUnit = createOrganisationUnit('A'); Period period = stubPeriod(); Data schemeData = @@ -587,7 +587,7 @@ void testGetSchemeIdResponseMapWhenOutputDataElementOrgUnitIdSchemeOverrideOutpu DataElement dataElement = stubDataElement(); Indicator indicator = stubIndicator(); ProgramIndicator programIndicator = stubProgramIndicator(); - OrganisationUnit organisationUnit = stubOrgUnit(); + OrganisationUnit organisationUnit = createOrganisationUnit('A'); Period period = stubPeriod(); List dataElementOperands = stubDataElementOperands(); @@ -613,13 +613,11 @@ void testGetSchemeIdResponseMapWhenOutputDataElementOrgUnitIdSchemeOverrideOutpu Map responseMap = schemeIdResponseMapper.getSchemeIdResponseMap(schemeInfo); - String orgUnitUid = organisationUnit.getUid(); String periodIsoDate = period.getIsoDate(); DataElement dataElementA = dataElementOperands.get(0).getDataElement(); DataElement dataElementB = dataElementOperands.get(1).getDataElement(); CategoryOptionCombo categoryOptionComboC = dataElementOperands.get(0).getCategoryOptionCombo(); - assertThat(responseMap.get(orgUnitUid), is(equalTo(valueOf(organisationUnit.getId())))); assertThat(responseMap.get(periodIsoDate), is(equalTo(period.getName()))); assertThat(responseMap.get(dataElementA.getUid()), is(equalTo(dataElementA.getCode()))); assertThat(responseMap.get(dataElementB.getUid()), is(equalTo(dataElementB.getCode()))); @@ -636,7 +634,7 @@ void testGetSchemeIdResponseMapWhenOutputDataItemIdSchemeOverridesOutputIdScheme DataElement dataElement = stubDataElement(); Indicator indicator = stubIndicator(); ProgramIndicator programIndicator = stubProgramIndicator(); - OrganisationUnit organisationUnit = stubOrgUnit(); + OrganisationUnit organisationUnit = createOrganisationUnit('A'); Period period = stubPeriod(); Data schemeData = @@ -870,7 +868,8 @@ private Settings stubSchemeSettings(IdScheme idScheme) { private Data stubSchemeData(Program program) { return Data.builder() .programs(List.of(program)) - .dimensionalItemObjects(Set.of(stubPeriod(), stubOrgUnit(), stubDataElement())) + .dimensionalItemObjects( + Set.of(stubPeriod(), createOrganisationUnit('A'), stubDataElement())) .build(); } @@ -944,15 +943,4 @@ private ProgramIndicator stubProgramIndicator() { return programIndicatorA; } - - private OrganisationUnit stubOrgUnit() { - OrganisationUnit organisationUnit = new OrganisationUnit(); - organisationUnit.setName("OrgUnitA"); - organisationUnit.setShortName("ShortOrgUnitA"); - organisationUnit.setUid("org1234567A"); - organisationUnit.setCode("CodeA"); - organisationUnit.setId(1); - - return organisationUnit; - } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/AbstractJdbcEventAnalyticsManagerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/AbstractJdbcEventAnalyticsManagerTest.java index b37709db626e..c78346771352 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/AbstractJdbcEventAnalyticsManagerTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/AbstractJdbcEventAnalyticsManagerTest.java @@ -528,7 +528,7 @@ void testGetWhereClauseWithMultipleOrgUnitDescendantsAtSameLevel() { assertThat( whereClause, - containsString("and ax.\"uidlevel0\" in ('ouabcdefghA','ouabcdefghB','ouabcdefghC')")); + containsString("and ax.\"uidlevel1\" in ('ouabcdefghA','ouabcdefghB','ouabcdefghC')")); } @Test diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/DefaultEventCoordinateServiceTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/DefaultEventCoordinateServiceTest.java index d7bc5b74197e..2d836bbbbc66 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/DefaultEventCoordinateServiceTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/DefaultEventCoordinateServiceTest.java @@ -69,7 +69,7 @@ class DefaultEventCoordinateServiceTest { void testGetCoordinateFieldOrFail(String geometry) { when(programService.getProgram(any(String.class))).thenReturn(createProgram('A')); - assertEquals(geometry, service.getCoordinateField("A", geometry, ErrorCode.E7232)); + assertEquals(geometry, service.validateCoordinateField("A", geometry, ErrorCode.E7232)); } @Test diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/EventQueryServiceTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/EventQueryServiceTest.java index 4d5b916e4475..3865f5f258c1 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/EventQueryServiceTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/EventQueryServiceTest.java @@ -53,9 +53,9 @@ import org.hisp.dhis.analytics.tracker.MetadataItemsHandler; import org.hisp.dhis.analytics.tracker.SchemeIdHandler; import org.hisp.dhis.common.IdScheme; +import org.hisp.dhis.db.sql.SqlBuilder; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.program.Program; -import org.hisp.dhis.system.database.DatabaseInfoProvider; import org.hisp.dhis.user.SystemUser; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -85,7 +85,7 @@ class EventQueryServiceTest { @Mock private EventQueryPlanner queryPlanner; - @Mock private DatabaseInfoProvider databaseInfoProvider; + @Mock private SqlBuilder sqlBuilder; @Mock private SchemeIdResponseMapper schemeIdResponseMapper; diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/AbstractEventJdbcTableManagerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/AbstractEventJdbcTableManagerTest.java index b26200ffb396..a3be6e2be37d 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/AbstractEventJdbcTableManagerTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/AbstractEventJdbcTableManagerTest.java @@ -142,7 +142,7 @@ void testGetSelectExpressionText() { @Test void testGetSelectExpressionGeometry() { - when(manager.isSpatialSupport()).thenReturn(true); + when(manager.isGeospatialSupport()).thenReturn(true); String expected = """ diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcEnrollmentAnalyticsTableManagerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcEnrollmentAnalyticsTableManagerTest.java index 4d7463b8fb6e..a85f5cac3e71 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcEnrollmentAnalyticsTableManagerTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcEnrollmentAnalyticsTableManagerTest.java @@ -110,7 +110,6 @@ class JdbcEnrollmentAnalyticsTableManagerTest { @BeforeEach public void setUp() { - when(analyticsTableSettings.isSpatialSupport()).thenReturn(true); lenient().when(settingsProvider.getCurrentSettings()).thenReturn(SystemSettings.of(Map.of())); } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManagerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManagerTest.java index 7bc0ca4c0208..84289b558a3a 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManagerTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManagerTest.java @@ -357,7 +357,6 @@ private AnalyticsTableColumn getColumn(String column, AnalyticsTable analyticsTa @Test void verifyGetTableWithDataElements() { - when(analyticsTableSettings.isSpatialSupport()).thenReturn(true); Program program = createProgram('A'); DataElement d1 = createDataElement('Z', ValueType.TEXT, AggregationType.SUM); @@ -478,7 +477,6 @@ void verifyGetTableWithDataElements() { @Test void verifyGetTableWithTrackedEntityAttribute() { - when(analyticsTableSettings.isSpatialSupport()).thenReturn(true); Program program = createProgram('A'); TrackedEntityAttribute tea1 = rnd.nextObject(TrackedEntityAttribute.class); @@ -552,7 +550,6 @@ void verifyGetTableWithTrackedEntityAttribute() { @Test void verifyDataElementTypeOrgUnitFetchesOuNameWhenPopulatingEventAnalyticsTable() { ArgumentCaptor sql = ArgumentCaptor.forClass(String.class); - when(analyticsTableSettings.isSpatialSupport()).thenReturn(true); Program programA = createProgram('A'); DataElement d5 = createDataElement('G', ValueType.ORGANISATION_UNIT, AggregationType.NONE); @@ -605,7 +602,6 @@ void verifyDataElementTypeOrgUnitFetchesOuNameWhenPopulatingEventAnalyticsTable( @Test void verifyTeiTypeOrgUnitFetchesOuNameWhenPopulatingEventAnalyticsTable() { ArgumentCaptor sql = ArgumentCaptor.forClass(String.class); - when(analyticsTableSettings.isSpatialSupport()).thenReturn(true); Program programA = createProgram('A'); TrackedEntityAttribute tea = createTrackedEntityAttribute('a', ValueType.ORGANISATION_UNIT); @@ -661,7 +657,6 @@ void verifyTeiTypeOrgUnitFetchesOuNameWhenPopulatingEventAnalyticsTable() { @Test void verifyOrgUnitOwnershipJoinsWhenPopulatingEventAnalyticsTable() { ArgumentCaptor sql = ArgumentCaptor.forClass(String.class); - when(analyticsTableSettings.isSpatialSupport()).thenReturn(true); Program programA = createProgram('A'); TrackedEntityAttribute tea = createTrackedEntityAttribute('a', ValueType.ORGANISATION_UNIT); @@ -878,7 +873,6 @@ private void match(AnalyticsTableColumn col) { @Test void verifyTeaTypeOrgUnitFetchesOuNameWhenPopulatingEventAnalyticsTable() { ArgumentCaptor sql = ArgumentCaptor.forClass(String.class); - when(analyticsTableSettings.isSpatialSupport()).thenReturn(true); Program programA = createProgram('A'); TrackedEntityAttribute tea = createTrackedEntityAttribute('a', ValueType.ORGANISATION_UNIT); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/setting/SqlBuilderSettingsTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/setting/AnalyticsTableSettingsTest.java similarity index 89% rename from dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/setting/SqlBuilderSettingsTest.java rename to dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/setting/AnalyticsTableSettingsTest.java index 3b03b345b91d..6101509283f3 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/setting/SqlBuilderSettingsTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/setting/AnalyticsTableSettingsTest.java @@ -28,12 +28,10 @@ package org.hisp.dhis.analytics.table.setting; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.when; import java.util.Set; import org.hisp.dhis.analytics.table.model.Skip; -import org.hisp.dhis.db.model.Database; import org.hisp.dhis.external.conf.ConfigurationKey; import org.hisp.dhis.external.conf.DhisConfigurationProvider; import org.hisp.dhis.setting.SystemSettingsService; @@ -44,23 +42,13 @@ import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) -class SqlBuilderSettingsTest { +class AnalyticsTableSettingsTest { @Mock private DhisConfigurationProvider config; @Mock private SystemSettingsService systemSettings; @InjectMocks private AnalyticsTableSettings settings; - @Test - void testGetAndValidateDatabase() { - assertEquals(Database.POSTGRESQL, settings.getAndValidateDatabase("POSTGRESQL")); - } - - @Test - void testGetAndValidateInvalidDatabase() { - assertThrows(IllegalArgumentException.class, () -> settings.getAndValidateDatabase("ORACLE")); - } - @Test void testGetSkipIndexDimensionsDefault() { when(config.getProperty(ConfigurationKey.ANALYTICS_TABLE_SKIP_INDEX)) diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/resourcetable/table/OrganisationUnitStructureResourceTableTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/resourcetable/table/OrganisationUnitStructureResourceTableTest.java index 2080837b84e6..31d5fb05b764 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/resourcetable/table/OrganisationUnitStructureResourceTableTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/resourcetable/table/OrganisationUnitStructureResourceTableTest.java @@ -43,17 +43,17 @@ void testCreateBatchObjectsWhenLevelsAreSame() { int maxOrgUnitLevels = 3; int currentLevel = 3; - OrganisationUnit root = createOrganisationUnit("ouR"); + OrganisationUnit root = createOrganisationUnit('A'); root.setPath("/p1"); - OrganisationUnit ou1 = createOrganisationUnit("ou1", root); + OrganisationUnit ou1 = createOrganisationUnit('B', root); ou1.setPath("/p1/p2"); - OrganisationUnit ou2 = createOrganisationUnit("ou2", ou1); + OrganisationUnit ou2 = createOrganisationUnit('C', ou1); ou2.setHierarchyLevel(currentLevel); ou2.setPath("/p1/p2/ou2"); - OrganisationUnit ou3 = createOrganisationUnit("ou3", ou1); + OrganisationUnit ou3 = createOrganisationUnit('D', ou1); ou3.setHierarchyLevel(currentLevel); ou3.setPath("/p1/p2/ou3"); @@ -72,10 +72,10 @@ void testCreateBatchObjectsWhenHierarchyLevelIsLowerThanMaxLevel() { int maxOrgUnitLevels = 3; int currentLevel = 2; - OrganisationUnit root = createOrganisationUnit("ouR"); + OrganisationUnit root = createOrganisationUnit('A'); root.setPath("/p1"); - OrganisationUnit ou1 = createOrganisationUnit("ou1", root); + OrganisationUnit ou1 = createOrganisationUnit('B', root); ou1.setPath("/p1/p2"); List organisationUnits = new ArrayList<>(); @@ -92,10 +92,10 @@ void testCreateBatchObjectsWhenCurrentLevelIsLargerThanMaxLevel() { int maxOrgUnitLevels = 2; int currentLevel = 3; - OrganisationUnit root = createOrganisationUnit("ouR"); + OrganisationUnit root = createOrganisationUnit('A'); root.setPath("/p1"); - OrganisationUnit ou1 = createOrganisationUnit("ou1", root); + OrganisationUnit ou1 = createOrganisationUnit('B', root); ou1.setPath("/p1/p2"); ou1.setUid("uid-123"); @@ -120,10 +120,10 @@ void testCreateBatchObjectsWhenCurrentLevelHasNoParent() { int maxOrgUnitLevels = 2; int currentLevel = 3; - OrganisationUnit root = createOrganisationUnit("ouR"); + OrganisationUnit root = createOrganisationUnit('A'); root.setPath("/p1"); - OrganisationUnit ou1 = createOrganisationUnit("ou1"); + OrganisationUnit ou1 = createOrganisationUnit('B'); ou1.setPath("/p1/p2"); ou1.setUid("uid-123"); diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/association/jdbc/JdbcOrgUnitAssociationsStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/association/jdbc/JdbcOrgUnitAssociationsStore.java index 2fcad3f94b00..6aa5c6b3b373 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/association/jdbc/JdbcOrgUnitAssociationsStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/association/jdbc/JdbcOrgUnitAssociationsStore.java @@ -108,7 +108,7 @@ private Set getUserOrgUnitPaths() { User currentUser = userService.getUserByUsername(CurrentUserUtil.getCurrentUsername()); Set allUserOrgUnitPaths = currentUser.getOrganisationUnits().stream() - .map(OrganisationUnit::getPath) + .map(OrganisationUnit::getStoredPath) .collect(Collectors.toSet()); return allUserOrgUnitPaths.stream() diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/category/DefaultCategoryService.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/category/DefaultCategoryService.java index 14a2873596f1..b6f216c85205 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/category/DefaultCategoryService.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/category/DefaultCategoryService.java @@ -28,7 +28,6 @@ package org.hisp.dhis.category; import com.google.common.collect.Lists; -import com.google.common.collect.Sets; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -132,12 +131,6 @@ public void deleteCategory(Category dataElementCategory) { categoryStore.delete(dataElementCategory); } - @Override - @Transactional(readOnly = true) - public List getAllDataElementCategories() { - return categoryStore.getAll(); - } - @Override @Transactional(readOnly = true) public Category getCategory(long id) { @@ -421,38 +414,6 @@ public List getAttributeCategoryCombos() { return categoryComboStore.getCategoryCombosByDimensionType(DataDimensionType.ATTRIBUTE); } - @Override - @Transactional(readOnly = true) - public String validateCategoryCombo(CategoryCombo categoryCombo) { - if (categoryCombo == null) { - return "category_combo_is_null"; - } - - if (categoryCombo.getCategories() == null || categoryCombo.getCategories().isEmpty()) { - return "category_combo_must_have_at_least_one_category"; - } - - if (Sets.newHashSet(categoryCombo.getCategories()).size() - < categoryCombo.getCategories().size()) { - return "category_combo_cannot_have_duplicate_categories"; - } - - Set categoryOptions = new HashSet<>(); - - for (Category category : categoryCombo.getCategories()) { - if (category == null || category.getCategoryOptions().isEmpty()) { - return "categories_must_have_at_least_one_category_option"; - } - - if (!Sets.intersection(categoryOptions, Sets.newHashSet(category.getCategoryOptions())) - .isEmpty()) { - return "categories_cannot_share_category_options"; - } - } - - return null; - } - // ------------------------------------------------------------------------- // CategoryOptionCombo // ------------------------------------------------------------------------- @@ -701,11 +662,16 @@ public void updateCategoryOptionComboNames() { @Override public List getCategoryOptionCombosByCategoryOption( - Collection categoryOptionsUids) { + @Nonnull Collection categoryOptionsUids) { return categoryOptionComboStore.getCategoryOptionCombosByCategoryOption( UID.toValueList(categoryOptionsUids)); } + @Override + public List getCategoryOptionCombosByUid(@Nonnull Collection uids) { + return categoryOptionComboStore.getByUid(UID.toValueList(uids)); + } + // ------------------------------------------------------------------------- // DataElementOperand // ------------------------------------------------------------------------- diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/category/hibernate/HibernateCategoryComboStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/category/hibernate/HibernateCategoryComboStore.java index 4041df7038e5..65f215f77fb7 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/category/hibernate/HibernateCategoryComboStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/category/hibernate/HibernateCategoryComboStore.java @@ -29,10 +29,13 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.criteria.CriteriaBuilder; +import java.util.Collection; import java.util.List; +import javax.annotation.Nonnull; import org.hisp.dhis.category.CategoryCombo; import org.hisp.dhis.category.CategoryComboStore; import org.hisp.dhis.common.DataDimensionType; +import org.hisp.dhis.common.UID; import org.hisp.dhis.common.hibernate.HibernateIdentifiableObjectStore; import org.hisp.dhis.security.acl.AclService; import org.springframework.context.ApplicationEventPublisher; @@ -63,4 +66,17 @@ public List getCategoryCombosByDimensionType(DataDimensionType da .addPredicate(root -> builder.equal(root.get("dataDimensionType"), dataDimensionType)) .addPredicate(root -> builder.equal(root.get("name"), "default"))); } + + @Override + public List getByCategoryOptionCombo(@Nonnull Collection uids) { + if (uids.isEmpty()) return List.of(); + return getQuery( + """ + select distinct cc from CategoryCombo cc + join cc.optionCombos coc + where coc.uid in :uids + """) + .setParameter("uids", UID.toValueList(uids)) + .getResultList(); + } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/category/hibernate/HibernateCategoryOptionStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/category/hibernate/HibernateCategoryOptionStore.java index 63937a9ced9a..6528ad1e6c50 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/category/hibernate/HibernateCategoryOptionStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/category/hibernate/HibernateCategoryOptionStore.java @@ -29,7 +29,9 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.criteria.CriteriaBuilder; +import java.util.Collection; import java.util.List; +import javax.annotation.Nonnull; import org.hisp.dhis.category.Category; import org.hisp.dhis.category.CategoryOption; import org.hisp.dhis.category.CategoryOptionStore; @@ -93,4 +95,17 @@ public List getDataWriteCategoryOptions( .addPredicate( root -> builder.equal(root.join("categories").get("id"), category.getId()))); } + + @Override + public List getByCategoryOptionCombo(@Nonnull Collection uids) { + if (uids.isEmpty()) return List.of(); + return getQuery( + """ + select distinct co from CategoryOption co + join co.categoryOptionCombos coc + where coc.uid in :uids + """) + .setParameter("uids", UID.toValueList(uids)) + .getResultList(); + } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dashboard/hibernate/HibernateDashboardItemStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dashboard/hibernate/HibernateDashboardItemStore.java index 087858dcf807..c7707902f8d0 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dashboard/hibernate/HibernateDashboardItemStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dashboard/hibernate/HibernateDashboardItemStore.java @@ -67,7 +67,7 @@ public Dashboard getDashboardFromDashboardItem(DashboardItem dashboardItem) { Query query = getTypedQuery("from Dashboard d where :item in elements(d.items)"); query.setParameter("item", dashboardItem); - return query.getSingleResult(); + return getSingleResult(query); } @Override diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/hibernate/HibernateDataApprovalAuditStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/hibernate/HibernateDataApprovalAuditStore.java index 03858c8aaed7..59ae6d11efb6 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/hibernate/HibernateDataApprovalAuditStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/hibernate/HibernateDataApprovalAuditStore.java @@ -36,6 +36,7 @@ import java.util.List; import java.util.Set; import org.apache.commons.collections4.CollectionUtils; +import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.commons.util.SqlHelper; import org.hisp.dhis.commons.util.TextUtils; import org.hisp.dhis.dataapproval.DataApprovalAudit; @@ -85,6 +86,12 @@ public void deleteDataApprovalAudits(OrganisationUnit organisationUnit) { entityManager.createQuery(hql).setParameter("unit", organisationUnit).executeUpdate(); } + @Override + public void deleteDataApprovalAudits(CategoryOptionCombo coc) { + String hql = "delete from DataApprovalAudit d where d.attributeOptionCombo = :coc"; + entityManager.createQuery(hql).setParameter("coc", coc).executeUpdate(); + } + @Override public List getDataApprovalAudits(DataApprovalAuditQueryParams params) { SqlHelper hlp = new SqlHelper(); diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/hibernate/HibernateDataApprovalStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/hibernate/HibernateDataApprovalStore.java index bd62c36314ca..0d0e29525ebb 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/hibernate/HibernateDataApprovalStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataapproval/hibernate/HibernateDataApprovalStore.java @@ -44,6 +44,7 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import javax.annotation.Nonnull; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; @@ -54,6 +55,7 @@ import org.hisp.dhis.category.CategoryService; import org.hisp.dhis.common.BaseIdentifiableObject; import org.hisp.dhis.common.IdentifiableObjectUtils; +import org.hisp.dhis.common.UID; import org.hisp.dhis.dataapproval.DataApproval; import org.hisp.dhis.dataapproval.DataApprovalLevel; import org.hisp.dhis.dataapproval.DataApprovalState; @@ -797,6 +799,33 @@ public List getDataApprovalStatuses( return statusList; } + @Override + public List getByCategoryOptionCombo(@Nonnull Collection uids) { + if (uids.isEmpty()) return List.of(); + return getQuery( + """ + select da from DataApproval da + join da.attributeOptionCombo coc + where coc.uid in :uids + """) + .setParameter("uids", UID.toValueList(uids)) + .getResultList(); + } + + @Override + public void deleteByCategoryOptionCombo(@Nonnull Collection uids) { + if (uids.isEmpty()) return; + String hql = + """ + delete from DataApproval da + where da.attributeOptionCombo in + (select coc from CategoryOptionCombo coc + where coc.uid in :uids) + """; + + entityManager.createQuery(hql).setParameter("uids", UID.toValueList(uids)).executeUpdate(); + } + /** * Get the id for the workflow period that spans the given end date. The workflow period may or * may not be the same as the period for which we are checking data validity. The workflow period diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataelement/DefaultDataElementService.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataelement/DefaultDataElementService.java index c37fa43fee1a..2894c1d670a9 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataelement/DefaultDataElementService.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataelement/DefaultDataElementService.java @@ -29,20 +29,16 @@ import java.util.Collection; import java.util.List; -import java.util.stream.Collectors; import javax.annotation.Nonnull; import lombok.RequiredArgsConstructor; -import org.hisp.dhis.category.CategoryCombo; import org.hisp.dhis.common.GenericDimensionalObjectStore; import org.hisp.dhis.common.IdentifiableObjectStore; import org.hisp.dhis.common.IllegalQueryException; -import org.hisp.dhis.common.UID; import org.hisp.dhis.common.ValueType; import org.hisp.dhis.feedback.ErrorCode; import org.hisp.dhis.feedback.ErrorMessage; import org.hisp.dhis.option.Option; import org.hisp.dhis.option.OptionSet; -import org.hisp.dhis.period.PeriodType; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -142,38 +138,6 @@ public List getAllDataElements() { return dataElementStore.getAll(); } - @Override - @Transactional(readOnly = true) - public List getAllDataElementsByValueType(ValueType valueType) { - return dataElementStore.getDataElementsByValueType(valueType); - } - - @Override - @Transactional(readOnly = true) - public List getDataElementsByZeroIsSignificant(boolean zeroIsSignificant) { - return dataElementStore.getDataElementsByZeroIsSignificant(zeroIsSignificant); - } - - @Override - @Transactional(readOnly = true) - public List getDataElementsByPeriodType(final PeriodType periodType) { - return getAllDataElements().stream() - .filter(p -> p.getPeriodType() != null && p.getPeriodType().equals(periodType)) - .collect(Collectors.toList()); - } - - @Override - @Transactional(readOnly = true) - public List getDataElementsByDomainType(DataElementDomain domainType) { - return dataElementStore.getDataElementsByDomainType(domainType); - } - - @Override - @Transactional(readOnly = true) - public List getDataElementByCategoryCombo(CategoryCombo categoryCombo) { - return dataElementStore.getDataElementByCategoryCombo(categoryCombo); - } - @Override @Transactional(readOnly = true) public List getDataElementsWithoutGroups() { @@ -295,36 +259,4 @@ public DataElementGroupSet getDataElementGroupSet(long id) { public DataElementGroupSet getDataElementGroupSet(String uid) { return dataElementGroupSetStore.getByUid(uid); } - - @Override - @Transactional(readOnly = true) - public DataElementGroupSet getDataElementGroupSetByName(String name) { - List dataElementGroupSets = dataElementGroupSetStore.getAllEqName(name); - - return !dataElementGroupSets.isEmpty() ? dataElementGroupSets.get(0) : null; - } - - @Override - @Transactional(readOnly = true) - public List getAllDataElementGroupSets() { - return dataElementGroupSetStore.getAll(); - } - - @Override - @Transactional(readOnly = true) - public List getByAttributeAndValue(UID attribute, String value) { - return dataElementStore.getByAttributeAndValue(attribute, value); - } - - @Override - @Transactional(readOnly = true) - public List getByAttribute(UID attribute) { - return dataElementStore.getByAttribute(attribute); - } - - @Override - @Transactional(readOnly = true) - public DataElement getByUniqueAttributeValue(UID attribute, String value) { - return dataElementStore.getByUniqueAttributeValue(attribute, value); - } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataelement/hibernate/HibernateDataElementOperandStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataelement/hibernate/HibernateDataElementOperandStore.java index f43fcac8ca51..8970385f074f 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataelement/hibernate/HibernateDataElementOperandStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataelement/hibernate/HibernateDataElementOperandStore.java @@ -31,6 +31,7 @@ import java.util.Collection; import java.util.List; import javax.annotation.Nonnull; +import org.hisp.dhis.common.UID; import org.hisp.dhis.common.hibernate.HibernateIdentifiableObjectStore; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.dataelement.DataElementOperand; @@ -79,4 +80,17 @@ public List getByDataElement(Collection dataEle .setParameter("dataElements", dataElements) .list(); } + + @Override + public List getByCategoryOptionCombo(@Nonnull Collection uids) { + if (uids.isEmpty()) return List.of(); + return getQuery( + """ + select distinct deo from DataElementOperand deo + join deo.categoryOptionCombo coc + where coc.uid in :uids + """) + .setParameter("uids", UID.toValueList(uids)) + .getResultList(); + } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataelement/hibernate/HibernateDataElementStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataelement/hibernate/HibernateDataElementStore.java index d271ac4f22ee..0c29da27e7b2 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataelement/hibernate/HibernateDataElementStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataelement/hibernate/HibernateDataElementStore.java @@ -33,9 +33,7 @@ import jakarta.persistence.criteria.Root; import java.util.List; import java.util.function.Function; -import lombok.extern.slf4j.Slf4j; import org.hisp.dhis.category.CategoryCombo; -import org.hisp.dhis.common.ValueType; import org.hisp.dhis.common.hibernate.HibernateIdentifiableObjectStore; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.dataelement.DataElementDomain; @@ -51,7 +49,6 @@ /** * @author Torgeir Lorange Ostby */ -@Slf4j @Repository("org.hisp.dhis.dataelement.DataElementStore") public class HibernateDataElementStore extends HibernateIdentifiableObjectStore implements DataElementStore { @@ -67,24 +64,6 @@ public HibernateDataElementStore( // DataElement // ------------------------------------------------------------------------- - @Override - public List getDataElementsByDomainType(DataElementDomain domainType) { - CriteriaBuilder builder = getCriteriaBuilder(); - - return getList( - builder, - newJpaParameters().addPredicate(root -> builder.equal(root.get("domainType"), domainType))); - } - - @Override - public List getDataElementsByValueType(ValueType valueType) { - CriteriaBuilder builder = getCriteriaBuilder(); - - return getList( - builder, - newJpaParameters().addPredicate(root -> builder.equal(root.get("valueType"), valueType))); - } - @Override public List getDataElementByCategoryCombo(CategoryCombo categoryCombo) { CriteriaBuilder builder = getCriteriaBuilder(); @@ -95,17 +74,6 @@ public List getDataElementByCategoryCombo(CategoryCombo categoryCom .addPredicate(root -> builder.equal(root.get("categoryCombo"), categoryCombo))); } - @Override - public List getDataElementsByZeroIsSignificant(boolean zeroIsSignificant) { - CriteriaBuilder builder = getCriteriaBuilder(); - - return getList( - builder, - newJpaParameters() - .addPredicate(root -> builder.equal(root.get("zeroIsSignificant"), zeroIsSignificant)) - .addPredicate(root -> root.get("valueType").in(ValueType.NUMERIC_TYPES))); - } - @Override public List getDataElementsWithoutGroups() { String hql = "from DataElement d where size(d.groups) = 0"; diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataset/DefaultDataSetService.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataset/DefaultDataSetService.java index 20254f48a893..63a1a22f9492 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataset/DefaultDataSetService.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataset/DefaultDataSetService.java @@ -45,7 +45,6 @@ import org.hisp.dhis.dataentryform.DataEntryForm; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.period.Period; -import org.hisp.dhis.period.PeriodType; import org.hisp.dhis.query.QueryParserException; import org.hisp.dhis.security.Authorities; import org.hisp.dhis.user.CurrentUserUtil; @@ -123,12 +122,6 @@ public List getAllDataSets() { return dataSetStore.getAll(); } - @Override - @Transactional(readOnly = true) - public List getDataSetsByPeriodType(PeriodType periodType) { - return dataSetStore.getDataSetsByPeriodType(periodType); - } - @Override @Transactional(readOnly = true) public List getUserDataRead(@Nonnull UserDetails user) { diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataset/hibernate/HibernateCompleteDataSetRegistrationStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataset/hibernate/HibernateCompleteDataSetRegistrationStore.java index c89cc4c00b59..f265f1c6324f 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataset/hibernate/HibernateCompleteDataSetRegistrationStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataset/hibernate/HibernateCompleteDataSetRegistrationStore.java @@ -33,9 +33,12 @@ import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; +import java.util.Collection; import java.util.Date; import java.util.List; +import javax.annotation.Nonnull; import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.common.UID; import org.hisp.dhis.dataset.CompleteDataSetRegistration; import org.hisp.dhis.dataset.CompleteDataSetRegistrationStore; import org.hisp.dhis.dataset.DataSet; @@ -80,6 +83,12 @@ public void saveCompleteDataSetRegistration(CompleteDataSetRegistration registra getSession().save(registration); } + @Override + public void saveWithoutUpdatingLastUpdated(@Nonnull CompleteDataSetRegistration registration) { + registration.setPeriod(periodStore.reloadForceAddPeriod(registration.getPeriod())); + getSession().save(registration); + } + @Override public void updateCompleteDataSetRegistration(CompleteDataSetRegistration registration) { registration.setPeriod(periodStore.reloadForceAddPeriod(registration.getPeriod())); @@ -154,4 +163,32 @@ public int getCompleteDataSetCountLastUpdatedAfter(Date lastUpdated) { return Math.toIntExact(entityManager.createQuery(query).getSingleResult()); } + + @Override + public List getAllByCategoryOptionCombo( + @Nonnull Collection uids) { + if (uids.isEmpty()) return List.of(); + return getQuery( + """ + select cdsr from CompleteDataSetRegistration cdsr + join cdsr.attributeOptionCombo aoc + where aoc.uid in :uids + """) + .setParameter("uids", UID.toValueList(uids)) + .getResultList(); + } + + @Override + public void deleteByCategoryOptionCombo(@Nonnull Collection cocs) { + if (cocs.isEmpty()) return; + String hql = + """ + delete from CompleteDataSetRegistration cdsr + where cdsr.attributeOptionCombo in + (select coc from CategoryOptionCombo coc + where coc in :cocs) + """; + + entityManager.createQuery(hql).setParameter("cocs", cocs).executeUpdate(); + } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataset/hibernate/HibernateDataSetStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataset/hibernate/HibernateDataSetStore.java index 74a1673a7303..09f1850576f4 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataset/hibernate/HibernateDataSetStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/dataset/hibernate/HibernateDataSetStore.java @@ -31,7 +31,6 @@ import com.google.common.collect.Lists; import jakarta.persistence.EntityManager; -import jakarta.persistence.criteria.CriteriaBuilder; import java.util.Collection; import java.util.List; import javax.annotation.Nonnull; @@ -42,7 +41,6 @@ import org.hisp.dhis.dataset.DataSet; import org.hisp.dhis.dataset.DataSetElement; import org.hisp.dhis.dataset.DataSetStore; -import org.hisp.dhis.hibernate.JpaQueryParameters; import org.hisp.dhis.period.PeriodService; import org.hisp.dhis.period.PeriodType; import org.hisp.dhis.security.acl.AclService; @@ -97,19 +95,6 @@ public void update(@Nonnull DataSet dataSet) { super.update(dataSet); } - @Override - public List getDataSetsByPeriodType(PeriodType periodType) { - PeriodType refreshedPeriodType = periodService.reloadPeriodType(periodType); - - CriteriaBuilder builder = getCriteriaBuilder(); - - JpaQueryParameters parameters = - newJpaParameters() - .addPredicate(root -> builder.equal(root.get("periodType"), refreshedPeriodType)); - - return getList(builder, parameters); - } - @Override public List getDataSetsByDataEntryForm(DataEntryForm dataEntryForm) { if (dataEntryForm == null) { diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/DataValueUtil.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/DataValueUtil.java new file mode 100644 index 000000000000..8f844f2a5d5d --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/DataValueUtil.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.datavalue; + +import java.util.function.BiFunction; +import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.common.BaseIdentifiableObject; +import org.hisp.dhis.dataelement.DataElement; + +public final class DataValueUtil { + + private DataValueUtil() {} + + /** + * Creates and returns a new {@link DataValue}. All the old values are used from the supplied old + * {@link DataValue} except for the {@link DataElement} field, which uses the supplied {@link + * DataElement}. + */ + public static final BiFunction + dataValueWithNewDataElement = + (oldDv, newDataElement) -> { + DataValue newValue = + DataValue.builder() + .dataElement((DataElement) newDataElement) + .period(oldDv.getPeriod()) + .source(oldDv.getSource()) + .categoryOptionCombo(oldDv.getCategoryOptionCombo()) + .attributeOptionCombo(oldDv.getAttributeOptionCombo()) + .value(oldDv.getValue()) + .storedBy(oldDv.getStoredBy()) + .lastUpdated(oldDv.getLastUpdated()) + .comment(oldDv.getComment()) + .followup(oldDv.isFollowup()) + .deleted(oldDv.isDeleted()) + .build(); + newValue.setCreated(oldDv.getCreated()); + return newValue; + }; + + /** + * Creates and returns a new {@link DataValue}. All the old values are used from the supplied old + * {@link DataValue} except for the {@link CategoryOptionCombo} field, which uses the supplied + * {@link CategoryOptionCombo}. + */ + public static final BiFunction + dataValueWithNewCatOptionCombo = + (oldDv, newCoc) -> { + DataValue newValue = + DataValue.builder() + .dataElement(oldDv.getDataElement()) + .period(oldDv.getPeriod()) + .source(oldDv.getSource()) + .categoryOptionCombo((CategoryOptionCombo) newCoc) + .attributeOptionCombo(oldDv.getAttributeOptionCombo()) + .value(oldDv.getValue()) + .storedBy(oldDv.getStoredBy()) + .lastUpdated(oldDv.getLastUpdated()) + .comment(oldDv.getComment()) + .followup(oldDv.isFollowup()) + .deleted(oldDv.isDeleted()) + .build(); + newValue.setCreated(oldDv.getCreated()); + return newValue; + }; + + /** + * Creates and returns a new {@link DataValue}. All the old values are used from the supplied old + * {@link DataValue} except for the attributeOptionCombo} field, which uses the supplied {@link + * CategoryOptionCombo}. + */ + public static final BiFunction + dataValueWithNewAttrOptionCombo = + (oldDv, newAoc) -> { + DataValue newValue = + DataValue.builder() + .dataElement(oldDv.getDataElement()) + .period(oldDv.getPeriod()) + .source(oldDv.getSource()) + .categoryOptionCombo(oldDv.getCategoryOptionCombo()) + .attributeOptionCombo((CategoryOptionCombo) newAoc) + .value(oldDv.getValue()) + .storedBy(oldDv.getStoredBy()) + .lastUpdated(oldDv.getLastUpdated()) + .comment(oldDv.getComment()) + .followup(oldDv.isFollowup()) + .deleted(oldDv.isDeleted()) + .build(); + newValue.setCreated(oldDv.getCreated()); + return newValue; + }; +} diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueAuditStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueAuditStore.java index dab7ac29f2b9..283d96cfdca4 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueAuditStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueAuditStore.java @@ -36,6 +36,8 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Function; +import javax.annotation.Nonnull; +import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.datavalue.DataValueAudit; import org.hisp.dhis.datavalue.DataValueAuditQueryParams; @@ -104,6 +106,16 @@ public void deleteDataValueAudits(DataElement dataElement) { entityManager.createQuery(hql).setParameter("dataElement", dataElement).executeUpdate(); } + @Override + public void deleteDataValueAudits(@Nonnull CategoryOptionCombo categoryOptionCombo) { + String hql = + "delete from DataValueAudit d where d.categoryOptionCombo = :categoryOptionCombo or d.attributeOptionCombo = :categoryOptionCombo"; + entityManager + .createQuery(hql) + .setParameter("categoryOptionCombo", categoryOptionCombo) + .executeUpdate(); + } + @Override public List getDataValueAudits(DataValueAuditQueryParams params) { CriteriaBuilder builder = entityManager.getCriteriaBuilder(); diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueStore.java index 338b825c5453..d4f026b62a20 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueStore.java @@ -51,10 +51,13 @@ import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.function.Function; +import java.util.stream.Collectors; +import javax.annotation.Nonnull; import lombok.extern.slf4j.Slf4j; import org.hibernate.query.Query; import org.hisp.dhis.category.CategoryCombo; import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.common.UID; import org.hisp.dhis.commons.util.SqlHelper; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.datavalue.DataExportParams; @@ -129,6 +132,35 @@ public void deleteDataValues(DataElement dataElement) { entityManager.createQuery(hql).setParameter("dataElement", dataElement).executeUpdate(); } + @Override + public void deleteDataValues(@Nonnull Collection dataElements) { + String hql = "delete from DataValue d where d.dataElement in :dataElements"; + + entityManager.createQuery(hql).setParameter("dataElements", dataElements).executeUpdate(); + } + + @Override + public void deleteDataValuesByCategoryOptionCombo( + @Nonnull Collection categoryOptionCombos) { + String hql = "delete from DataValue d where d.categoryOptionCombo in :categoryOptionCombos"; + + entityManager + .createQuery(hql) + .setParameter("categoryOptionCombos", categoryOptionCombos) + .executeUpdate(); + } + + @Override + public void deleteDataValuesByAttributeOptionCombo( + @Nonnull Collection attributeOptionCombos) { + String hql = "delete from DataValue d where d.attributeOptionCombo in :attributeOptionCombos"; + + entityManager + .createQuery(hql) + .setParameter("attributeOptionCombos", attributeOptionCombos) + .executeUpdate(); + } + @Override public void deleteDataValue(DataValue dataValue) { getQuery("delete from DataValue dv where dv = :dataValue") @@ -276,6 +308,206 @@ public boolean dataValueExistsForDataElement(String uid) { .isEmpty(); } + @Override + public List getAllDataValuesByCatOptCombo(@Nonnull Collection uids) { + if (uids.isEmpty()) return List.of(); + return getQuery( + """ + select dv from DataValue dv + join dv.categoryOptionCombo coc + where coc.uid in :uids + """) + .setParameter("uids", UID.toValueList(uids)) + .getResultList(); + } + + @Override + public List getAllDataValuesByAttrOptCombo(@Nonnull Collection uids) { + if (uids.isEmpty()) return List.of(); + return getQuery( + """ + select dv from DataValue dv + join dv.attributeOptionCombo aoc + where aoc.uid in :uids + """) + .setParameter("uids", UID.toValueList(uids)) + .getResultList(); + } + + @Override + public void mergeDataValuesWithCategoryOptionCombos( + @Nonnull CategoryOptionCombo target, @Nonnull Collection sources) { + String plpgsql = + """ + do + $$ + declare + source_dv record; + target_duplicate record; + target_coc bigint default %s; + begin + + -- loop through each record with a source CategoryOptionCombo + for source_dv in + select * from datavalue where categoryoptioncomboid in (%s) + loop + + -- check if target Data Value exists with same unique key + select dv.* + into target_duplicate + from datavalue dv + where dv.dataelementid = source_dv.dataelementid + and dv.periodid = source_dv.periodid + and dv.sourceid = source_dv.sourceid + and dv.attributeoptioncomboid = source_dv.attributeoptioncomboid + and dv.categoryoptioncomboid = target_coc; + + -- target duplicate found and target has latest lastUpdated value + if (target_duplicate.categoryoptioncomboid is not null + and target_duplicate.lastupdated >= source_dv.lastupdated) + then + -- delete source + delete from datavalue + where dataelementid = source_dv.dataelementid + and periodid = source_dv.periodid + and sourceid = source_dv.sourceid + and attributeoptioncomboid = source_dv.attributeoptioncomboid + and categoryoptioncomboid = source_dv.categoryoptioncomboid; + + -- target duplicate found and source has latest lastUpdated value + elsif (target_duplicate.categoryoptioncomboid is not null + and target_duplicate.lastupdated < source_dv.lastupdated) + then + -- delete target + delete from datavalue + where dataelementid = target_duplicate.dataelementid + and periodid = target_duplicate.periodid + and sourceid = target_duplicate.sourceid + and attributeoptioncomboid = target_duplicate.attributeoptioncomboid + and categoryoptioncomboid = target_duplicate.categoryoptioncomboid; + + -- update source with target CategoryOptionCombo + update datavalue + set categoryoptioncomboid = target_coc + where dataelementid = source_dv.dataelementid + and periodid = source_dv.periodid + and sourceid = source_dv.sourceid + and attributeoptioncomboid = source_dv.attributeoptioncomboid + and categoryoptioncomboid = source_dv.categoryoptioncomboid; + + else + -- no target duplicate found, update source with target CategoryOptionCombo + update datavalue + set categoryoptioncomboid = target_coc + where dataelementid = source_dv.dataelementid + and periodid = source_dv.periodid + and sourceid = source_dv.sourceid + and attributeoptioncomboid = source_dv.attributeoptioncomboid + and categoryoptioncomboid = source_dv.categoryoptioncomboid; + + end if; + + end loop; + end; + $$ + language plpgsql; + """ + .formatted( + target.getId(), + sources.stream() + .map(s -> String.valueOf(s.getId())) + .collect(Collectors.joining(","))); + + jdbcTemplate.update(plpgsql); + } + + @Override + public void mergeDataValuesWithAttributeOptionCombos( + @Nonnull CategoryOptionCombo target, @Nonnull Collection sources) { + String plpgsql = + """ + do + $$ + declare + source_dv record; + target_duplicate record; + target_aoc bigint default %s; + begin + + -- loop through each record with a source Attribute Option Combo + for source_dv in + select * from datavalue where attributeoptioncomboid in (%s) + loop + + -- check if target DataValue exists with same unique key + select dv.* + into target_duplicate + from datavalue dv + where dv.dataelementid = source_dv.dataelementid + and dv.periodid = source_dv.periodid + and dv.sourceid = source_dv.sourceid + and dv.attributeoptioncomboid = target_aoc + and dv.categoryoptioncomboid = source_dv.categoryoptioncomboid; + + -- target duplicate found and target has latest lastUpdated value + if (target_duplicate.attributeoptioncomboid is not null + and target_duplicate.lastupdated >= source_dv.lastupdated) + then + -- delete source + delete from datavalue + where dataelementid = source_dv.dataelementid + and periodid = source_dv.periodid + and sourceid = source_dv.sourceid + and attributeoptioncomboid = source_dv.attributeoptioncomboid + and categoryoptioncomboid = source_dv.categoryoptioncomboid; + + -- target duplicate found and source has latest lastUpdated value + elsif (target_duplicate.attributeoptioncomboid is not null + and target_duplicate.lastupdated < source_dv.lastupdated) + then + -- delete target + delete from datavalue + where dataelementid = target_duplicate.dataelementid + and periodid = target_duplicate.periodid + and sourceid = target_duplicate.sourceid + and attributeoptioncomboid = target_duplicate.attributeoptioncomboid + and categoryoptioncomboid = target_duplicate.categoryoptioncomboid; + + -- update source with target Attribute Option Combo + update datavalue + set attributeoptioncomboid = target_duplicate.attributeoptioncomboid + where dataelementid = source_dv.dataelementid + and periodid = source_dv.periodid + and sourceid = source_dv.sourceid + and attributeoptioncomboid = source_dv.attributeoptioncomboid + and categoryoptioncomboid = source_dv.categoryoptioncomboid; + + else + -- no target duplicate found, update source with target Attribute Option Combo + update datavalue + SET attributeoptioncomboid = target_aoc + where dataelementid = source_dv.dataelementid + and periodid = source_dv.periodid + and sourceid = source_dv.sourceid + and attributeoptioncomboid = source_dv.attributeoptioncomboid + and categoryoptioncomboid = source_dv.categoryoptioncomboid; + + end if; + + end loop; + end; + $$ + language plpgsql; + """ + .formatted( + target.getId(), + sources.stream() + .map(s -> String.valueOf(s.getId())) + .collect(Collectors.joining(","))); + + jdbcTemplate.update(plpgsql); + } + // ------------------------------------------------------------------------- // getDataValues and related supportive methods // ------------------------------------------------------------------------- @@ -334,7 +566,7 @@ private String getDataValuesHql( hql.append( params.getOrganisationUnits().stream() - .map(OrganisationUnit::getPath) + .map(OrganisationUnit::getStoredPath) .map(p -> "ou.path like '" + p + "%'") .collect(joining(" or "))); @@ -565,7 +797,7 @@ private void getDdvOrgUnits( where .append(sqlHelper.or()) .append("ou.path like '") - .append(parent.getPath()) + .append(parent.getStoredPath()) .append("%'"); } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/expression/function/FunctionOrgUnitAncestor.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/expression/function/FunctionOrgUnitAncestor.java index 891620983f60..915df6787b65 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/expression/function/FunctionOrgUnitAncestor.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/expression/function/FunctionOrgUnitAncestor.java @@ -65,7 +65,7 @@ public Object evaluate(ExpressionParser.ExprContext ctx, CommonExpressionVisitor if (orgUnit != null) { for (TerminalNode uid : ctx.UID()) { - if (orgUnit.getPath().contains(uid.getText() + "/")) { + if (orgUnit.getStoredPath().contains(uid.getText() + "/")) { return true; } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/organisationunit/DefaultOrganisationUnitService.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/organisationunit/DefaultOrganisationUnitService.java index 7a3bf1cc9eeb..4342ec0960ec 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/organisationunit/DefaultOrganisationUnitService.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/organisationunit/DefaultOrganisationUnitService.java @@ -53,7 +53,6 @@ import org.hisp.dhis.commons.collection.ListUtils; import org.hisp.dhis.commons.filter.FilterUtils; import org.hisp.dhis.configuration.ConfigurationService; -import org.hisp.dhis.dataset.DataSet; import org.hisp.dhis.expression.ExpressionService; import org.hisp.dhis.feedback.BadRequestException; import org.hisp.dhis.hierarchy.HierarchyViolationException; @@ -73,12 +72,10 @@ public class DefaultOrganisationUnitService implements OrganisationUnitService { private static final String LEVEL_PREFIX = "Level "; private final OrganisationUnitStore organisationUnitStore; - private final IdentifiableObjectManager idObjectManager; private final OrganisationUnitLevelStore organisationUnitLevelStore; private final ConfigurationService configurationService; private final Cache inUserOrgUnitHierarchyCache; - private final Cache inUserOrgUnitSearchHierarchyCache; public DefaultOrganisationUnitService( OrganisationUnitStore organisationUnitStore, @@ -94,13 +91,10 @@ public DefaultOrganisationUnitService( checkNotNull(cacheProvider); this.organisationUnitStore = organisationUnitStore; - this.idObjectManager = idObjectManager; this.organisationUnitLevelStore = organisationUnitLevelStore; this.configurationService = configurationService; this.inUserOrgUnitHierarchyCache = cacheProvider.createInUserOrgUnitHierarchyCache(); - this.inUserOrgUnitSearchHierarchyCache = - cacheProvider.createInUserSearchOrgUnitHierarchyCache(); } // ------------------------------------------------------------------------- @@ -362,34 +356,6 @@ public Long getOrganisationUnitHierarchyMemberCount( parent, member, collectionName); } - @Override - @Transactional(readOnly = true) - public OrganisationUnitDataSetAssociationSet getOrganisationUnitDataSetAssociationSet(User user) { - - Set organisationUnits = user != null ? user.getOrganisationUnits() : null; - List dataSets = idObjectManager.getDataWriteAll(DataSet.class); - - Map> associationSet = - organisationUnitStore.getOrganisationUnitDataSetAssocationMap(organisationUnits, dataSets); - - OrganisationUnitDataSetAssociationSet set = new OrganisationUnitDataSetAssociationSet(); - - for (Map.Entry> entry : associationSet.entrySet()) { - int index = set.getDataSetAssociationSets().indexOf(entry.getValue()); - - if (index == -1) // Association set does not exist, add new - { - index = set.getDataSetAssociationSets().size(); - set.getDataSetAssociationSets().add(entry.getValue()); - } - - set.getOrganisationUnitAssociationSetMap().put(entry.getKey(), index); - set.getDistinctDataSets().addAll(entry.getValue()); - } - - return set; - } - @Override @Transactional(readOnly = true) public boolean isInUserHierarchyCached(User user, OrganisationUnit organisationUnit) { @@ -424,15 +390,6 @@ public boolean isInUserDataViewHierarchy(User user, OrganisationUnit organisatio return organisationUnit.isDescendant(user.getDataViewOrganisationUnitsWithFallback()); } - @Override - @Transactional(readOnly = true) - public boolean isInUserSearchHierarchyCached(User user, OrganisationUnit organisationUnit) { - String cacheKey = joinHyphen(user.getUsername(), organisationUnit.getUid()); - - return inUserOrgUnitSearchHierarchyCache.get( - cacheKey, ou -> isInUserSearchHierarchy(user, organisationUnit)); - } - @Override @Transactional(readOnly = true) public boolean isInUserSearchHierarchy(User user, OrganisationUnit organisationUnit) { diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/organisationunit/hibernate/HibernateOrganisationUnitStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/organisationunit/hibernate/HibernateOrganisationUnitStore.java index d45785e0a15d..aff18bb99259 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/organisationunit/hibernate/HibernateOrganisationUnitStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/organisationunit/hibernate/HibernateOrganisationUnitStore.java @@ -29,42 +29,33 @@ import static com.google.common.base.Preconditions.checkNotNull; import static java.util.stream.Collectors.toSet; -import static org.hisp.dhis.system.util.SqlUtils.escape; import jakarta.persistence.EntityManager; import java.util.ArrayList; import java.util.Collection; import java.util.Date; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; import org.hibernate.Session; import org.hibernate.query.Query; import org.hisp.dhis.common.IdentifiableObjectUtils; import org.hisp.dhis.common.hibernate.HibernateIdentifiableObjectStore; import org.hisp.dhis.commons.util.SqlHelper; import org.hisp.dhis.commons.util.TextUtils; -import org.hisp.dhis.dataset.DataSet; import org.hisp.dhis.dbms.DbmsManager; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.organisationunit.OrganisationUnitQueryParams; import org.hisp.dhis.organisationunit.OrganisationUnitStore; import org.hisp.dhis.program.Program; import org.hisp.dhis.security.acl.AclService; -import org.hisp.dhis.system.util.SqlUtils; import org.springframework.context.ApplicationEventPublisher; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; -import org.springframework.util.Assert; /** * @author Kristian Nordal */ -@Slf4j @Repository("org.hisp.dhis.organisationunit.OrganisationUnitStore") public class HibernateOrganisationUnitStore extends HibernateIdentifiableObjectStore implements OrganisationUnitStore { @@ -195,7 +186,7 @@ public Long getOrganisationUnitHierarchyMemberCount( + ")"; Query query = getTypedQuery(hql); - query.setParameter("path", parent.getPath() + "%").setParameter("object", member); + query.setParameter("path", parent.getStoredPath() + "%").setParameter("object", member); return query.getSingleResult(); } @@ -253,7 +244,7 @@ public List getOrganisationUnits(OrganisationUnitQueryParams p if (params.hasParents()) { for (OrganisationUnit parent : params.getParents()) { - query.setParameter(parent.getUid(), parent.getPath() + "%"); + query.setParameter(parent.getUid(), parent.getStoredPath() + "%"); } } @@ -281,57 +272,6 @@ public List getOrganisationUnits(OrganisationUnitQueryParams p return query.list(); } - @Override - public Map> getOrganisationUnitDataSetAssocationMap( - Collection organisationUnits, Collection dataSets) { - SqlHelper hlp = new SqlHelper(); - - String sql = - "select ou.uid as ou_uid, array_agg(ds.uid) as ds_uid " - + "from datasetsource d " - + "inner join organisationunit ou on ou.organisationunitid=d.sourceid " - + "inner join dataset ds on ds.datasetid=d.datasetid "; - - if (organisationUnits != null) { - Assert.notEmpty(organisationUnits, "Organisation units cannot be empty"); - - sql += hlp.whereAnd() + " ("; - - for (OrganisationUnit unit : organisationUnits) { - sql += "ou.path like '" + unit.getPath() + "%' or "; - } - - sql = TextUtils.removeLastOr(sql) + ") "; - } - - if (dataSets != null) { - Assert.notEmpty(dataSets, "Data sets cannot be empty"); - - sql += - hlp.whereAnd() - + " ds.datasetid in (" - + StringUtils.join(IdentifiableObjectUtils.getIdentifiers(dataSets), ",") - + ") "; - } - - sql += "group by ou_uid"; - - log.debug("Org unit data set association map SQL: " + sql); - - Map> map = new HashMap<>(); - - jdbcTemplate.query( - sql, - rs -> { - String organisationUnitId = rs.getString("ou_uid"); - Set dataSetIds = SqlUtils.getArrayAsSet(rs, "ds_uid"); - - map.put(organisationUnitId, dataSetIds); - }); - - return map; - } - @Override public List getWithinCoordinateArea(double[] box) { // can't use hibernate-spatial 'makeenvelope' function, because not @@ -379,31 +319,11 @@ public int getMaxLevel() { String hql = "select max(ou.hierarchyLevel) from OrganisationUnit ou"; Query query = getTypedQuery(hql); - Integer maxLength = query.getSingleResult(); + Integer maxLength = getSingleResult(query); return maxLength != null ? maxLength : 0; } - @Override - public boolean isOrgUnitCountAboveThreshold(OrganisationUnitQueryParams params, int threshold) { - String sql = buildOrganisationUnitDistinctUidsSql(params); - - StringBuilder sb = new StringBuilder(); - sb.append("select count(*) from ("); - sb.append(sql); - sb.append(" limit "); - sb.append(threshold + 1); - sb.append(") as douid"); - - return (jdbcTemplate.queryForObject(sb.toString(), Integer.class) > threshold); - } - - @Override - public List getOrganisationUnitUids(OrganisationUnitQueryParams params) { - String sql = buildOrganisationUnitDistinctUidsSql(params); - return jdbcTemplate.queryForList(sql, String.class); - } - @Override public int updateAllOrganisationUnitsGeometryToNull() { return getQuery("update OrganisationUnit o set o.geometry = null").executeUpdate(); @@ -423,30 +343,6 @@ public List getByCategoryOption(@Nonnull Collection ca .getResultList(); } - private String buildOrganisationUnitDistinctUidsSql(OrganisationUnitQueryParams params) { - SqlHelper hlp = new SqlHelper(); - - String sql = "select distinct o.uid from organisationunit o "; - - if (params.isFetchChildren()) { - sql += " left outer join organisationunit c ON o.organisationunitid = c.parentid "; - } - - if (params.hasParents()) { - sql += hlp.whereAnd() + " ("; - - for (OrganisationUnit parent : params.getParents()) { - sql += "o.path like '" + escape(parent.getPath()) + "%'" + " or "; - } - - sql = TextUtils.removeLastOr(sql) + ") "; - } - - // TODO: Support Groups + Query + Hierarchy + MaxLevels in this sql - - return sql; - } - private void updatePaths(List organisationUnits) { Session session = getSession(); int counter = 0; diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/predictor/hibernate/HibernatePredictorStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/predictor/hibernate/HibernatePredictorStore.java index d88cce1894c0..e1e2069a2fad 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/predictor/hibernate/HibernatePredictorStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/predictor/hibernate/HibernatePredictorStore.java @@ -33,6 +33,7 @@ import java.util.Collection; import java.util.List; import javax.annotation.Nonnull; +import org.hisp.dhis.common.UID; import org.hisp.dhis.common.hibernate.HibernateIdentifiableObjectStore; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.period.PeriodService; @@ -125,4 +126,17 @@ public List getAllWithSampleSkipTestContainingDataElement( .formatted(multiLike)) .getResultList(); } + + @Override + public List getByCategoryOptionCombo(@Nonnull Collection uids) { + if (uids.isEmpty()) return List.of(); + return getQuery( + """ + select distinct p from Predictor p + join p.outputCombo coc + where coc.uid in :uids + """) + .setParameter("uids", UID.toValueList(uids)) + .list(); + } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/program/hibernate/HibernateEventStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/program/hibernate/HibernateEventStore.java index 87a578577bc2..06670fa25518 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/program/hibernate/HibernateEventStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/program/hibernate/HibernateEventStore.java @@ -33,7 +33,9 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import java.util.List; +import java.util.Set; import java.util.function.Function; +import java.util.stream.Collectors; import org.hisp.dhis.common.UID; import org.hisp.dhis.common.hibernate.SoftDeleteHibernateObjectStore; import org.hisp.dhis.dataelement.DataElement; @@ -88,4 +90,18 @@ where jsonb_exists_any(e.eventdatavalues, :searchStrings) "searchStrings", searchStrings.toArray(String[]::new), StringArrayType.INSTANCE) .getResultList(); } + + @Override + public void setAttributeOptionCombo(Set cocs, long coc) { + if (cocs.isEmpty()) return; + String sql = + """ + update event + set attributeoptioncomboid = %s + where attributeoptioncomboid in (%s) + """ + .formatted(coc, cocs.stream().map(String::valueOf).collect(Collectors.joining(","))); + + entityManager.createNativeQuery(sql).executeUpdate(); + } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/DefaultJobConfigurationService.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/DefaultJobConfigurationService.java index b3af654bb06b..e1749f453806 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/DefaultJobConfigurationService.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/DefaultJobConfigurationService.java @@ -63,8 +63,8 @@ import org.hisp.dhis.commons.util.TextUtils; import org.hisp.dhis.feedback.ConflictException; import org.hisp.dhis.feedback.ErrorCode; -import org.hisp.dhis.feedback.NotFoundException; import org.hisp.dhis.fileresource.FileResource; +import org.hisp.dhis.fileresource.FileResourceDomain; import org.hisp.dhis.fileresource.FileResourceService; import org.hisp.dhis.fileresource.FileResourceStorageStatus; import org.hisp.dhis.jsontree.JsonMixed; @@ -92,17 +92,30 @@ public class DefaultJobConfigurationService implements JobConfigurationService { private final JobConfigurationStore jobConfigurationStore; private final FileResourceService fileResourceService; private final SystemSettingsProvider settingsProvider; - private final JobCreationHelper jobCreationHelper; @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) public String create(JobConfiguration config) throws ConflictException { - return jobCreationHelper.create(config); + config.setAutoFields(); + jobConfigurationStore.save(config); + return config.getUid(); } @Override + @Transactional(propagation = Propagation.REQUIRES_NEW) public String create(JobConfiguration config, MimeType contentType, InputStream content) throws ConflictException { - return jobCreationHelper.create(config, contentType, content); + if (config.getSchedulingType() != SchedulingType.ONCE_ASAP) + throw new ConflictException( + "Job must be of type %s to allow content data".formatted(SchedulingType.ONCE_ASAP)); + config.setAutoFields(); // ensure UID is set + FileResource fr = + FileResource.ofKey(FileResourceDomain.JOB_DATA, config.getUid(), contentType.toString()); + fr.setUid(config.getUid()); + fr.setAssigned(true); + fileResourceService.syncSaveFileResource(fr, content); + jobConfigurationStore.save(config); + return config.getUid(); } @Override @@ -140,43 +153,6 @@ public void createDefaultJob(JobType type) { createDefaultJob(type, CurrentUserUtil.getCurrentUserDetails()); } - @Override - @Transactional(propagation = Propagation.REQUIRES_NEW) - public String createInTransaction( - JobConfiguration jobConfiguration, MimeType contentType, InputStream content) - throws ConflictException, NotFoundException { - String jobId = jobCreationHelper.create(jobConfiguration, contentType, content); - - if (!jobConfigurationStore.executeNow(jobId)) { - JobConfiguration job = jobConfigurationStore.getByUid(jobId); - if (job == null) throw new NotFoundException(JobConfiguration.class, jobId); - if (job.getJobStatus() == JobStatus.RUNNING) - throw new ConflictException("Job is already running."); - if (job.getSchedulingType() == SchedulingType.ONCE_ASAP && job.getLastFinished() != null) - throw new ConflictException("Job did already run once."); - throw new ConflictException("Failed to transition job into ONCE_ASAP state."); - } - return jobId; - } - - @Override - @Transactional(propagation = Propagation.REQUIRES_NEW) - public String createInTransaction(JobConfiguration jobConfiguration) - throws ConflictException, NotFoundException { - String jobId = jobCreationHelper.create(jobConfiguration); - - if (!jobConfigurationStore.executeNow(jobId)) { - JobConfiguration job = jobConfigurationStore.getByUid(jobId); - if (job == null) throw new NotFoundException(JobConfiguration.class, jobId); - if (job.getJobStatus() == JobStatus.RUNNING) - throw new ConflictException("Job is already running."); - if (job.getSchedulingType() == SchedulingType.ONCE_ASAP && job.getLastFinished() != null) - throw new ConflictException("Job did already run once."); - throw new ConflictException("Failed to transition job into ONCE_ASAP state."); - } - return jobId; - } - @Override @Transactional public int updateDisabledJobs() { diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/JobCreationHelper.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/DefaultJobExecutionService.java similarity index 50% rename from dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/JobCreationHelper.java rename to dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/DefaultJobExecutionService.java index 5c15c2aff28f..b2b68b0038c9 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/JobCreationHelper.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/DefaultJobExecutionService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2004-2024, University of Oslo + * Copyright (c) 2004-2025, University of Oslo * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -28,45 +28,54 @@ package org.hisp.dhis.scheduling; import java.io.InputStream; +import javax.annotation.Nonnull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.hisp.dhis.common.NonTransactional; import org.hisp.dhis.feedback.ConflictException; -import org.hisp.dhis.fileresource.FileResource; -import org.hisp.dhis.fileresource.FileResourceDomain; -import org.hisp.dhis.fileresource.FileResourceService; +import org.hisp.dhis.feedback.NotFoundException; +import org.springframework.stereotype.Service; import org.springframework.util.MimeType; -/** - * @author Morten Svanæs - */ -public interface JobCreationHelper { +@Slf4j +@Service +@RequiredArgsConstructor +public class DefaultJobExecutionService implements JobExecutionService { + + private final JobConfigurationService jobConfigurationService; + private final JobSchedulerService jobSchedulerService; - String create(JobConfiguration config) throws ConflictException; + @Override + @NonTransactional + public void executeOnceNow( + @Nonnull JobConfiguration config, @Nonnull MimeType contentType, @Nonnull InputStream content) + throws ConflictException { + validateIsNewRunOnce(config); + executeOnceNow(jobConfigurationService.create(config, contentType, content)); + } - String create(JobConfiguration config, MimeType contentType, InputStream content) - throws ConflictException; + @Override + @NonTransactional + public void executeOnceNow(@Nonnull JobConfiguration config) throws ConflictException { + validateIsNewRunOnce(config); + executeOnceNow(jobConfigurationService.create(config)); + } - default String createFromConfig(JobConfiguration config, JobConfigurationStore store) { - config.setAutoFields(); - store.save(config); - return config.getUid(); + private void executeOnceNow(String jobId) throws ConflictException { + try { + jobSchedulerService.executeNow(jobId); + } catch (NotFoundException ex) { + log.error("Ad-hoc job creation failed", ex); + ConflictException error = new ConflictException("Ad-hoc job creation failed"); + error.initCause(ex); + throw error; + } } - default String createFromConfigAndInputStream( - JobConfiguration config, - MimeType contentType, - InputStream content, - JobConfigurationStore store, - FileResourceService fileResourceService) - throws ConflictException { - if (config.getSchedulingType() != SchedulingType.ONCE_ASAP) + private void validateIsNewRunOnce(JobConfiguration config) throws ConflictException { + if (config.getId() != 0 || config.getSchedulingType() != SchedulingType.ONCE_ASAP) throw new ConflictException( - "Job must be of type %s to allow content data".formatted(SchedulingType.ONCE_ASAP)); - config.setAutoFields(); // ensure UID is set - FileResource fr = - FileResource.ofKey(FileResourceDomain.JOB_DATA, config.getUid(), contentType.toString()); - fr.setUid(config.getUid()); - fr.setAssigned(true); - fileResourceService.syncSaveFileResource(fr, content); - store.save(config); - return config.getUid(); + "Job %s must be a run once type but was: %s" + .formatted(config.getName(), config.getSchedulingType())); } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/DefaultJobSchedulerService.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/DefaultJobSchedulerService.java index dedad7f48690..a36cc0e5426d 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/DefaultJobSchedulerService.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/DefaultJobSchedulerService.java @@ -33,7 +33,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import java.io.InputStream; import java.util.Collection; import java.util.List; import java.util.Map; @@ -50,9 +49,7 @@ import org.hisp.dhis.scheduling.JobProgress.Progress; import org.hisp.dhis.user.UserDetails; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.MimeType; /** * @author Jan Bernitt @@ -63,11 +60,9 @@ @RequiredArgsConstructor public class DefaultJobSchedulerService implements JobSchedulerService { - private final JobConfigurationStore jobConfigurationStore; private final JobRunner jobRunner; + private final JobConfigurationStore jobConfigurationStore; private final ObjectMapper jsonMapper; - private final JobCreationHelper jobCreationHelper; - private final JobConfigurationService jobConfigurationService; @Override @Transactional @@ -82,11 +77,16 @@ public boolean requestCancel(@Nonnull JobType type) { return jobId != null && requestCancel(jobId); } + /** + * Note that the TX is opened on the store level for {@code tryExecuteNow} so that state changes + * to the job are already visible to other threads even when called from within this method as + * done in case of continuous execution. + */ @Override - @Transactional + @NonTransactional public void executeNow(@Nonnull String jobId) throws NotFoundException, ConflictException { if (!jobConfigurationStore.tryExecuteNow(jobId)) { - JobConfiguration job = jobConfigurationStore.getByUid(jobId); + JobConfiguration job = jobConfigurationStore.getByUidNoAcl(jobId); if (job == null) throw new NotFoundException(JobConfiguration.class, jobId); if (job.getJobStatus() == JobStatus.RUNNING) throw new ConflictException("Job is already running."); @@ -95,45 +95,12 @@ public void executeNow(@Nonnull String jobId) throws NotFoundException, Conflict throw new ConflictException("Failed to transition job into ONCE_ASAP state."); } if (!jobRunner.isScheduling()) { - JobConfiguration job = jobConfigurationStore.getByUid(jobId); - if (job == null) throw new NotFoundException(JobConfiguration.class, jobId); - // run "execute now" request directly when scheduling is not active (tests) - jobRunner.runDueJob(job); - } else { - JobConfiguration job = jobConfigurationStore.getByUid(jobId); - if (job == null) throw new NotFoundException(JobConfiguration.class, jobId); - if (job.getJobType().isUsingContinuousExecution()) { - jobRunner.runIfDue(job); - } - } - } - - @Override - @NonTransactional - public void createThenExecute(JobConfiguration config, MimeType contentType, InputStream content) - throws ConflictException, NotFoundException { - String jobId = jobConfigurationService.createInTransaction(config, contentType, content); - runInTransaction(jobId); - } - - @Override - @NonTransactional - public void createThenExecute(JobConfiguration config) - throws ConflictException, NotFoundException { - String jobId = jobConfigurationService.createInTransaction(config); - runInTransaction(jobId); - } - - @Override - @Transactional(propagation = Propagation.REQUIRES_NEW) - public void runInTransaction(String jobId) throws NotFoundException, ConflictException { - if (!jobRunner.isScheduling()) { - JobConfiguration job = jobConfigurationStore.getByUid(jobId); + JobConfiguration job = jobConfigurationStore.getByUidNoAcl(jobId); if (job == null) throw new NotFoundException(JobConfiguration.class, jobId); // run "execute now" request directly when scheduling is not active (tests) jobRunner.runDueJob(job); } else { - JobConfiguration job = jobConfigurationStore.getByUid(jobId); + JobConfiguration job = jobConfigurationStore.getByUidNoAcl(jobId); if (job == null) throw new NotFoundException(JobConfiguration.class, jobId); if (job.getJobType().isUsingContinuousExecution()) { jobRunner.runIfDue(job); @@ -149,7 +116,7 @@ public void revertNow(@Nonnull UID jobId) if (!currentUser.isAuthorized(F_PERFORM_MAINTENANCE)) throw new ForbiddenException(JobConfiguration.class, jobId.getValue()); if (!jobConfigurationStore.tryRevertNow(jobId.getValue())) { - JobConfiguration job = jobConfigurationStore.getByUid(jobId.getValue()); + JobConfiguration job = jobConfigurationStore.getByUidNoAcl(jobId.getValue()); if (job == null) throw new NotFoundException(JobConfiguration.class, jobId.getValue()); if (job.getJobStatus() != JobStatus.RUNNING) throw new ConflictException("Job is not running"); diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/HibernateJobConfigurationStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/HibernateJobConfigurationStore.java index 0cc216f956a8..fbfb39ea9af1 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/HibernateJobConfigurationStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/HibernateJobConfigurationStore.java @@ -48,7 +48,6 @@ import org.springframework.context.ApplicationEventPublisher; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; /** @@ -269,8 +268,13 @@ select jsonb_build_object( Object::toString); } + /** + * Note that the transaction boundary has been set here instead of the service to avoid over + * complicating the "executeNow" service method which needs this change to be completed and + * visible at the end of this method. + */ @Override - @Transactional(propagation = Propagation.REQUIRES_NEW) + @Transactional public boolean tryExecuteNow(@Nonnull String jobId) { String sql = """ @@ -287,23 +291,6 @@ public boolean tryExecuteNow(@Nonnull String jobId) { return nativeSynchronizedQuery(sql).setParameter("id", jobId).executeUpdate() > 0; } - @Override - public boolean executeNow(@Nonnull String jobId) { - String sql = - """ - update jobconfiguration - set - schedulingtype = 'ONCE_ASAP', - cancel = false, - jobstatus = 'SCHEDULED' - where uid = :id - and enabled = true - and jobstatus != 'RUNNING' - and (schedulingtype != 'ONCE_ASAP' or lastfinished is null) - """; - return nativeSynchronizedQuery(sql).setParameter("id", jobId).executeUpdate() > 0; - } - @Override public boolean tryStart(@Nonnull String jobId) { // only flip from SCHEDULED to RUNNING if no other job of same type is RUNNING @@ -527,13 +514,13 @@ public boolean tryRevertNow(@Nonnull String jobId) { else schedulingtype end where jobstatus = 'RUNNING' and uid = :id + and now() > jobconfiguration.lastalive + interval '1 minute' """; return nativeSynchronizedQuery(sql).setParameter("id", jobId).executeUpdate() > 0; } private static String getSingleResultOrNull(NativeQuery query) { - List res = query.list(); - return res == null || res.isEmpty() ? null : (String) res.get(0); + return (String) query.getResultStream().findFirst().orElse(null); } @SuppressWarnings("unchecked") diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/JobScheduler.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/JobScheduler.java index 0b74023e93b5..484e15f20d29 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/JobScheduler.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/JobScheduler.java @@ -179,7 +179,7 @@ private void runContinuous(JobType type) { String jobId = jobIds.poll(); while (jobId != null) { JobConfiguration config = service.getJobConfiguration(jobId); - if (config != null && config.getJobStatus() == JobStatus.SCHEDULED) { + if (config != null && (config.getJobStatus() == JobStatus.SCHEDULED)) { Instant now = Instant.now().truncatedTo(ChronoUnit.SECONDS); Instant dueTime = dueTime(now, config); runDueJob(config, dueTime); @@ -225,7 +225,7 @@ private void runDueJob(JobConfiguration config, Instant start) { JobProgress progress = null; try { settingsProvider.clearCurrentSettings(); // ensure working with recent settings - AtomicLong lastAlive = new AtomicLong(currentTimeMillis()); + AtomicLong lastAlive = new AtomicLong(0L); progress = service.startRun(jobId, config.getExecutedBy(), () -> alive(jobId, lastAlive)); jobService.getJob(config.getJobType()).execute(config, progress); diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/RecordingJobProgress.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/RecordingJobProgress.java index c1f5eb712d25..d416b6133154 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/RecordingJobProgress.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/RecordingJobProgress.java @@ -214,12 +214,27 @@ private RuntimeException cancellationException(boolean failedPostCondition) { if (skipRecording && cause instanceof RuntimeException rex) throw rex; CancellationException ex = failedPostCondition - ? new CancellationException("Non-null post-condition failed") + ? new CancellationException(postConditionFailureMessage()) : new CancellationException(); ex.initCause(cause); return ex; } + private String postConditionFailureMessage() { + String msg = "Non-null post-condition failed after: "; + Process p = incompleteProcess.get(); + if (p != null) { + msg += p.getDescription(); + Stage s = incompleteStage.get(); + if (s != null) { + msg += "\n => " + s.getDescription(); + Item i = incompleteItem.get(); + if (i != null) msg += "\n => " + i.getDescription(); + } + } + return msg; + } + @Override public void completedProcess(String summary, Object... args) { observer.run(); @@ -279,6 +294,7 @@ public void startingStage( if (isCancelled()) throw cancellationException(false); skipCurrentStage.set(false); tracker.startingStage(description, workItems); + incompleteItem.remove(); Stage stage = addStageRecord(getOrAddLastIncompleteProcess(), description, workItems, onFailure); logInfo(stage, "", description); diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/TestJob.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/TestJob.java index 5dc879166cc2..9a6f4089af55 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/TestJob.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/TestJob.java @@ -87,6 +87,7 @@ public void execute(JobConfiguration conf, JobProgress progress) { simulateWorkForDuration(params.getItemDuration()); if (failAtThisStage && item == failAtItem) { progress.failedWorkItem(msg); + if (params.isFailWithPostCondition()) progress.nonNullStagePostCondition(null); } else { progress.completedWorkItem(null); } @@ -95,6 +96,7 @@ public void execute(JobConfiguration conf, JobProgress progress) { } else if (failAtThisStage) { if (params.isFailWithException()) throw new RuntimeException(msg); progress.failedStage(msg); + if (params.isFailWithPostCondition()) progress.nonNullStagePostCondition(null); } else { progress.completedStage(format("Stage %d complete", stage + 1)); } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/sms/command/hibernate/HibernateSMSCommandStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/sms/command/hibernate/HibernateSMSCommandStore.java index c37bb17b3861..a90d9cda23f6 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/sms/command/hibernate/HibernateSMSCommandStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/sms/command/hibernate/HibernateSMSCommandStore.java @@ -31,7 +31,9 @@ import jakarta.persistence.criteria.CriteriaBuilder; import java.util.Collection; import java.util.List; +import javax.annotation.Nonnull; import org.hibernate.query.Query; +import org.hisp.dhis.common.UID; import org.hisp.dhis.common.hibernate.HibernateIdentifiableObjectStore; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.dataset.DataSet; @@ -110,4 +112,18 @@ public List getCodesByDataElement(Collection dataElements) .setParameter("dataElements", dataElements) .list(); } + + @Override + public List getCodesByCategoryOptionCombo(@Nonnull Collection uids) { + if (uids.isEmpty()) return List.of(); + return getQuery( + """ + select distinct sms from SMSCode sms + join sms.optionId coc + where coc.uid in :uids + """, + SMSCode.class) + .setParameter("uids", UID.toValueList(uids)) + .list(); + } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/sms/command/hibernate/SMSCommandStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/sms/command/hibernate/SMSCommandStore.java index 0c295c37f74c..fa8d64ad9b97 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/sms/command/hibernate/SMSCommandStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/sms/command/hibernate/SMSCommandStore.java @@ -29,7 +29,9 @@ import java.util.Collection; import java.util.List; +import javax.annotation.Nonnull; import org.hisp.dhis.common.IdentifiableObjectStore; +import org.hisp.dhis.common.UID; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.dataset.DataSet; import org.hisp.dhis.sms.command.SMSCommand; @@ -44,4 +46,6 @@ public interface SMSCommandStore extends IdentifiableObjectStore { int countDataSetSmsCommands(DataSet dataSet); List getCodesByDataElement(Collection dataElements); + + List getCodesByCategoryOptionCombo(@Nonnull Collection uids); } 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 36217045a05e..d097174739d8 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 @@ -164,6 +164,7 @@ F_CATEGORY_OPTION_MERGE=Merge Category Options F_CATEGORY_COMBO_PUBLIC_ADD=Add/Update Public Category Combo F_CATEGORY_COMBO_PRIVATE_ADD=Add/Update Private Category Combo F_CATEGORY_COMBO_DELETE=Delete Category Combo +F_CATEGORY_OPTION_COMBO_MERGE=Merge Category Option Combos F_CATEGORY_OPTION_GROUP_PUBLIC_ADD=Add/Update Public Category Option Group F_CATEGORY_OPTION_GROUP_PRIVATE_ADD=Add/Update Private Category Option Group F_CATEGORY_OPTION_GROUP_DELETE=Delete Category Option Group @@ -2130,6 +2131,7 @@ data_integrity.user_roles_with_no_users.name=User roles with no users data_integrity.option_groups_empty.users=Option groups with no options data_integrity.push_analysis_no_recipients.name=Push analyses without recipients data_integrity.file_resources_no_icon.name=File resources that are missing an icon +data_integrity.users_with_all_authority.name=Users with the ALL authority # -- End Data Integrity Checks--------------------------------------------# diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/dataelement/hibernate/DataElementOperand.hbm.xml b/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/dataelement/hibernate/DataElementOperand.hbm.xml index dd29470a73f3..df166643a1ab 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/dataelement/hibernate/DataElementOperand.hbm.xml +++ b/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/dataelement/hibernate/DataElementOperand.hbm.xml @@ -18,6 +18,7 @@ + diff --git a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/copy/CopyServiceTest.java b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/copy/CopyServiceTest.java index 177e20ecadc5..aded62162396 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/copy/CopyServiceTest.java +++ b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/copy/CopyServiceTest.java @@ -131,7 +131,7 @@ void setup() { @Test void testCopyProgramFromUidWithValidProgram() throws NotFoundException, ForbiddenException { - OrganisationUnit orgUnit = createOrganisationUnit("New Org 1"); + OrganisationUnit orgUnit = createOrganisationUnit('A'); List originalEnrollments = List.of(createEnrollment(original, createTrackedEntity(orgUnit), orgUnit)); when(programService.getProgram(VALID_PROGRAM_UID)).thenReturn(original); @@ -382,7 +382,7 @@ Program createProgram() { Set.of(createProgramNotificationTemplate("not1", 20, ENROLLMENT, WEB_HOOK))); p.setOnlyEnrollOnce(true); p.setOpenDaysAfterCoEndDate(20); - p.setOrganisationUnits(Set.of(createOrganisationUnit("Org 1"))); + p.setOrganisationUnits(Set.of(createOrganisationUnit('A'))); p.setProgramAttributes(createProgramAttributes(p)); p.setProgramIndicators(createIndicators(p)); p.setProgramRuleVariables(Set.of(createProgramRuleVariable('v', p))); diff --git a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/datavalue/DataValueUtilTest.java b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/datavalue/DataValueUtilTest.java new file mode 100644 index 000000000000..5755ea39d847 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/datavalue/DataValueUtilTest.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.datavalue; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import javax.annotation.Nonnull; +import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.dataelement.DataElement; +import org.hisp.dhis.organisationunit.OrganisationUnit; +import org.hisp.dhis.period.Period; +import org.hisp.dhis.util.DateUtils; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class DataValueUtilTest { + + @Test + @DisplayName("Creates a new DataValue with the expected new category option combo") + void newDataValueWithCocTest() { + // given + DataValue originalDataValue = getDataValue(); + + // new category option combo to use + CategoryOptionCombo newCoc = new CategoryOptionCombo(); + newCoc.setName("New COC"); + + // when + DataValue newDataValue = + DataValueUtil.dataValueWithNewCatOptionCombo.apply(originalDataValue, newCoc); + + // then + assertWithNewValue(newDataValue, "New COC", "COC"); + } + + @Test + @DisplayName("Creates a new DataValue with the expected new attribute option combo") + void newDataValueWithAocTest() { + // given + DataValue originalDataValue = getDataValue(); + + // new attribute option combo to use + CategoryOptionCombo newAoc = new CategoryOptionCombo(); + newAoc.setName("New AOC"); + + // when + DataValue newDataValue = + DataValueUtil.dataValueWithNewAttrOptionCombo.apply(originalDataValue, newAoc); + + // then + assertWithNewValue(newDataValue, "New AOC", "AOC"); + } + + @Test + @DisplayName("Creates a new DataValue with the expected new data element") + void newDataValueWithDeTest() { + // given + DataValue originalDataValue = getDataValue(); + + // new attribute option combo to use + DataElement newDe = new DataElement("New DE"); + + // when + DataValue newDataValue = + DataValueUtil.dataValueWithNewDataElement.apply(originalDataValue, newDe); + + // then + assertWithNewValue(newDataValue, "New DE", "DE"); + } + + private void assertWithNewValue( + @Nonnull DataValue dv, @Nonnull String newValue, @Nonnull String property) { + if (property.equals("DE")) { + assertEquals(newValue, dv.getDataElement().getName()); + } else assertEquals("test DE", dv.getDataElement().getName()); + assertEquals("test Period", dv.getPeriod().getName()); + assertEquals("test Org Unit", dv.getSource().getName()); + if (property.equals("COC")) { + assertEquals(newValue, dv.getCategoryOptionCombo().getName()); + } else assertEquals("test COC", dv.getCategoryOptionCombo().getName()); + if (property.equals("AOC")) { + assertEquals(newValue, dv.getAttributeOptionCombo().getName()); + } else assertEquals("test AOC", dv.getAttributeOptionCombo().getName()); + assertEquals("test value", dv.getValue()); + assertEquals("test user", dv.getStoredBy()); + assertEquals(DateUtils.toMediumDate("2024-11-15"), dv.getLastUpdated()); + assertEquals("test comment", dv.getComment()); + assertTrue(dv.isFollowup()); + assertTrue(dv.isDeleted()); + assertEquals(DateUtils.toMediumDate("2024-07-25"), dv.getCreated()); + } + + private DataValue getDataValue() { + DataElement dataElement = new DataElement("test DE"); + Period period = new Period(); + period.setName("test Period"); + OrganisationUnit orgUnit = new OrganisationUnit("test Org Unit"); + CategoryOptionCombo coc = new CategoryOptionCombo(); + coc.setName("test COC"); + CategoryOptionCombo aoc = new CategoryOptionCombo(); + aoc.setName("test AOC"); + + DataValue dv = + DataValue.builder() + .dataElement(dataElement) + .period(period) + .source(orgUnit) + .categoryOptionCombo(coc) + .attributeOptionCombo(aoc) + .value("test value") + .storedBy("test user") + .lastUpdated(DateUtils.toMediumDate("2024-11-15")) + .comment("test comment") + .followup(true) + .deleted(true) + .build(); + dv.setCreated(DateUtils.toMediumDate("2024-07-25")); + return dv; + } +} diff --git a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/query/DefaultQueryServiceTest.java b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/query/DefaultQueryServiceTest.java index bb84e91d776f..6087b19e25d5 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/query/DefaultQueryServiceTest.java +++ b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/query/DefaultQueryServiceTest.java @@ -35,7 +35,6 @@ import java.util.ArrayList; import java.util.List; -import org.apache.commons.lang3.RandomStringUtils; import org.hisp.dhis.common.IdentifiableObject; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.query.planner.DefaultQueryPlanner; @@ -96,7 +95,7 @@ private List createOrgUnits(int size) { List result = new ArrayList<>(); for (int i = 0; i < size; i++) { - result.add(createOrganisationUnit(RandomStringUtils.randomAlphabetic(1))); + result.add(createOrganisationUnit((char) (i + 'A'))); } return result; } diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/dataset/JdbcCompleteDataSetRegistrationExchangeStore.java b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/dataset/JdbcCompleteDataSetRegistrationExchangeStore.java index ddc97d992bf4..c67d8b3242f2 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/dataset/JdbcCompleteDataSetRegistrationExchangeStore.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/dataset/JdbcCompleteDataSetRegistrationExchangeStore.java @@ -272,7 +272,7 @@ private static String createOrgUnitClause( clause += params.getOrganisationUnits().stream() - .map(o -> " ou.path LIKE '" + o.getPath() + "%' OR ") + .map(o -> " ou.path LIKE '" + o.getStoredPath() + "%' OR ") .collect(Collectors.joining()); return TextUtils.removeLastOr(clause) + " ) "; diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/datavalueset/SpringDataValueSetStore.java b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/datavalueset/SpringDataValueSetStore.java index 5f2213f268a4..594192de27d9 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/datavalueset/SpringDataValueSetStore.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/datavalueset/SpringDataValueSetStore.java @@ -289,7 +289,7 @@ private String getDataValueSql(DataExportParams params) { sql += "and ("; for (OrganisationUnit parent : params.getOrganisationUnits()) { - sql += "ou.path like '" + parent.getPath() + "%' or "; + sql += "ou.path like '" + parent.getStoredPath() + "%' or "; } sql = TextUtils.removeLastOr(sql) + ") "; diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/DefaultDataSetMetadataExportService.java b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/DefaultDataSetMetadataExportService.java index 35c5c26c27c9..8e60e6ff0b40 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/DefaultDataSetMetadataExportService.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/DefaultDataSetMetadataExportService.java @@ -72,7 +72,6 @@ import org.hisp.dhis.schema.descriptors.OptionSetSchemaDescriptor; import org.hisp.dhis.user.CurrentUserUtil; import org.hisp.dhis.user.UserDetails; -import org.hisp.dhis.user.UserService; import org.hisp.dhis.util.DateUtils; import org.springframework.stereotype.Service; @@ -136,8 +135,6 @@ public class DefaultDataSetMetadataExportService implements DataSetMetadataExpor private final ExpressionService expressionService; - private final UserService userService; - @Override public ObjectNode getDataSetMetadata() { UserDetails currentUserDetails = CurrentUserUtil.getCurrentUserDetails(); diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/DefaultMetadataImportService.java b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/DefaultMetadataImportService.java index 6f7d7ffaa0b8..5187a64e66e7 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/DefaultMetadataImportService.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/DefaultMetadataImportService.java @@ -56,6 +56,7 @@ import org.hisp.dhis.preheat.PreheatIdentifier; import org.hisp.dhis.preheat.PreheatMode; import org.hisp.dhis.scheduling.JobProgress; +import org.hisp.dhis.scheduling.RecordingJobProgress; import org.hisp.dhis.security.acl.AclService; import org.hisp.dhis.user.CurrentUserUtil; import org.hisp.dhis.user.User; @@ -80,7 +81,7 @@ public class DefaultMetadataImportService implements MetadataImportService { @Transactional public ImportReport importMetadata( @Nonnull MetadataImportParams params, @Nonnull MetadataObjects objects) { - return importMetadata(params, objects, JobProgress.noop()); + return importMetadata(params, objects, RecordingJobProgress.transitory()); } @Override diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/objectbundle/validation/MandatoryAttributesCheck.java b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/objectbundle/validation/MandatoryAttributesCheck.java index ff12ed78b8ba..c82747031c33 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/objectbundle/validation/MandatoryAttributesCheck.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/objectbundle/validation/MandatoryAttributesCheck.java @@ -27,15 +27,13 @@ */ package org.hisp.dhis.dxf2.metadata.objectbundle.validation; -import static java.util.Collections.emptyList; import static org.hisp.dhis.dxf2.metadata.objectbundle.validation.ValidationUtils.createObjectReport; -import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.Consumer; -import java.util.stream.Collectors; import org.hisp.dhis.attribute.Attribute; +import org.hisp.dhis.attribute.AttributeValues; import org.hisp.dhis.common.IdentifiableObject; import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundle; import org.hisp.dhis.feedback.ErrorCode; @@ -64,41 +62,34 @@ public void check( List objects = selectObjectsBasedOnImportStrategy(persistedObjects, nonPersistedObjects, importStrategy); - if (objects.isEmpty() || !schema.hasPersistedProperty("attributeValues")) { - return; - } + if (objects.isEmpty() || !schema.hasAttributeValues()) return; - for (T object : objects) { - List errorReports = checkMandatoryAttributes(klass, object, bundle.getPreheat()); + Preheat preheat = bundle.getPreheat(); + Set mandatoryAttributes = preheat.getMandatoryAttributes().get(klass); + if (mandatoryAttributes == null || mandatoryAttributes.isEmpty()) return; - if (!errorReports.isEmpty()) { - addReports.accept(createObjectReport(errorReports, object, bundle)); - ctx.markForRemoval(object); + for (T object : objects) { + if (object != null && !preheat.isDefault(object)) { + AttributeValues attributeValues = object.getAttributeValues(); + mandatoryAttributes.stream() + .filter(attrId -> !isDefined(attrId, attributeValues)) + .forEach( + attrId -> { + addReports.accept( + createObjectReport( + new ErrorReport(Attribute.class, ErrorCode.E4011, attrId) + .setMainId(attrId) + .setErrorProperty("value"), + object, + bundle)); + ctx.markForRemoval(object); + }); } } } - private List checkMandatoryAttributes( - Class klass, IdentifiableObject object, Preheat preheat) { - if (object == null - || preheat.isDefault(object) - || !preheat.getMandatoryAttributes().containsKey(klass)) { - return emptyList(); - } - - Set mandatoryAttributes = preheat.getMandatoryAttributes().get(klass); - if (mandatoryAttributes.isEmpty()) { - return emptyList(); - } - Set missingMandatoryAttributes = new HashSet<>(mandatoryAttributes); - missingMandatoryAttributes.removeAll(object.getAttributeValues().keys()); - - return missingMandatoryAttributes.stream() - .map( - att -> - new ErrorReport(Attribute.class, ErrorCode.E4011, att) - .setMainId(att) - .setErrorProperty("value")) - .collect(Collectors.toList()); + private static boolean isDefined(String attrId, AttributeValues values) { + String value = values.get(attrId); + return value != null && !value.isEmpty(); } } diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/dataset/DefaultCompleteDataSetRegistrationExchangeServiceTest.java b/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/dataset/DefaultCompleteDataSetRegistrationExchangeServiceTest.java index 4f9664d8b3b8..1411f486443c 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/dataset/DefaultCompleteDataSetRegistrationExchangeServiceTest.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/dataset/DefaultCompleteDataSetRegistrationExchangeServiceTest.java @@ -121,6 +121,7 @@ class DefaultCompleteDataSetRegistrationExchangeServiceTest { @Mock private BatchHandlerFactory batchHandlerFactory; @Mock private SystemSettingsProvider settingsProvider; + @Mock private SystemSettings settings; @Mock private CategoryService categoryService; @@ -158,7 +159,9 @@ class DefaultCompleteDataSetRegistrationExchangeServiceTest { @Mock private Environment environment; @Mock private AclService aclService; + @Mock private UserService userService; + private User user; private DefaultCompleteDataSetRegistrationExchangeService subject; @@ -299,7 +302,7 @@ void verifyUserHasNoWritePermissionOnCategoryOption() { void testValidateAssertMissingDataSet() { ExportParams params = new ExportParams() - .setOrganisationUnits(Sets.newHashSet(new OrganisationUnit())) + .setOrganisationUnits(Sets.newHashSet(createOrganisationUnit('A'))) .setPeriods(Sets.newHashSet(new Period())); assertIllegalQueryEx( diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/datavalueset/DataValueSetImportValidatorTest.java b/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/datavalueset/DataValueSetImportValidatorTest.java index 3cda0b79904a..90b7ba472ffd 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/datavalueset/DataValueSetImportValidatorTest.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/datavalueset/DataValueSetImportValidatorTest.java @@ -29,6 +29,7 @@ import static java.util.Collections.emptySet; import static java.util.Collections.singleton; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -878,11 +879,11 @@ private DataValueContextBuilder createDataValueContext(DataValue dataValue) { builder.period(p); } if (ouId != null) { - OrganisationUnit ou = new OrganisationUnit(); + OrganisationUnit ou = createOrganisationUnit('A'); ou.setUid(ouId); // we set the path here just for the tests. This is usually done by the persistence layer // but there is no interaction with that in these tests. - ou.setPath(ou.getPath()); + ou.updatePath(); builder.orgUnit(ou); } if (coId != null) { diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/attribute/GeoJsonAttributesCheckTest.java b/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/attribute/GeoJsonAttributesCheckTest.java index a7a57ed76697..54a62128fb6f 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/attribute/GeoJsonAttributesCheckTest.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/attribute/GeoJsonAttributesCheckTest.java @@ -27,6 +27,7 @@ */ package org.hisp.dhis.dxf2.metadata.attribute; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -71,8 +72,7 @@ class GeoJsonAttributesCheckTest { @BeforeEach public void setUpTest() { - organisationUnit = new OrganisationUnit(); - organisationUnit.setName("A"); + organisationUnit = createOrganisationUnit('A'); attribute = new Attribute(); attribute.setUid("geoJson"); attribute.setName("geoJson"); diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/attribute/MetadataAttributeCheckTest.java b/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/attribute/MetadataAttributeCheckTest.java index b07d0158b6ed..ea1140c4fb7e 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/attribute/MetadataAttributeCheckTest.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/attribute/MetadataAttributeCheckTest.java @@ -27,6 +27,7 @@ */ package org.hisp.dhis.dxf2.metadata.attribute; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -84,8 +85,7 @@ class MetadataAttributeCheckTest { @BeforeEach void setUpTest() { - organisationUnit = new OrganisationUnit(); - organisationUnit.setName("A"); + organisationUnit = createOrganisationUnit('A'); attribute = new Attribute(); attribute.setUid("attributeID"); attribute.setName("attributeA"); @@ -456,7 +456,7 @@ void testAttributeOrganisationUnit() { organisationUnit.addAttributeValue(attribute.getUid(), "OU-ID"); // OrganisationUnit exists - when(manager.get(OrganisationUnit.class, "OU-ID")).thenReturn(new OrganisationUnit()); + when(manager.get(OrganisationUnit.class, "OU-ID")).thenReturn(createOrganisationUnit('A')); List objectReportList = new ArrayList<>(); diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/objectbundle/ObjectBundleHooksTest.java b/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/objectbundle/ObjectBundleHooksTest.java index ecf8d84e51b7..608e403d83c3 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/objectbundle/ObjectBundleHooksTest.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/objectbundle/ObjectBundleHooksTest.java @@ -30,6 +30,7 @@ import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -67,20 +68,20 @@ class ObjectBundleHooksTest { @Test void testMatchingClassBoundIsIncluded() { - assertHasHooksOfType(new OrganisationUnit(), OrganisationUnitObjectBundleHook.class); + assertHasHooksOfType(createOrganisationUnit('A'), OrganisationUnitObjectBundleHook.class); assertHasHooksOfType(new User(), UserObjectBundleHook.class); } @Test void testNonMatchingClassBoundIsNotIncluded() { - assertHasNotHooksOfType(new OrganisationUnit(), UserObjectBundleHook.class); + assertHasNotHooksOfType(createOrganisationUnit('A'), UserObjectBundleHook.class); assertHasNotHooksOfType(new User(), OrganisationUnitObjectBundleHook.class); } @Test void testMatchingInterfaceBoundIsIncluded() { assertHasHooksOfType( - new OrganisationUnit(), + createOrganisationUnit('A'), IdentifiableObjectBundleHook.class, VersionedObjectObjectBundleHook.class); assertHasHooksOfType( @@ -94,7 +95,7 @@ void testMatchingInterfaceBoundIsIncluded() { @Test void testNonMatchingInterfaceBoundIsNotIncluded() { - assertHasNotHooksOfType(new OrganisationUnit(), AnalyticalObjectObjectBundleHook.class); + assertHasNotHooksOfType(createOrganisationUnit('A'), AnalyticalObjectObjectBundleHook.class); assertHasNotHooksOfType(new User(), AnalyticalObjectObjectBundleHook.class); } diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/objectbundle/hooks/ProgramObjectBundleHookTest.java b/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/objectbundle/hooks/ProgramObjectBundleHookTest.java index 5d4e36618955..edf7d86c6467 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/objectbundle/hooks/ProgramObjectBundleHookTest.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/objectbundle/hooks/ProgramObjectBundleHookTest.java @@ -30,6 +30,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; import static org.hisp.dhis.test.TestBase.createProgram; import static org.hisp.dhis.test.TestBase.createProgramStage; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -45,7 +46,6 @@ import org.hisp.dhis.common.IdentifiableObjectManager; import org.hisp.dhis.feedback.ErrorCode; import org.hisp.dhis.feedback.ErrorReport; -import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.organisationunit.OrganisationUnitService; import org.hisp.dhis.program.Enrollment; import org.hisp.dhis.program.EnrollmentStatus; @@ -112,7 +112,7 @@ void verifyMissingBundleIsIgnored() { @Test void verifyProgramInstanceIsSavedForEventProgram() { when(organisationUnitService.getRootOrganisationUnits()) - .thenReturn(List.of(new OrganisationUnit())); + .thenReturn(List.of(createOrganisationUnit('A'))); ArgumentCaptor argument = ArgumentCaptor.forClass(Enrollment.class); programA.setProgramType(ProgramType.WITHOUT_REGISTRATION); diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/objectbundle/hooks/ProgramStageWorkingListObjectBundleHookTest.java b/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/objectbundle/hooks/ProgramStageWorkingListObjectBundleHookTest.java index 774b8a513ef3..26f098305938 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/objectbundle/hooks/ProgramStageWorkingListObjectBundleHookTest.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/objectbundle/hooks/ProgramStageWorkingListObjectBundleHookTest.java @@ -35,6 +35,7 @@ import static org.hisp.dhis.feedback.ErrorCode.E4067; import static org.hisp.dhis.feedback.ErrorCode.E4068; import static org.hisp.dhis.feedback.ErrorCode.E7500; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; import static org.hisp.dhis.test.utils.Assertions.assertErrorReport; import static org.hisp.dhis.test.utils.Assertions.assertIsEmpty; import static org.mockito.ArgumentMatchers.anyString; @@ -50,7 +51,6 @@ import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundle; import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundleParams; import org.hisp.dhis.feedback.ErrorReport; -import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.organisationunit.OrganisationUnitService; import org.hisp.dhis.preheat.Preheat; import org.hisp.dhis.programstagefilter.DateFilterPeriod; @@ -136,7 +136,7 @@ void shouldReturnNoErrorsWhenQueryCriteriaSuppliedIsValid() { .build(); when(organisationUnitService.getOrganisationUnit(anyString())) - .thenReturn(new OrganisationUnit()); + .thenReturn(createOrganisationUnit('A')); when(dataElementService.getDataElement(anyString())).thenReturn(new DataElement()); when(attributeService.getTrackedEntityAttribute(anyString())) .thenReturn(new TrackedEntityAttribute()); diff --git a/dhis-2/dhis-services/dhis-service-metadata-workflow/src/main/java/org/hisp/dhis/metadata/HibernateMetadataProposalStore.java b/dhis-2/dhis-services/dhis-service-metadata-workflow/src/main/java/org/hisp/dhis/metadata/HibernateMetadataProposalStore.java index 90381eaaddeb..bdaee738bbc4 100644 --- a/dhis-2/dhis-services/dhis-service-metadata-workflow/src/main/java/org/hisp/dhis/metadata/HibernateMetadataProposalStore.java +++ b/dhis-2/dhis-services/dhis-service-metadata-workflow/src/main/java/org/hisp/dhis/metadata/HibernateMetadataProposalStore.java @@ -49,7 +49,9 @@ public MetadataProposal getByUid(String uid) { return getSession() .createQuery("from MetadataProposal p where p.uid = :uid", MetadataProposal.class) .setParameter("uid", uid) - .getSingleResult(); + .getResultStream() + .findFirst() + .orElse(null); } @Override diff --git a/dhis-2/dhis-services/dhis-service-reporting/src/main/java/org/hisp/dhis/predictor/PredictionDataValueFetcher.java b/dhis-2/dhis-services/dhis-service-reporting/src/main/java/org/hisp/dhis/predictor/PredictionDataValueFetcher.java index 32411bd17e9c..0d433ae22556 100644 --- a/dhis-2/dhis-services/dhis-service-reporting/src/main/java/org/hisp/dhis/predictor/PredictionDataValueFetcher.java +++ b/dhis-2/dhis-services/dhis-service-reporting/src/main/java/org/hisp/dhis/predictor/PredictionDataValueFetcher.java @@ -207,7 +207,8 @@ public void init( this.outputDataElementOperand = outputDataElementOperand; orgUnitLookup = - orgUnits.stream().collect(Collectors.toMap(OrganisationUnit::getPath, Function.identity())); + orgUnits.stream() + .collect(Collectors.toMap(OrganisationUnit::getStoredPath, Function.identity())); dataElementLookup = dataElements.stream() .collect(toMap(DataElement::getId, Function.identity(), (de1, de2) -> de1)); @@ -359,7 +360,7 @@ private PredictionData getPredictionData( addValueToMap(dv, map); } - if (ddv.getSourcePath().equals(dv.getSource().getPath()) + if (ddv.getSourcePath().equals(dv.getSource().getStoredPath()) && ddv.getDataElementId() == outputDataElementOperand.getDataElement().getId() && (outputDataElementOperand.getCategoryOptionCombo() == null || ddv.getCategoryOptionComboId() diff --git a/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/predictor/PredictionAnalyticsDataFetcherTest.java b/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/predictor/PredictionAnalyticsDataFetcherTest.java index 5404db0b3a52..eb26204fb471 100644 --- a/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/predictor/PredictionAnalyticsDataFetcherTest.java +++ b/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/predictor/PredictionAnalyticsDataFetcherTest.java @@ -106,8 +106,8 @@ void initTest() { periods = Sets.newHashSet(periodA, periodB); - orgUnitA = createOrganisationUnit("A"); - orgUnitB = createOrganisationUnit("B"); + orgUnitA = createOrganisationUnit('A'); + orgUnitB = createOrganisationUnit('B'); orgUnitA.setUid("orgUnitAuid"); orgUnitB.setUid("orgUnitBuid"); diff --git a/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/predictor/PredictionDataConsolidatorTest.java b/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/predictor/PredictionDataConsolidatorTest.java index bb3db588a2e6..010c677236cb 100644 --- a/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/predictor/PredictionDataConsolidatorTest.java +++ b/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/predictor/PredictionDataConsolidatorTest.java @@ -231,13 +231,13 @@ void initTest() { // -- C ------ F // -- D ------ G - orgUnitA = createOrganisationUnit("A"); - orgUnitB = createOrganisationUnit("B"); - orgUnitC = createOrganisationUnit("C"); - orgUnitD = createOrganisationUnit("D"); - orgUnitE = createOrganisationUnit("E", orgUnitB); - orgUnitF = createOrganisationUnit("F", orgUnitC); - orgUnitG = createOrganisationUnit("G", orgUnitD); + orgUnitA = createOrganisationUnit('A'); + orgUnitB = createOrganisationUnit('B'); + orgUnitC = createOrganisationUnit('C'); + orgUnitD = createOrganisationUnit('D'); + orgUnitE = createOrganisationUnit('E', orgUnitB); + orgUnitF = createOrganisationUnit('F', orgUnitC); + orgUnitG = createOrganisationUnit('G', orgUnitD); orgUnitA.setId(20); orgUnitB.setId(21); diff --git a/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/predictor/PredictionDataValueFetcherTest.java b/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/predictor/PredictionDataValueFetcherTest.java index 4b847c628d33..1fe83aff83c4 100644 --- a/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/predictor/PredictionDataValueFetcherTest.java +++ b/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/predictor/PredictionDataValueFetcherTest.java @@ -248,13 +248,13 @@ void initTest() { // -- C ------ F // -- D ------ G - orgUnitA = createOrganisationUnit("A"); - orgUnitB = createOrganisationUnit("B"); - orgUnitC = createOrganisationUnit("C"); - orgUnitD = createOrganisationUnit("D"); - orgUnitE = createOrganisationUnit("E", orgUnitB); - orgUnitF = createOrganisationUnit("F", orgUnitC); - orgUnitG = createOrganisationUnit("G", orgUnitD); + orgUnitA = createOrganisationUnit('A'); + orgUnitB = createOrganisationUnit('B'); + orgUnitC = createOrganisationUnit('C'); + orgUnitD = createOrganisationUnit('D'); + orgUnitE = createOrganisationUnit('E', orgUnitB); + orgUnitF = createOrganisationUnit('F', orgUnitC); + orgUnitG = createOrganisationUnit('G', orgUnitD); orgUnitA.setId(20); orgUnitB.setId(21); diff --git a/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/predictor/PredictionWriterTest.java b/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/predictor/PredictionWriterTest.java index 4c62cee80b16..2a546a204d76 100644 --- a/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/predictor/PredictionWriterTest.java +++ b/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/predictor/PredictionWriterTest.java @@ -28,7 +28,7 @@ package org.hisp.dhis.predictor; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -123,7 +123,7 @@ public void initTest() { cocA.setId(++id); cocB.setId(++id); - orgUnitA = createOrganisationUnit("A"); + orgUnitA = createOrganisationUnit('A'); orgUnitA.setId(++id); diff --git a/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/visualization/VisualizationGridServiceTest.java b/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/visualization/VisualizationGridServiceTest.java index 4aab0ba6ec60..e7e3b0959938 100644 --- a/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/visualization/VisualizationGridServiceTest.java +++ b/dhis-2/dhis-services/dhis-service-reporting/src/test/java/org/hisp/dhis/visualization/VisualizationGridServiceTest.java @@ -31,6 +31,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.hasSize; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -90,7 +91,7 @@ void getVisualizationGridByUserWhenItHasOrganisationUnitLevels() { final String anyOrganisationUnitUid = "ouiRzW5e"; final User userStub = userStub(); final List orgUnitLevels = asList(1, 2); - final List orgUnits = asList(new OrganisationUnit()); + final List orgUnits = asList(createOrganisationUnit('A')); final Map valueMap = valueMapStub(); final Visualization visualizationStub = visualizationStub("abc123xy"); @@ -124,7 +125,7 @@ void getVisualizationGridByUserWhenItHasItemOrganisationUnitGroups() { final Date anyRelativePeriodDate = new Date(); final String anyOrganisationUnitUid = "ouiRzW5e"; final User userStub = userStub(); - final List orgUnits = asList(new OrganisationUnit()); + final List orgUnits = asList(createOrganisationUnit('A')); final List orgUnitGroups = asList(new OrganisationUnitGroup()); final Map valueMap = valueMapStub(); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/acl/DefaultTrackerAccessManager.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/acl/DefaultTrackerAccessManager.java index 9a9a18a5a491..d5d19148dd31 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/acl/DefaultTrackerAccessManager.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/acl/DefaultTrackerAccessManager.java @@ -282,7 +282,7 @@ public List canCreate( Program program = enrollment.getProgram(); List errors = new ArrayList<>(); OrganisationUnit ou = enrollment.getOrganisationUnit(); - if (ou != null && !user.isInUserHierarchy(ou.getPath())) { + if (ou != null && !user.isInUserHierarchy(ou.getStoredPath())) { errors.add("User has no create access to organisation unit: " + ou.getUid()); } @@ -336,7 +336,7 @@ public List canUpdate( } else { OrganisationUnit ou = enrollment.getOrganisationUnit(); - if (ou != null && !user.isInUserHierarchy(ou.getPath())) { + if (ou != null && !user.isInUserHierarchy(ou.getStoredPath())) { errors.add("User has no write access to organisation unit: " + ou.getUid()); } } @@ -371,7 +371,7 @@ public List canDelete( } } else { OrganisationUnit ou = enrollment.getOrganisationUnit(); - if (ou != null && !user.isInUserHierarchy(ou.getPath())) { + if (ou != null && !user.isInUserHierarchy(ou.getStoredPath())) { errors.add("User has no delete access to organisation unit: " + ou.getUid()); } } @@ -447,8 +447,8 @@ public List canCreate( if (ou != null) { boolean isInHierarchy = event.isCreatableInSearchScope() - ? user.isInUserEffectiveSearchOrgUnitHierarchy(ou.getPath()) - : user.isInUserHierarchy(ou.getPath()); + ? user.isInUserEffectiveSearchOrgUnitHierarchy(ou.getStoredPath()) + : user.isInUserHierarchy(ou.getStoredPath()); if (!isInHierarchy) { errors.add("User has no create access to organisation unit: " + ou.getUid()); @@ -493,7 +493,7 @@ public List canUpdate( canManageWithRegistration(errors, user, programStage, program); OrganisationUnit ou = event.getOrganisationUnit(); - if (ou != null && !user.isInUserEffectiveSearchOrgUnitHierarchy(ou.getPath())) { + if (ou != null && !user.isInUserEffectiveSearchOrgUnitHierarchy(ou.getStoredPath())) { errors.add("User has no update access to organisation unit: " + ou.getUid()); } @@ -528,7 +528,7 @@ public List canDelete( List errors = new ArrayList<>(); if (program.isWithoutRegistration()) { OrganisationUnit ou = event.getOrganisationUnit(); - if (ou != null && !user.isInUserHierarchy(ou.getPath())) { + if (ou != null && !user.isInUserHierarchy(ou.getStoredPath())) { errors.add("User has no delete access to organisation unit: " + ou.getUid()); } @@ -748,10 +748,10 @@ public boolean canAccess(@Nonnull UserDetails user, Program program, Organisatio } if (program != null && (program.isClosed() || program.isProtected())) { - return user.isInUserHierarchy(orgUnit.getPath()); + return user.isInUserHierarchy(orgUnit.getStoredPath()); } - return user.isInUserEffectiveSearchOrgUnitHierarchy(orgUnit.getPath()); + return user.isInUserEffectiveSearchOrgUnitHierarchy(orgUnit.getStoredPath()); } @Override diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/acl/DefaultTrackerOwnershipManager.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/acl/DefaultTrackerOwnershipManager.java index e7c7ebb2a8d1..068d619ce5d0 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/acl/DefaultTrackerOwnershipManager.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/acl/DefaultTrackerOwnershipManager.java @@ -226,7 +226,7 @@ public boolean hasAccess(UserDetails user, TrackedEntity trackedEntity, Program OrganisationUnit ou = getOwner(trackedEntity, program, trackedEntity::getOrganisationUnit); - final String orgUnitPath = ou.getPath(); + final String orgUnitPath = ou.getStoredPath(); return switch (program.getAccessLevel()) { case OPEN, AUDITED -> user.isInUserEffectiveSearchOrgUnitHierarchy(orgUnitPath); case PROTECTED -> @@ -243,7 +243,7 @@ public boolean hasAccess( return true; } - final String orgUnitPath = owningOrgUnit.getPath(); + final String orgUnitPath = owningOrgUnit.getStoredPath(); return switch (program.getAccessLevel()) { case OPEN, AUDITED -> user.isInUserEffectiveSearchOrgUnitHierarchy(orgUnitPath); case PROTECTED -> @@ -267,7 +267,7 @@ public boolean canSkipOwnershipCheck(UserDetails user, ProgramType programType) public boolean isOwnerInUserSearchScope( UserDetails user, TrackedEntity trackedEntity, Program program) { return user.isInUserSearchHierarchy( - getOwner(trackedEntity, program, trackedEntity::getOrganisationUnit).getPath()); + getOwner(trackedEntity, program, trackedEntity::getOrganisationUnit).getStoredPath()); } // ------------------------------------------------------------------------- diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/OperationsParamsValidator.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/OperationsParamsValidator.java index 26ef82f9ee46..03ebb2305160 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/OperationsParamsValidator.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/OperationsParamsValidator.java @@ -242,7 +242,8 @@ public Set validateOrgUnits(Set orgUnitIds, UserDetails u throw new BadRequestException("Organisation unit does not exist: " + orgUnitUid); } - if (!user.isSuper() && !user.isInUserEffectiveSearchOrgUnitHierarchy(orgUnit.getPath())) { + if (!user.isSuper() + && !user.isInUserEffectiveSearchOrgUnitHierarchy(orgUnit.getStoredPath())) { throw new ForbiddenException( "Organisation unit is not part of the search scope: " + orgUnit.getUid()); } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/enrollment/HibernateEnrollmentStore.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/enrollment/HibernateEnrollmentStore.java index d67e20076ef6..7a7612b0f459 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/enrollment/HibernateEnrollmentStore.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/enrollment/HibernateEnrollmentStore.java @@ -237,7 +237,7 @@ private String getDescendantsQuery(Set organisationUnits) { ouClause .append(orHlp.or()) .append("en.organisationUnit.path LIKE '") - .append(organisationUnit.getPath()) + .append(organisationUnit.getStoredPath()) .append("%'"); } @@ -252,7 +252,7 @@ private String getChildrenQuery(SqlHelper hlp, Set organisatio orgUnits .append(hlp.or()) .append("en.organisationUnit.path LIKE '") - .append(organisationUnit.getPath()) + .append(organisationUnit.getStoredPath()) .append("%'") .append(" AND (en.organisationUnit.hierarchyLevel = ") .append(organisationUnit.getHierarchyLevel()) diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/EventOperationParamsMapper.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/EventOperationParamsMapper.java index 76da7f7c71b8..de9898b5f70f 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/EventOperationParamsMapper.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/EventOperationParamsMapper.java @@ -175,7 +175,7 @@ private OrganisationUnit validateRequestedOrgUnit(String orgUnitUid, UserDetails "Organisation unit is specified but does not exist: " + orgUnitUid); } - if (!user.isInUserEffectiveSearchOrgUnitHierarchy(orgUnit.getPath())) { + if (!user.isInUserEffectiveSearchOrgUnitHierarchy(orgUnit.getStoredPath())) { throw new ForbiddenException( "Organisation unit is not part of your search scope: " + orgUnit.getUid()); } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/JdbcEventStore.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/JdbcEventStore.java index 2b5d9f11e8b6..a5518be292f0 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/JdbcEventStore.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/JdbcEventStore.java @@ -1195,7 +1195,7 @@ private String createAccessibleSql( private String createDescendantsSql( User user, EventQueryParams params, MapSqlParameterSource mapSqlParameterSource) { - mapSqlParameterSource.addValue(COLUMN_ORG_UNIT_PATH, params.getOrgUnit().getPath()); + mapSqlParameterSource.addValue(COLUMN_ORG_UNIT_PATH, params.getOrgUnit().getStoredPath()); if (isProgramRestricted(params.getProgram())) { return createCaptureScopeQuery( @@ -1208,7 +1208,7 @@ private String createDescendantsSql( private String createChildrenSql( User user, EventQueryParams params, MapSqlParameterSource mapSqlParameterSource) { - mapSqlParameterSource.addValue(COLUMN_ORG_UNIT_PATH, params.getOrgUnit().getPath()); + mapSqlParameterSource.addValue(COLUMN_ORG_UNIT_PATH, params.getOrgUnit().getStoredPath()); String customChildrenQuery = " and (ou.hierarchylevel = " @@ -1231,7 +1231,7 @@ private String createChildrenSql( private String createSelectedSql( User user, EventQueryParams params, MapSqlParameterSource mapSqlParameterSource) { - mapSqlParameterSource.addValue(COLUMN_ORG_UNIT_PATH, params.getOrgUnit().getPath()); + mapSqlParameterSource.addValue(COLUMN_ORG_UNIT_PATH, params.getOrgUnit().getStoredPath()); String orgUnitPathEqualsMatchQuery = " ou.path = :" diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/DefaultTrackedEntityService.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/DefaultTrackedEntityService.java index 7932f4a7510e..c9f500a60318 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/DefaultTrackedEntityService.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/DefaultTrackedEntityService.java @@ -33,7 +33,6 @@ import static org.hisp.dhis.user.CurrentUserUtil.getCurrentUsername; import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -211,21 +210,14 @@ private static TrackedEntityAttribute getAttribute( () -> new NotFoundException(TrackedEntityAttribute.class, attribute.getValue())); } + @Nonnull @Override public TrackedEntity getTrackedEntity(@Nonnull UID uid) throws NotFoundException, ForbiddenException { - UserDetails currentUser = getCurrentUserDetails(); - TrackedEntity trackedEntity = - mapTrackedEntity( - getTrackedEntity(uid, currentUser), - TrackedEntityParams.FALSE, - currentUser, - null, - false); - mapTrackedEntityTypeAttributes(trackedEntity); - return trackedEntity; + return getTrackedEntity(uid, null, TrackedEntityParams.FALSE); } + @Nonnull @Override public TrackedEntity getTrackedEntity( @Nonnull UID trackedEntityUid, @@ -233,7 +225,6 @@ public TrackedEntity getTrackedEntity( @Nonnull TrackedEntityParams params) throws NotFoundException, ForbiddenException { Program program = null; - if (programIdentifier != null) { program = programService.getProgram(programIdentifier.getValue()); if (program == null) { @@ -241,70 +232,20 @@ public TrackedEntity getTrackedEntity( } } - TrackedEntity trackedEntity; - if (program != null) { - trackedEntity = getTrackedEntity(trackedEntityUid.getValue(), program, params); - - if (params.isIncludeProgramOwners()) { - Set filteredProgramOwners = - trackedEntity.getProgramOwners().stream() - .filter(te -> te.getProgram().getUid().equals(programIdentifier.getValue())) - .collect(Collectors.toSet()); - trackedEntity.setProgramOwners(filteredProgramOwners); - } - } else { - UserDetails userDetails = getCurrentUserDetails(); - - trackedEntity = - mapTrackedEntity( - getTrackedEntity(trackedEntityUid, userDetails), params, userDetails, null, false); - - mapTrackedEntityTypeAttributes(trackedEntity); - } + UserDetails userDetails = getCurrentUserDetails(); + TrackedEntity trackedEntity = getTrackedEntity(trackedEntityUid, userDetails, program); + trackedEntity = mapTrackedEntity(trackedEntity, params, userDetails, program, false); return trackedEntity; } /** - * Gets a tracked entity based on the program and org unit ownership + * Gets a tracked entity based on the program and org unit ownership. * * @return the TE object if found and accessible by the current user * @throws NotFoundException if uid does not exist * @throws ForbiddenException if TE owner is not in user's scope or not enough sharing access */ - private TrackedEntity getTrackedEntity(String uid, Program program, TrackedEntityParams params) - throws NotFoundException, ForbiddenException { - TrackedEntity trackedEntity = trackedEntityStore.getByUid(uid); - trackedEntityAuditService.addTrackedEntityAudit(trackedEntity, getCurrentUsername(), READ); - if (trackedEntity == null) { - throw new NotFoundException(TrackedEntity.class, uid); - } - - UserDetails userDetails = getCurrentUserDetails(); - List errors = - trackerAccessManager.canReadProgramAndTrackedEntityType( - userDetails, trackedEntity, program); - if (!errors.isEmpty()) { - throw new ForbiddenException(errors.toString()); - } - - String error = - trackerAccessManager.canAccessProgramOwner(userDetails, trackedEntity, program, false); - if (error != null) { - throw new ForbiddenException(error); - } - - return mapTrackedEntity(trackedEntity, params, userDetails, program, false); - } - - /** - * Gets the requested tracked entity if the user owns at least one TE/program pair, or has access - * to the TE registering org unit, in case it doesn't own any. - * - * @return the TE object if found and accessible by the user - * @throws NotFoundException if TE does not exist - * @throws ForbiddenException if TE is not accessible - */ - private TrackedEntity getTrackedEntity(UID uid, UserDetails userDetails) + private TrackedEntity getTrackedEntity(UID uid, UserDetails userDetails, Program program) throws NotFoundException, ForbiddenException { TrackedEntity trackedEntity = trackedEntityStore.getByUid(uid.getValue()); trackedEntityAuditService.addTrackedEntityAudit(trackedEntity, getCurrentUsername(), READ); @@ -312,28 +253,28 @@ private TrackedEntity getTrackedEntity(UID uid, UserDetails userDetails) throw new NotFoundException(TrackedEntity.class, uid); } - if (!trackerAccessManager.canRead(userDetails, trackedEntity).isEmpty()) { - throw new ForbiddenException(TrackedEntity.class, uid); + if (program != null) { + List errors = + trackerAccessManager.canReadProgramAndTrackedEntityType( + userDetails, trackedEntity, program); + if (!errors.isEmpty()) { + throw new ForbiddenException(errors.toString()); + } + + String error = + trackerAccessManager.canAccessProgramOwner(userDetails, trackedEntity, program, false); + if (error != null) { + throw new ForbiddenException(error); + } + } else { + if (!trackerAccessManager.canRead(userDetails, trackedEntity).isEmpty()) { + throw new ForbiddenException(TrackedEntity.class, uid); + } } return trackedEntity; } - private void mapTrackedEntityTypeAttributes(TrackedEntity trackedEntity) { - TrackedEntityType trackedEntityType = trackedEntity.getTrackedEntityType(); - if (trackedEntityType != null) { - Set tetAttributes = - trackedEntityType.getTrackedEntityAttributes().stream() - .map(TrackedEntityAttribute::getUid) - .collect(Collectors.toSet()); - Set tetAttributeValues = - trackedEntity.getTrackedEntityAttributeValues().stream() - .filter(att -> tetAttributes.contains(att.getAttribute().getUid())) - .collect(Collectors.toCollection(LinkedHashSet::new)); - trackedEntity.setTrackedEntityAttributeValues(tetAttributeValues); - } - } - private TrackedEntity mapTrackedEntity( TrackedEntity trackedEntity, TrackedEntityParams params, @@ -364,7 +305,7 @@ private TrackedEntity mapTrackedEntity( result.setEnrollments(getEnrollments(trackedEntity, user, includeDeleted, program)); } if (params.isIncludeProgramOwners()) { - result.setProgramOwners(trackedEntity.getProgramOwners()); + result.setProgramOwners(getTrackedEntityProgramOwners(trackedEntity, program)); } result.setTrackedEntityAttributeValues(getTrackedEntityAttributeValues(trackedEntity, program)); @@ -408,22 +349,31 @@ private Set getEnrollments( .collect(Collectors.toSet()); } + private static Set getTrackedEntityProgramOwners( + TrackedEntity trackedEntity, Program program) { + if (program == null) { + return trackedEntity.getProgramOwners(); + } + + return trackedEntity.getProgramOwners().stream() + .filter(te -> te.getProgram().getUid().equals(program.getUid())) + .collect(Collectors.toSet()); + } + private Set getTrackedEntityAttributeValues( TrackedEntity trackedEntity, Program program) { - Set readableAttributes = + Set teas = // tracked entity type attributes trackedEntity.getTrackedEntityType().getTrackedEntityAttributes().stream() .map(IdentifiableObject::getUid) .collect(Collectors.toSet()); - - if (program != null) { - readableAttributes.addAll( + if (program != null) { // add program tracked entity attributes + teas.addAll( program.getTrackedEntityAttributes().stream() .map(IdentifiableObject::getUid) .collect(Collectors.toSet())); } - return trackedEntity.getTrackedEntityAttributeValues().stream() - .filter(av -> readableAttributes.contains(av.getAttribute().getUid())) + .filter(av -> teas.contains(av.getAttribute().getUid())) .collect(Collectors.toSet()); } @@ -488,6 +438,7 @@ private RelationshipItem getTrackedEntityInRelationshipItem( return relationshipItem; } + @Nonnull @Override public List getTrackedEntities( @Nonnull TrackedEntityOperationParams operationParams) diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/HibernateTrackedEntityStore.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/HibernateTrackedEntityStore.java index 948d63004212..a98de521046f 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/HibernateTrackedEntityStore.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/HibernateTrackedEntityStore.java @@ -685,7 +685,7 @@ private String getDescendantsQuery(TrackedEntityQueryParams params) { orgUnits .append(orHlp.or()) .append("OU.path LIKE '") - .append(organisationUnit.getPath()) + .append(organisationUnit.getStoredPath()) .append("%'"); } @@ -704,7 +704,7 @@ private String getChildrenQuery(TrackedEntityQueryParams params) { orgUnits .append(orHlp.or()) .append(" OU.path LIKE '") - .append(organisationUnit.getPath()) + .append(organisationUnit.getStoredPath()) .append("%'") .append(" AND (ou.hierarchylevel = ") .append(organisationUnit.getHierarchyLevel()) diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/TrackedEntityService.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/TrackedEntityService.java index 874886c46549..cc1ed7660b14 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/TrackedEntityService.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/trackedentity/TrackedEntityService.java @@ -57,16 +57,18 @@ FileResourceStream getFileResourceImage( * relationships are not included. Use {@link #getTrackedEntity(UID, UID, TrackedEntityParams)} * instead to also get the relationships, enrollments and program attributes. */ - TrackedEntity getTrackedEntity(UID uid) + @Nonnull + TrackedEntity getTrackedEntity(@Nonnull UID uid) throws NotFoundException, ForbiddenException, BadRequestException; /** * Get the tracked entity matching given {@code UID} under the privileges of the currently - * authenticated user. If @param programIdentifier is defined, program attributes for such program - * are included, otherwise only TETAs are included. It will include enrollments, relationships, - * attributes and ownerships as defined in @param params + * authenticated user. If {@code program} is defined, program attributes for such program are + * included, otherwise only TETAs are included. It will include enrollments, relationships, + * attributes and ownerships as defined in {@code params}. */ - TrackedEntity getTrackedEntity(UID uid, UID programIdentifier, TrackedEntityParams params) + @Nonnull + TrackedEntity getTrackedEntity(@Nonnull UID uid, UID program, @Nonnull TrackedEntityParams params) throws NotFoundException, ForbiddenException, BadRequestException; /** Get all tracked entities matching given params. */ diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/SecurityOwnershipValidator.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/SecurityOwnershipValidator.java index 29934dfa8eba..bccfd613cf23 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/SecurityOwnershipValidator.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/SecurityOwnershipValidator.java @@ -32,7 +32,6 @@ import java.util.Map; import javax.annotation.Nonnull; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.hisp.dhis.common.UID; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.program.Program; @@ -59,7 +58,6 @@ @Component( "org.hisp.dhis.tracker.imports.validation.validator.enrollment.SecurityOwnershipValidator") @RequiredArgsConstructor -@Slf4j class SecurityOwnershipValidator implements Validator { @Nonnull private final AclService aclService; @@ -170,7 +168,7 @@ public boolean needsToRun(TrackerImportStrategy strategy) { private void checkOrgUnitInCaptureScope( Reporter reporter, TrackerDto dto, OrganisationUnit orgUnit, UserDetails user) { - if (!user.isInUserHierarchy(orgUnit.getPath())) { + if (!user.isInUserHierarchy(orgUnit.getStoredPath())) { reporter.addError(dto, ValidationCode.E1000, user, orgUnit); } } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/event/SecurityOwnershipValidator.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/event/SecurityOwnershipValidator.java index 69e94d859633..60ee5c6e03d7 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/event/SecurityOwnershipValidator.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/event/SecurityOwnershipValidator.java @@ -33,7 +33,6 @@ import java.util.Map; import javax.annotation.Nonnull; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; import org.hisp.dhis.category.CategoryOption; import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.common.UID; @@ -63,7 +62,6 @@ */ @Component("org.hisp.dhis.tracker.imports.validation.validator.event.SecurityOwnershipValidator") @RequiredArgsConstructor -@Slf4j class SecurityOwnershipValidator implements Validator { @Nonnull private final AclService aclService; @@ -295,9 +293,10 @@ private void checkEventOrgUnitWriteAccess( OrganisationUnit eventOrgUnit, boolean isCreatableInSearchScope, UserDetails user) { + String path = eventOrgUnit.getStoredPath(); if (isCreatableInSearchScope - ? !user.isInUserEffectiveSearchOrgUnitHierarchy(eventOrgUnit.getPath()) - : !user.isInUserHierarchy(eventOrgUnit.getPath())) { + ? !user.isInUserEffectiveSearchOrgUnitHierarchy(path) + : !user.isInUserHierarchy(path)) { reporter.addError(event, ValidationCode.E1000, user, eventOrgUnit); } } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/trackedentity/SecurityOwnershipValidator.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/trackedentity/SecurityOwnershipValidator.java index d69bd64581fc..1de1b8055817 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/trackedentity/SecurityOwnershipValidator.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/trackedentity/SecurityOwnershipValidator.java @@ -125,7 +125,7 @@ public boolean needsToRun(TrackerImportStrategy strategy) { private void checkOrgUnitInCaptureScope( Reporter reporter, TrackerDto dto, OrganisationUnit orgUnit, UserDetails user) { - if (!user.isInUserHierarchy(orgUnit.getPath())) { + if (!user.isInUserHierarchy(orgUnit.getStoredPath())) { reporter.addError(dto, ValidationCode.E1000, user, orgUnit); } } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/acl/DefaultTrackerAccessManagerTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/acl/DefaultTrackerAccessManagerTest.java index 1f63f94d30fb..042b9fadc477 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/acl/DefaultTrackerAccessManagerTest.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/acl/DefaultTrackerAccessManagerTest.java @@ -30,6 +30,8 @@ import static org.hisp.dhis.common.AccessLevel.CLOSED; import static org.hisp.dhis.common.AccessLevel.OPEN; import static org.hisp.dhis.common.AccessLevel.PROTECTED; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; +import static org.hisp.dhis.test.TestBase.createProgram; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -38,6 +40,7 @@ import org.hisp.dhis.program.Program; import org.hisp.dhis.user.User; import org.hisp.dhis.user.UserDetails; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -48,13 +51,22 @@ class DefaultTrackerAccessManagerTest { @InjectMocks private DefaultTrackerAccessManager trackerAccessManager; + private Program program; + + private OrganisationUnit orgUnit; + + private User user; + + @BeforeEach + public void before() { + program = createProgram('A'); + orgUnit = createOrganisationUnit('A'); + user = new User(); + } + @Test void shouldHaveAccessWhenProgramOpenAndSearchAccessAvailable() { - User user = new User(); - Program program = new Program(); program.setAccessLevel(OPEN); - OrganisationUnit orgUnit = new OrganisationUnit(); - user.setTeiSearchOrganisationUnits(Set.of(orgUnit)); assertTrue( @@ -64,10 +76,7 @@ void shouldHaveAccessWhenProgramOpenAndSearchAccessAvailable() { @Test void shouldNotHaveAccessWhenProgramOpenAndSearchAccessNotAvailable() { - User user = new User(); - Program program = new Program(); program.setAccessLevel(OPEN); - OrganisationUnit orgUnit = new OrganisationUnit(); assertFalse( trackerAccessManager.canAccess(UserDetails.fromUser(user), program, orgUnit), @@ -76,9 +85,6 @@ void shouldNotHaveAccessWhenProgramOpenAndSearchAccessNotAvailable() { @Test void shouldHaveAccessWhenProgramNullAndSearchAccessAvailable() { - User user = new User(); - OrganisationUnit orgUnit = new OrganisationUnit(); - user.setTeiSearchOrganisationUnits(Set.of(orgUnit)); assertTrue( @@ -88,9 +94,6 @@ void shouldHaveAccessWhenProgramNullAndSearchAccessAvailable() { @Test void shouldNotHaveAccessWhenProgramNullAndSearchAccessNotAvailable() { - User user = new User(); - OrganisationUnit orgUnit = new OrganisationUnit(); - assertFalse( trackerAccessManager.canAccess(UserDetails.fromUser(user), null, orgUnit), "User should not have access to unspecified program"); @@ -98,11 +101,7 @@ void shouldNotHaveAccessWhenProgramNullAndSearchAccessNotAvailable() { @Test void shouldHaveAccessWhenProgramClosedAndCaptureAccessAvailable() { - User user = new User(); - Program program = new Program(); program.setAccessLevel(CLOSED); - OrganisationUnit orgUnit = new OrganisationUnit(); - user.setOrganisationUnits(Set.of(orgUnit)); assertTrue( @@ -112,10 +111,7 @@ void shouldHaveAccessWhenProgramClosedAndCaptureAccessAvailable() { @Test void shouldNotHaveAccessWhenProgramClosedAndCaptureAccessNotAvailable() { - User user = new User(); - Program program = new Program(); program.setAccessLevel(CLOSED); - OrganisationUnit orgUnit = new OrganisationUnit(); assertFalse( trackerAccessManager.canAccess(UserDetails.fromUser(user), program, orgUnit), @@ -124,11 +120,7 @@ void shouldNotHaveAccessWhenProgramClosedAndCaptureAccessNotAvailable() { @Test void shouldHaveAccessWhenProgramProtectedAndCaptureAccessAvailable() { - User user = new User(); - Program program = new Program(); program.setAccessLevel(PROTECTED); - OrganisationUnit orgUnit = new OrganisationUnit(); - user.setOrganisationUnits(Set.of(orgUnit)); assertTrue( @@ -138,10 +130,7 @@ void shouldHaveAccessWhenProgramProtectedAndCaptureAccessAvailable() { @Test void shouldNotHaveAccessWhenProgramProtectedAndCaptureAccessNotAvailable() { - User user = new User(); - Program program = new Program(); program.setAccessLevel(PROTECTED); - OrganisationUnit orgUnit = new OrganisationUnit(); assertFalse( trackerAccessManager.canAccess(UserDetails.fromUser(user), program, orgUnit), diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/export/OperationsParamsValidatorTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/export/OperationsParamsValidatorTest.java index 5ab7b641dbcb..b7fa4e296971 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/export/OperationsParamsValidatorTest.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/export/OperationsParamsValidatorTest.java @@ -30,6 +30,7 @@ import static org.hisp.dhis.common.OrganisationUnitSelectionMode.ALL; import static org.hisp.dhis.common.OrganisationUnitSelectionMode.CAPTURE; import static org.hisp.dhis.program.ProgramType.WITHOUT_REGISTRATION; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; import static org.hisp.dhis.tracker.export.OperationsParamsValidator.validateOrgUnitMode; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -54,7 +55,6 @@ import org.hisp.dhis.user.User; import org.hisp.dhis.user.UserDetails; import org.hisp.dhis.user.UserRole; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; @@ -65,20 +65,13 @@ @ExtendWith(MockitoExtension.class) class OperationsParamsValidatorTest { - - private static final String PARENT_ORG_UNIT_UID = "parent-org-unit"; - - private final OrganisationUnit captureScopeOrgUnit = createOrgUnit("captureScopeOrgUnit", "uid3"); - - private final OrganisationUnit searchScopeOrgUnit = createOrgUnit("searchScopeOrgUnit", "uid4"); - private final Program program = new Program("program"); private final TrackedEntity trackedEntity = new TrackedEntity(); private final TrackedEntityType trackedEntityType = new TrackedEntityType(); - private final OrganisationUnit orgUnit = new OrganisationUnit(); + private final OrganisationUnit orgUnit = createOrganisationUnit('A'); private static final UID PROGRAM_UID = UID.generate(); @@ -104,12 +97,6 @@ class OperationsParamsValidatorTest { private final UserDetails user = UserDetails.fromUser(new User()); - @BeforeEach - public void setUp() { - OrganisationUnit organisationUnit = createOrgUnit("orgUnit", PARENT_ORG_UNIT_UID); - organisationUnit.setChildren(Set.of(captureScopeOrgUnit, searchScopeOrgUnit)); - } - @Test void shouldFailWhenOuModeCaptureAndUserHasNoOrgUnitsAssigned() { Exception exception = @@ -385,10 +372,4 @@ void shouldReturnOrgUnitsWhenUserIsSuperButHasNoAccessToOrgUnit() assertEquals(Set.of(orgUnit), orgUnits); } - - private OrganisationUnit createOrgUnit(String name, String uid) { - OrganisationUnit orgUnit = new OrganisationUnit(name); - orgUnit.setUid(uid); - return orgUnit; - } } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/export/enrollment/EnrollmentOperationParamsMapperTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/export/enrollment/EnrollmentOperationParamsMapperTest.java index 853d171634ba..537ce85115ac 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/export/enrollment/EnrollmentOperationParamsMapperTest.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/export/enrollment/EnrollmentOperationParamsMapperTest.java @@ -31,6 +31,7 @@ import static org.hisp.dhis.common.OrganisationUnitSelectionMode.CAPTURE; import static org.hisp.dhis.common.OrganisationUnitSelectionMode.CHILDREN; import static org.hisp.dhis.common.OrganisationUnitSelectionMode.DESCENDANTS; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; import static org.hisp.dhis.test.utils.Assertions.assertIsEmpty; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.verifyNoInteractions; @@ -105,10 +106,10 @@ void setUp() throws ForbiddenException, BadRequestException { User testUser = new User(); testUser.setUsername("admin"); - orgUnit1 = new OrganisationUnit("orgUnit1"); + orgUnit1 = createOrganisationUnit('A'); orgUnit1.setUid(ORG_UNIT_1_UID.getValue()); when(organisationUnitService.getOrganisationUnit(orgUnit1.getUid())).thenReturn(orgUnit1); - orgUnit2 = new OrganisationUnit("orgUnit2"); + orgUnit2 = createOrganisationUnit('B'); orgUnit2.setUid(ORG_UNIT_2_UID.getValue()); orgUnit2.setParent(orgUnit1); orgUnit1.setChildren(Set.of(orgUnit2)); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/export/event/EventOperationParamsMapperTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/export/event/EventOperationParamsMapperTest.java index 824853977ba6..c235081c16d5 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/export/event/EventOperationParamsMapperTest.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/export/event/EventOperationParamsMapperTest.java @@ -36,6 +36,7 @@ import static org.hisp.dhis.common.OrganisationUnitSelectionMode.DESCENDANTS; import static org.hisp.dhis.common.OrganisationUnitSelectionMode.SELECTED; import static org.hisp.dhis.security.Authorities.F_TRACKED_ENTITY_INSTANCE_SEARCH_IN_ALL_ORGUNITS; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; import static org.hisp.dhis.test.utils.Assertions.assertContains; import static org.hisp.dhis.test.utils.Assertions.assertContainsOnly; import static org.hisp.dhis.test.utils.Assertions.assertStartsWith; @@ -131,9 +132,8 @@ class EventOperationParamsMapperTest { @BeforeEach public void setUp() { - OrganisationUnit orgUnit = createOrgUnit("orgUnit"); - orgUnit.setChildren( - Set.of(createOrgUnit("captureScopeChild"), createOrgUnit("searchScopeChild"))); + OrganisationUnit orgUnit = createOrganisationUnit('A'); + orgUnit.setChildren(Set.of(createOrganisationUnit('B'), createOrganisationUnit('C'))); User testUser = new User(); testUser.setUid(CodeGenerator.generateUid()); @@ -142,8 +142,7 @@ public void setUp() { user = UserDetails.fromUser(testUser); // By default, set to ACCESSIBLE for tests that don't set an orgUnit. The orgUnitMode needs to - // be - // set because its validation is in the EventRequestParamsMapper. + // be set because its validation is in the EventRequestParamsMapper. eventBuilder = eventBuilder.orgUnitMode(ACCESSIBLE).eventParams(EventParams.FALSE); userMap.put("admin", createUserWithAuthority(F_TRACKED_ENTITY_INSTANCE_SEARCH_IN_ALL_ORGUNITS)); @@ -421,15 +420,13 @@ void shouldMapOrgUnitWhenProgramProvidedAndRequestedOrgUnitInSearchScope( program.setUid(CodeGenerator.generateUid()); program.setAccessLevel(accessLevel); - OrganisationUnit searchScopeOrgUnit = createOrgUnit("searchScopeOrgUnit"); - OrganisationUnit searchScopeChildOrgUnit = createOrgUnit("searchScopeChildOrgUnit"); - searchScopeOrgUnit.setChildren(Set.of(searchScopeChildOrgUnit)); - searchScopeChildOrgUnit.setParent(searchScopeOrgUnit); + OrganisationUnit searchScopeOrgUnit = createOrganisationUnit('A'); + OrganisationUnit searchScopeChildOrgUnit = createOrganisationUnit('B', searchScopeOrgUnit); User user = new User(); user.setUid(CodeGenerator.generateUid()); user.setUsername("testB"); - user.setOrganisationUnits(Set.of(createOrgUnit("captureScopeOrgUnit"))); + user.setOrganisationUnits(Set.of(createOrganisationUnit('C'))); user.setTeiSearchOrganisationUnits(Set.of(searchScopeOrgUnit)); when(organisationUnitService.getOrganisationUnit(searchScopeChildOrgUnit.getUid())) @@ -453,15 +450,13 @@ void shouldMapOrgUnitWhenModeAllProgramProvidedAndRequestedOrgUnitInSearchScope( program.setUid(CodeGenerator.generateUid()); program.setAccessLevel(OPEN); - OrganisationUnit searchScopeOrgUnit = createOrgUnit("searchScopeOrgUnit"); - OrganisationUnit searchScopeChildOrgUnit = createOrgUnit("searchScopeChildOrgUnit"); - searchScopeOrgUnit.setChildren(Set.of(searchScopeChildOrgUnit)); - searchScopeChildOrgUnit.setParent(searchScopeOrgUnit); + OrganisationUnit searchScopeOrgUnit = createOrganisationUnit('A'); + OrganisationUnit searchScopeChildOrgUnit = createOrganisationUnit('B', searchScopeOrgUnit); User user = new User(); user.setUid(CodeGenerator.generateUid()); user.setUsername("testB"); - user.setOrganisationUnits(Set.of(createOrgUnit("captureScopeOrgUnit"))); + user.setOrganisationUnits(Set.of(createOrganisationUnit('C'))); user.setTeiSearchOrganisationUnits(Set.of(searchScopeOrgUnit)); UserRole userRole = new UserRole(); userRole.setAuthorities(Set.of(F_TRACKED_ENTITY_INSTANCE_SEARCH_IN_ALL_ORGUNITS.name())); @@ -481,7 +476,7 @@ void shouldMapOrgUnitWhenModeAllProgramProvidedAndRequestedOrgUnitInSearchScope( @EnumSource(value = OrganisationUnitSelectionMode.class) void shouldFailWhenRequestedOrgUnitOutsideOfSearchScope( OrganisationUnitSelectionMode orgUnitMode) { - OrganisationUnit orgUnit = createOrgUnit("name"); + OrganisationUnit orgUnit = createOrganisationUnit('A'); when(organisationUnitService.getOrganisationUnit(orgUnit.getUid())).thenReturn(orgUnit); EventOperationParams operationParams = EventOperationParams.builder().orgUnit(orgUnit).orgUnitMode(orgUnitMode).build(); @@ -544,10 +539,4 @@ private User createUserWithAuthority(Authorities authority) { return user; } - - private OrganisationUnit createOrgUnit(String name) { - OrganisationUnit orgUnit = new OrganisationUnit(name); - orgUnit.setUid(CodeGenerator.generateUid()); - return orgUnit; - } } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/preheat/mappers/OrganisationUnitMapperTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/preheat/mappers/OrganisationUnitMapperTest.java index 1f46665e7b91..2816f9e68991 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/preheat/mappers/OrganisationUnitMapperTest.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/preheat/mappers/OrganisationUnitMapperTest.java @@ -27,6 +27,7 @@ */ package org.hisp.dhis.tracker.imports.preheat.mappers; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; import static org.hisp.dhis.tracker.imports.preheat.mappers.AttributeCreator.attributeValues; import static org.hisp.dhis.tracker.imports.preheat.mappers.AttributeCreator.setIdSchemeFields; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -44,7 +45,7 @@ void testIdSchemeRelatedFieldsAreMapped() { OrganisationUnit orgUnit = setIdSchemeFields( - new OrganisationUnit(), + createOrganisationUnit('A'), "HpSAvRWtdDR", "meet", "green", @@ -60,11 +61,11 @@ void testIdSchemeRelatedFieldsAreMapped() { @Test void testParentFieldsAreMapped() { - OrganisationUnit rootOrgUnit = new OrganisationUnit(); + OrganisationUnit rootOrgUnit = createOrganisationUnit('A'); rootOrgUnit.setUid("root"); - OrganisationUnit level1OrgUnit = new OrganisationUnit(); + OrganisationUnit level1OrgUnit = createOrganisationUnit('B'); level1OrgUnit.setUid("level1"); - OrganisationUnit level2OrgUnit = new OrganisationUnit(); + OrganisationUnit level2OrgUnit = createOrganisationUnit('C'); level2OrgUnit.setUid("level2"); level2OrgUnit.setParent(level1OrgUnit); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/preheat/mappers/TrackedEntityMapperTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/preheat/mappers/TrackedEntityMapperTest.java index eb1e4127ff3e..1972dcc9f201 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/preheat/mappers/TrackedEntityMapperTest.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/preheat/mappers/TrackedEntityMapperTest.java @@ -27,6 +27,7 @@ */ package org.hisp.dhis.tracker.imports.preheat.mappers; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; import static org.hisp.dhis.tracker.imports.preheat.mappers.AttributeCreator.attributeValues; import static org.hisp.dhis.tracker.imports.preheat.mappers.AttributeCreator.setIdSchemeFields; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -57,7 +58,7 @@ void testIdSchemeRelatedFieldsAreMapped() { OrganisationUnit orgUnit = setIdSchemeFields( - new OrganisationUnit(), + createOrganisationUnit('A'), "HpSAvRWtdDR", "meet", "green", diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/MessageFormatterTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/MessageFormatterTest.java index dbb09804f82d..5d969ae716e9 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/MessageFormatterTest.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/MessageFormatterTest.java @@ -29,6 +29,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; import static org.hisp.dhis.test.utils.Assertions.assertContainsOnly; import static org.hisp.dhis.test.utils.Assertions.assertIsEmpty; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -104,7 +105,7 @@ void formatArgumentsShouldTurnIdentifiableObjectIntoArgument() { relationshipType.setUid("WTTYiPQDqh1"); Program program = new Program("friendship"); ProgramStage programStage = new ProgramStage("meet", program); - OrganisationUnit orgUnit = new OrganisationUnit(); + OrganisationUnit orgUnit = createOrganisationUnit('A'); orgUnit.setAttributeValues(attributeValues("HpSAvRWtdDR", "sunshine")); DataElement dataElement = new DataElement(); dataElement.setAttributeValues(attributeValues("m0GpPuMUfFW", "ice")); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/MetaValidatorTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/MetaValidatorTest.java index 5af6e72e5d0e..a13524d98edc 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/MetaValidatorTest.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/MetaValidatorTest.java @@ -27,6 +27,7 @@ */ package org.hisp.dhis.tracker.imports.validation.validator.enrollment; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; import static org.hisp.dhis.test.utils.Assertions.assertIsEmpty; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1068; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1069; @@ -36,7 +37,6 @@ import java.util.Optional; import org.hisp.dhis.common.UID; -import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.program.Program; import org.hisp.dhis.trackedentity.TrackedEntity; import org.hisp.dhis.tracker.TrackerIdSchemeParams; @@ -84,7 +84,7 @@ public void setUp() { void verifyEnrollmentValidationSuccess() { Enrollment enrollment = validEnrollment(); when(preheat.getOrganisationUnit(MetadataIdentifier.ofUid(ORG_UNIT_UID))) - .thenReturn(new OrganisationUnit()); + .thenReturn(createOrganisationUnit('A')); when(preheat.getTrackedEntity(TRACKED_ENTITY_UID)).thenReturn(new TrackedEntity()); when(preheat.getProgram(MetadataIdentifier.ofUid(PROGRAM_UID))).thenReturn(new Program()); @@ -99,7 +99,7 @@ void verifyEnrollmentValidationSuccessWhenTeiIsInPayload() { when(bundle.findTrackedEntityByUid(TRACKED_ENTITY_UID)) .thenReturn(Optional.of(new org.hisp.dhis.tracker.imports.domain.TrackedEntity())); when(preheat.getOrganisationUnit(MetadataIdentifier.ofUid(ORG_UNIT_UID))) - .thenReturn(new OrganisationUnit()); + .thenReturn(createOrganisationUnit('A')); when(preheat.getProgram(MetadataIdentifier.ofUid(PROGRAM_UID))).thenReturn(new Program()); validator.validate(reporter, bundle, enrollment); @@ -122,7 +122,7 @@ void verifyEnrollmentValidationFailsWhenOrgUnitIsNotPresentInDb() { void verifyEnrollmentValidationFailsWhenTrackedEntityIsNotPresentInDbOrPayload() { Enrollment enrollment = validEnrollment(); when(preheat.getOrganisationUnit(MetadataIdentifier.ofUid(ORG_UNIT_UID))) - .thenReturn(new OrganisationUnit()); + .thenReturn(createOrganisationUnit('A')); when(preheat.getProgram(MetadataIdentifier.ofUid(PROGRAM_UID))).thenReturn(new Program()); validator.validate(reporter, bundle, enrollment); @@ -134,7 +134,7 @@ void verifyEnrollmentValidationFailsWhenTrackedEntityIsNotPresentInDbOrPayload() void verifyEnrollmentValidationFailsWhenProgramIsNotPresentInDb() { Enrollment enrollment = validEnrollment(); when(preheat.getOrganisationUnit(MetadataIdentifier.ofUid(ORG_UNIT_UID))) - .thenReturn(new OrganisationUnit()); + .thenReturn(createOrganisationUnit('A')); when(preheat.getTrackedEntity(TRACKED_ENTITY_UID)).thenReturn(new TrackedEntity()); validator.validate(reporter, bundle, enrollment); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/SecurityOwnershipValidatorTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/SecurityOwnershipValidatorTest.java index f471e3731b0d..a2a34fd7197f 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/SecurityOwnershipValidatorTest.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/SecurityOwnershipValidatorTest.java @@ -111,6 +111,7 @@ class SecurityOwnershipValidatorTest extends TestBase { public void setUp() { organisationUnit = createOrganisationUnit('A'); organisationUnit.setUid(ORG_UNIT_ID); + organisationUnit.updatePath(); User u = makeUser("A"); user = UserDetails.fromUser(u); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/event/DataValuesValidatorTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/event/DataValuesValidatorTest.java index 9c46f774365b..5e9d99599208 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/event/DataValuesValidatorTest.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/event/DataValuesValidatorTest.java @@ -27,6 +27,7 @@ */ package org.hisp.dhis.tracker.imports.validation.validator.event; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; import static org.hisp.dhis.test.utils.Assertions.assertIsEmpty; import static org.hisp.dhis.tracker.imports.validation.validator.AssertValidations.assertHasError; import static org.hisp.dhis.tracker.imports.validation.validator.AssertValidations.assertNoErrors; @@ -1128,7 +1129,7 @@ private Set getProgramStageDataElements( } private OrganisationUnit organisationUnit() { - OrganisationUnit organisationUnit = new OrganisationUnit(); + OrganisationUnit organisationUnit = createOrganisationUnit('A'); organisationUnit.setUid(ORGANISATION_UNIT_UID); return organisationUnit; } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/event/SecurityOwnershipValidatorTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/event/SecurityOwnershipValidatorTest.java index 552ecdc2797d..c09843523208 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/event/SecurityOwnershipValidatorTest.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/event/SecurityOwnershipValidatorTest.java @@ -112,6 +112,7 @@ public void setUp() { when(bundle.getUser()).thenReturn(user); organisationUnit = createOrganisationUnit('A'); organisationUnit.setUid(ORG_UNIT_ID); + organisationUnit.updatePath(); trackedEntityType = createTrackedEntityType('A'); trackedEntityType.setUid(TE_TYPE_ID); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/trackedentity/MetaValidatorTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/trackedentity/MetaValidatorTest.java index c9dbcde74fe0..d9aac02c46cf 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/trackedentity/MetaValidatorTest.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/trackedentity/MetaValidatorTest.java @@ -27,6 +27,7 @@ */ package org.hisp.dhis.tracker.imports.validation.validator.trackedentity; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; import static org.hisp.dhis.test.utils.Assertions.assertIsEmpty; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1005; import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1049; @@ -34,7 +35,6 @@ import static org.mockito.Mockito.when; import org.hisp.dhis.common.UID; -import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.trackedentity.TrackedEntityType; import org.hisp.dhis.tracker.TrackerIdSchemeParams; import org.hisp.dhis.tracker.imports.bundle.TrackerBundle; @@ -81,7 +81,7 @@ public void setUp() { void verifyTrackedEntityValidationSuccess() { TrackedEntity te = validTe(); when(preheat.getOrganisationUnit(MetadataIdentifier.ofUid(ORG_UNIT_UID))) - .thenReturn(new OrganisationUnit()); + .thenReturn(createOrganisationUnit('A')); when(preheat.getTrackedEntityType(MetadataIdentifier.ofUid(TRACKED_ENTITY_TYPE_UID))) .thenReturn(new TrackedEntityType()); @@ -105,7 +105,7 @@ void verifyTrackedEntityValidationFailsWhenOrgUnitIsNotPresentInDb() { void verifyTrackedEntityValidationFailsWhenTrackedEntityTypeIsNotPresentInDb() { TrackedEntity te = validTe(); when(preheat.getOrganisationUnit(MetadataIdentifier.ofUid(ORG_UNIT_UID))) - .thenReturn(new OrganisationUnit()); + .thenReturn(createOrganisationUnit('A')); validator.validate(reporter, bundle, te); diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/trackedentity/SecurityOwnershipValidatorTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/trackedentity/SecurityOwnershipValidatorTest.java index 38186f6b4c65..9228ee53a58a 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/trackedentity/SecurityOwnershipValidatorTest.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/trackedentity/SecurityOwnershipValidatorTest.java @@ -106,6 +106,7 @@ class SecurityOwnershipValidatorTest extends TestBase { public void setUp() { organisationUnit = createOrganisationUnit('A'); organisationUnit.setUid(ORG_UNIT_ID); + organisationUnit.updatePath(); User userA = makeUser("A"); userA.addOrganisationUnit(organisationUnit); diff --git a/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/hibernate/HibernateMinMaxDataElementStore.java b/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/hibernate/HibernateMinMaxDataElementStore.java index 61e453207ba8..1a33cfcc4a2f 100644 --- a/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/hibernate/HibernateMinMaxDataElementStore.java +++ b/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/hibernate/HibernateMinMaxDataElementStore.java @@ -37,8 +37,10 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import javax.annotation.Nonnull; import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.common.Pager; +import org.hisp.dhis.common.UID; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.hibernate.HibernateGenericStore; import org.hisp.dhis.hibernate.JpaQueryParameters; @@ -177,7 +179,7 @@ public void delete(Collection dataElements, OrganisationUnit parent getQuery(hql) .setParameterList("dataElements", dataElements) - .setParameter("path", parent.getPath() + "%") + .setParameter("path", parent.getStoredPath() + "%") .executeUpdate(); } @@ -193,6 +195,19 @@ public List getByDataElement(Collection dataElem .list(); } + @Override + public List getByCategoryOptionCombo(@Nonnull Collection uids) { + if (uids.isEmpty()) return List.of(); + return getQuery( + """ + select distinct mmde from MinMaxDataElement mmde + join mmde.optionCombo coc + where coc.uid in :uids + """) + .setParameter("uids", UID.toValueList(uids)) + .list(); + } + private Predicate parseFilter(CriteriaBuilder builder, Root root, List filters) { Predicate conjunction = builder.conjunction(); diff --git a/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/outlierdetection/util/OutlierDetectionUtils.java b/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/outlierdetection/util/OutlierDetectionUtils.java index a6e9c6321960..da8852edffe1 100644 --- a/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/outlierdetection/util/OutlierDetectionUtils.java +++ b/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/outlierdetection/util/OutlierDetectionUtils.java @@ -63,7 +63,10 @@ public static String getOrgUnitPathClause(List orgUnits, Strin StringBuilder sql = new StringBuilder("("); orgUnits.forEach( ou -> - sql.append(pathAlias).append(".\"path\" like '").append(ou.getPath()).append("%' or ")); + sql.append(pathAlias) + .append(".\"path\" like '") + .append(ou.getStoredPath()) + .append("%' or ")); return StringUtils.trim(TextUtils.removeLastOr(sql.toString())) + ")"; } diff --git a/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/validation/hibernate/HibernateValidationResultStore.java b/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/validation/hibernate/HibernateValidationResultStore.java index 284d5c7076bc..c561f52dfa5c 100644 --- a/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/validation/hibernate/HibernateValidationResultStore.java +++ b/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/validation/hibernate/HibernateValidationResultStore.java @@ -205,7 +205,8 @@ public List getValidationResults( + "vr.validationRule in :validationRules and vr.period in :periods "); if (orgUnit != null) { - query.setParameter("orgUnitPath", orgUnit.getPath() + (includeOrgUnitDescendants ? "%" : "")); + query.setParameter( + "orgUnitPath", orgUnit.getStoredPath() + (includeOrgUnitDescendants ? "%" : "")); } query.setParameter("validationRules", validationRules); diff --git a/dhis-2/dhis-services/dhis-service-validation/src/test/java/org/hisp/dhis/validation/ValidationResultServiceTest.java b/dhis-2/dhis-services/dhis-service-validation/src/test/java/org/hisp/dhis/validation/ValidationResultServiceTest.java index 8639a8e0ba3f..d05f735efc2a 100644 --- a/dhis-2/dhis-services/dhis-service-validation/src/test/java/org/hisp/dhis/validation/ValidationResultServiceTest.java +++ b/dhis-2/dhis-services/dhis-service-validation/src/test/java/org/hisp/dhis/validation/ValidationResultServiceTest.java @@ -30,6 +30,7 @@ import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; +import static org.hisp.dhis.test.TestBase.createOrganisationUnit; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -82,7 +83,7 @@ void setUp() { List units = new ArrayList<>(); for (String uid : uids) { if (CodeGenerator.isValidUid(uid)) { - OrganisationUnit unit = new OrganisationUnit(); + OrganisationUnit unit = createOrganisationUnit('A'); unit.setUid(uid); units.add(unit); } diff --git a/dhis-2/dhis-support/dhis-support-external/src/main/java/org/hisp/dhis/external/conf/ConfigurationKey.java b/dhis-2/dhis-support/dhis-support-external/src/main/java/org/hisp/dhis/external/conf/ConfigurationKey.java index 1fcfe101b848..44dbad2efb45 100644 --- a/dhis-2/dhis-support/dhis-support-external/src/main/java/org/hisp/dhis/external/conf/ConfigurationKey.java +++ b/dhis-2/dhis-support/dhis-support-external/src/main/java/org/hisp/dhis/external/conf/ConfigurationKey.java @@ -100,8 +100,7 @@ public enum ConfigurationKey { CONNECTION_DRIVER_CLASS("connection.driver_class", "org.postgresql.Driver", false), /** Analytics JDBC driver class. */ - ANALYTICS_CONNECTION_DRIVER_CLASS( - "analytics.connection.driver_class", "org.postgresql.Driver", false), + ANALYTICS_CONNECTION_DRIVER_CLASS("analytics.connection.driver_class", "", false), /** Database connection URL. */ CONNECTION_URL("connection.url", "", false), diff --git a/dhis-2/dhis-support/dhis-support-hibernate/pom.xml b/dhis-2/dhis-support/dhis-support-hibernate/pom.xml index 2a818f633722..b7c274dbca11 100644 --- a/dhis-2/dhis-support/dhis-support-hibernate/pom.xml +++ b/dhis-2/dhis-support/dhis-support-hibernate/pom.xml @@ -31,6 +31,10 @@ org.hisp.dhis dhis-support-commons + + org.hisp.dhis + dhis-support-sql + diff --git a/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/config/AnalyticsDataSourceConfig.java b/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/config/AnalyticsDataSourceConfig.java index 344fc26ad20d..468a6ff711bd 100644 --- a/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/config/AnalyticsDataSourceConfig.java +++ b/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/config/AnalyticsDataSourceConfig.java @@ -27,6 +27,7 @@ */ package org.hisp.dhis.config; +import static org.apache.commons.lang3.StringUtils.isBlank; import static org.hisp.dhis.config.DataSourceConfig.createLoggingDataSource; import static org.hisp.dhis.datasource.DatabasePoolUtils.ConfigKeyMapper.ANALYTICS; import static org.hisp.dhis.external.conf.ConfigurationKey.ANALYTICS_CONNECTION_URL; @@ -42,7 +43,9 @@ import org.hisp.dhis.commons.util.TextUtils; import org.hisp.dhis.datasource.DatabasePoolUtils; import org.hisp.dhis.datasource.ReadOnlyDataSourceManager; -import org.hisp.dhis.datasource.model.PoolConfig; +import org.hisp.dhis.datasource.model.DbPoolConfig; +import org.hisp.dhis.db.model.Database; +import org.hisp.dhis.db.setting.SqlBuilderSettings; import org.hisp.dhis.external.conf.ConfigurationKey; import org.hisp.dhis.external.conf.DhisConfigurationProvider; import org.springframework.beans.factory.annotation.Qualifier; @@ -61,6 +64,8 @@ public class AnalyticsDataSourceConfig { private final DhisConfigurationProvider config; + private final SqlBuilderSettings sqlBuilderSettings; + @Bean("analyticsDataSource") @DependsOn("analyticsActualDataSource") public DataSource jdbcDataSource( @@ -126,11 +131,17 @@ public JdbcTemplate jdbcTemplate(@Qualifier("analyticsDataSource") DataSource da * @return a {@link DataSource}. */ private DataSource getAnalyticsDataSource() { - String jdbcUrl = config.getProperty(ANALYTICS_CONNECTION_URL); - String dbPoolType = config.getProperty(ConfigurationKey.DB_POOL_TYPE); - - PoolConfig poolConfig = - PoolConfig.builder().dhisConfig(config).mapper(ANALYTICS).dbPoolType(dbPoolType).build(); + final String jdbcUrl = config.getProperty(ANALYTICS_CONNECTION_URL); + final String driverClassName = inferDriverClassName(); + final String dbPoolType = config.getProperty(ConfigurationKey.DB_POOL_TYPE); + + DbPoolConfig poolConfig = + DbPoolConfig.builder() + .driverClassName(driverClassName) + .dhisConfig(config) + .mapper(ANALYTICS) + .dbPoolType(dbPoolType) + .build(); try { return DatabasePoolUtils.createDbPool(poolConfig); @@ -157,4 +168,29 @@ private JdbcTemplate getJdbcTemplate(DataSource dataSource) { jdbcTemplate.setFetchSize(FETCH_SIZE); return jdbcTemplate; } + + /** + * If the driver class name is not explicitly specified, returns the driver class name based on + * the the specified analytics database. + * + * @return a driver class name. + */ + private String inferDriverClassName() { + String driverClass = config.getProperty(ConfigurationKey.ANALYTICS_CONNECTION_DRIVER_CLASS); + return isBlank(driverClass) ? getDriverClassName() : driverClass; + } + + /** + * Returns a driver class name based on the specified analytics database. + * + * @return a driver class name. + */ + private String getDriverClassName() { + final Database database = sqlBuilderSettings.getAnalyticsDatabase(); + return switch (database) { + case POSTGRESQL -> org.postgresql.Driver.class.getName(); + case DORIS -> com.mysql.cj.jdbc.Driver.class.getName(); + case CLICKHOUSE -> com.clickhouse.jdbc.ClickHouseDriver.class.getName(); + }; + } } diff --git a/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/config/DataSourceConfig.java b/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/config/DataSourceConfig.java index a92661c2cd04..90beb36cfebb 100644 --- a/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/config/DataSourceConfig.java +++ b/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/config/DataSourceConfig.java @@ -46,7 +46,7 @@ import org.hisp.dhis.commons.util.DebugUtils; import org.hisp.dhis.datasource.DatabasePoolUtils; import org.hisp.dhis.datasource.ReadOnlyDataSourceManager; -import org.hisp.dhis.datasource.model.PoolConfig; +import org.hisp.dhis.datasource.model.DbPoolConfig; import org.hisp.dhis.external.conf.ConfigurationKey; import org.hisp.dhis.external.conf.DhisConfigurationProvider; import org.springframework.context.annotation.Bean; @@ -100,7 +100,8 @@ private DataSource actualDataSource(DhisConfigurationProvider config) { String username = config.getProperty(ConfigurationKey.CONNECTION_USERNAME); String dbPoolType = config.getProperty(ConfigurationKey.DB_POOL_TYPE); - PoolConfig poolConfig = PoolConfig.builder().dhisConfig(config).dbPoolType(dbPoolType).build(); + DbPoolConfig poolConfig = + DbPoolConfig.builder().dhisConfig(config).dbPoolType(dbPoolType).build(); try { return DatabasePoolUtils.createDbPool(poolConfig); @@ -187,16 +188,7 @@ private static void executeAfterMethod(MethodExecutionContext executionContext) StackTraceElement nextElement = stackTrace[i - 1]; String methodName1 = nextElement.getMethodName(); String className1 = nextElement.getClassName(); - - log.info( - "JDBC: " - + className - + "#" - + methodName - + " ---- \n ----" - + className1 - + "#" - + methodName1); + log.info("JDBC: {}#{} - \n - {}#{}", className, methodName, className1, methodName1); break; } } diff --git a/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/datasource/DatabasePoolUtils.java b/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/datasource/DatabasePoolUtils.java index be96ae976bc5..ecb3cef22f49 100644 --- a/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/datasource/DatabasePoolUtils.java +++ b/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/datasource/DatabasePoolUtils.java @@ -84,7 +84,7 @@ import lombok.extern.slf4j.Slf4j; import org.hisp.dhis.common.CodeGenerator; import org.hisp.dhis.commons.util.TextUtils; -import org.hisp.dhis.datasource.model.PoolConfig; +import org.hisp.dhis.datasource.model.DbPoolConfig; import org.hisp.dhis.external.conf.ConfigurationKey; import org.hisp.dhis.external.conf.DhisConfigurationProvider; import org.springframework.jdbc.datasource.DriverManagerDataSource; @@ -148,7 +148,7 @@ public enum DbPoolType { UNPOOLED } - public static DataSource createDbPool(PoolConfig config) + public static DataSource createDbPool(DbPoolConfig config) throws PropertyVetoException, SQLException { Objects.requireNonNull(config); @@ -158,7 +158,9 @@ public static DataSource createDbPool(PoolConfig config) DhisConfigurationProvider dhisConfig = config.getDhisConfig(); final String driverClassName = - dhisConfig.getProperty(mapper.getConfigKey(CONNECTION_DRIVER_CLASS)); + firstNonNull( + config.getDriverClassName(), + dhisConfig.getProperty(mapper.getConfigKey(CONNECTION_DRIVER_CLASS))); final String jdbcUrl = firstNonNull( config.getJdbcUrl(), dhisConfig.getProperty(mapper.getConfigKey(CONNECTION_URL))); @@ -173,7 +175,7 @@ public static DataSource createDbPool(PoolConfig config) switch (dbPoolType) { case C3P0 -> createC3p0DbPool(username, password, driverClassName, jdbcUrl, config); case HIKARI -> createHikariDbPool(username, password, driverClassName, jdbcUrl, config); - case UNPOOLED -> createUnPooledDataSource(username, password, driverClassName, jdbcUrl); + case UNPOOLED -> createNoPoolDataSource(username, password, driverClassName, jdbcUrl); default -> throw new IllegalArgumentException( TextUtils.format( @@ -185,8 +187,22 @@ public static DataSource createDbPool(PoolConfig config) return dataSource; } + public static void testConnection(DataSource dataSource) { + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.executeQuery("select 'connection_test' as connection_test;"); + } catch (SQLException e) { + log.error(e.getMessage()); + } + } + + /** Create a data source based on a Hikari connection pool. */ private static DataSource createHikariDbPool( - String username, String password, String driverClassName, String jdbcUrl, PoolConfig config) { + String username, + String password, + String driverClassName, + String jdbcUrl, + DbPoolConfig config) { ConfigKeyMapper mapper = config.getMapper(); DhisConfigurationProvider dhisConfig = config.getDhisConfig(); @@ -222,19 +238,9 @@ private static DataSource createHikariDbPool( return ds; } - private static DriverManagerDataSource createUnPooledDataSource( - String username, String password, String driverClassName, String jdbcUrl) { - final DriverManagerDataSource unPooledDataSource = new DriverManagerDataSource(); - unPooledDataSource.setDriverClassName(driverClassName); - unPooledDataSource.setUrl(jdbcUrl); - unPooledDataSource.setUsername(username); - unPooledDataSource.setPassword(password); - - return unPooledDataSource; - } - + /** Create a data source based on a C3p0 connection pool. */ private static ComboPooledDataSource createC3p0DbPool( - String username, String password, String driverClassName, String jdbcUrl, PoolConfig config) + String username, String password, String driverClassName, String jdbcUrl, DbPoolConfig config) throws PropertyVetoException { ConfigKeyMapper mapper = config.getMapper(); DhisConfigurationProvider dhisConfig = config.getDhisConfig(); @@ -306,12 +312,15 @@ private static ComboPooledDataSource createC3p0DbPool( return pooledDataSource; } - public static void testConnection(DataSource dataSource) { - try (Connection conn = dataSource.getConnection(); - Statement stmt = conn.createStatement()) { - stmt.executeQuery("select 'connection_test' as connection_test;"); - } catch (SQLException e) { - log.error(e.getMessage()); - } + /** Creates a data source with no connection pool. */ + private static DriverManagerDataSource createNoPoolDataSource( + String username, String password, String driverClassName, String jdbcUrl) { + final DriverManagerDataSource unPooledDataSource = new DriverManagerDataSource(); + unPooledDataSource.setDriverClassName(driverClassName); + unPooledDataSource.setUrl(jdbcUrl); + unPooledDataSource.setUsername(username); + unPooledDataSource.setPassword(password); + + return unPooledDataSource; } } diff --git a/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/datasource/ReadOnlyDataSourceManager.java b/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/datasource/ReadOnlyDataSourceManager.java index 9aa4fc035ed2..e16413ea19df 100644 --- a/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/datasource/ReadOnlyDataSourceManager.java +++ b/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/datasource/ReadOnlyDataSourceManager.java @@ -43,7 +43,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.hisp.dhis.commons.util.DebugUtils; -import org.hisp.dhis.datasource.model.PoolConfig; +import org.hisp.dhis.datasource.model.DbPoolConfig; import org.hisp.dhis.datasource.model.ReadOnlyDataSourceConfig; import org.hisp.dhis.external.conf.ConfigurationKey; import org.hisp.dhis.external.conf.DhisConfigurationProvider; @@ -123,7 +123,7 @@ private List getReadOnlyDataSources(DhisConfigurationProvider config String username = StringUtils.defaultIfEmpty(dataSourceConfig.getUsername(), mainUser); String password = StringUtils.defaultIfEmpty(dataSourceConfig.getPassword(), mainPassword); - PoolConfig.PoolConfigBuilder builder = PoolConfig.builder(); + DbPoolConfig.DbPoolConfigBuilder builder = DbPoolConfig.builder(); builder.dhisConfig(config); builder.password(password); builder.username(username); diff --git a/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/datasource/model/PoolConfig.java b/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/datasource/model/DbPoolConfig.java similarity index 97% rename from dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/datasource/model/PoolConfig.java rename to dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/datasource/model/DbPoolConfig.java index 97bf390d358d..d47d4e9420eb 100644 --- a/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/datasource/model/PoolConfig.java +++ b/dhis-2/dhis-support/dhis-support-hibernate/src/main/java/org/hisp/dhis/datasource/model/DbPoolConfig.java @@ -40,11 +40,13 @@ */ @Value @Builder -public class PoolConfig { +public class DbPoolConfig { private String dbPoolType; private DhisConfigurationProvider dhisConfig; + private String driverClassName; + private String jdbcUrl; private String username; diff --git a/dhis-2/dhis-support/dhis-support-hibernate/src/test/java/org/hisp/dhis/datasource/DatabasePoolUtilsTest.java b/dhis-2/dhis-support/dhis-support-hibernate/src/test/java/org/hisp/dhis/datasource/DatabasePoolUtilsTest.java index ca6a381194ea..1974b7222ca1 100644 --- a/dhis-2/dhis-support/dhis-support-hibernate/src/test/java/org/hisp/dhis/datasource/DatabasePoolUtilsTest.java +++ b/dhis-2/dhis-support/dhis-support-hibernate/src/test/java/org/hisp/dhis/datasource/DatabasePoolUtilsTest.java @@ -38,7 +38,7 @@ import java.sql.SQLException; import java.util.concurrent.ThreadLocalRandom; import javax.sql.DataSource; -import org.hisp.dhis.datasource.model.PoolConfig; +import org.hisp.dhis.datasource.model.DbPoolConfig; import org.hisp.dhis.external.conf.ConfigurationKey; import org.hisp.dhis.external.conf.DhisConfigurationProvider; import org.junit.jupiter.api.BeforeEach; @@ -59,8 +59,8 @@ void testCreateDbPoolWhenDbPoolTypeIsUnPooled() throws PropertyVetoException, SQ given(mockDhisConfigurationProvider.getProperty(ConfigurationKey.CONNECTION_DRIVER_CLASS)) .willReturn("org.hisp.dhis.datasource.StubDriver"); - PoolConfig.PoolConfigBuilder poolConfigBuilder = - PoolConfig.builder() + DbPoolConfig.DbPoolConfigBuilder poolConfigBuilder = + DbPoolConfig.builder() .dbPoolType(DatabasePoolUtils.DbPoolType.UNPOOLED.name()) .jdbcUrl("jdbc:fake:db") .username("") @@ -91,8 +91,8 @@ void testCreateDbPoolWhenDbPoolTypeIsC3P0() throws PropertyVetoException, SQLExc given(mockDhisConfigurationProvider.getProperty(ConfigurationKey.CONNECTION_POOL_NUM_THREADS)) .willReturn("1"); - PoolConfig.PoolConfigBuilder poolConfigBuilder = - PoolConfig.builder() + DbPoolConfig.DbPoolConfigBuilder poolConfigBuilder = + DbPoolConfig.builder() .dbPoolType(DatabasePoolUtils.DbPoolType.C3P0.name()) .jdbcUrl("jdbc:fake:db") .username("") @@ -120,8 +120,8 @@ void testCreateDbPoolWhenDbPoolTypeIsHikari() throws PropertyVetoException, SQLE ConfigurationKey.CONNECTION_POOL_VALIDATION_TIMEOUT)) .willReturn("250"); - PoolConfig.PoolConfigBuilder poolConfigBuilder = - PoolConfig.builder() + DbPoolConfig.DbPoolConfigBuilder poolConfigBuilder = + DbPoolConfig.builder() .dbPoolType(DatabasePoolUtils.DbPoolType.HIKARI.name()) .jdbcUrl("jdbc:fake:db") .username("") diff --git a/dhis-2/dhis-support/dhis-support-sql/pom.xml b/dhis-2/dhis-support/dhis-support-sql/pom.xml index d26ebcf726a5..b27f0e772e63 100644 --- a/dhis-2/dhis-support/dhis-support-sql/pom.xml +++ b/dhis-2/dhis-support/dhis-support-sql/pom.xml @@ -21,60 +21,52 @@ org.hisp.dhis - dhis-support-commons + dhis-api - org.hisp.dhis - dhis-support-external + dhis-support-commons - org.hisp.dhis - dhis-api + dhis-support-external - org.springframework spring-context - org.springframework spring-beans - org.springframework spring-jdbc + + javax.annotation javax.annotation-api - org.apache.commons commons-text - org.apache.commons commons-lang3 - org.apache.commons commons-collections4 - org.projectlombok lombok provided - org.slf4j slf4j-api diff --git a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/init/AnalyticsDatabaseInit.java b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/init/AnalyticsDatabaseInit.java index 1bce670ce479..51de3c92b81e 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/init/AnalyticsDatabaseInit.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/init/AnalyticsDatabaseInit.java @@ -37,6 +37,7 @@ import org.hisp.dhis.db.model.Database; import org.hisp.dhis.db.setting.SqlBuilderSettings; import org.hisp.dhis.db.sql.ClickHouseSqlBuilder; +import org.hisp.dhis.db.sql.DorisSqlBuilder; import org.hisp.dhis.db.sql.SqlBuilder; import org.hisp.dhis.external.conf.ConfigurationKey; import org.hisp.dhis.external.conf.DhisConfigurationProvider; @@ -112,8 +113,10 @@ private void createDorisJdbcCatalog() { String username = config.getProperty(ConfigurationKey.CONNECTION_USERNAME); String password = config.getProperty(ConfigurationKey.CONNECTION_PASSWORD); - jdbcTemplate.execute(sqlBuilder.dropCatalogIfExists()); - jdbcTemplate.execute(sqlBuilder.createCatalog(connectionUrl, username, password)); + DorisSqlBuilder dorisSqlBuilder = (DorisSqlBuilder) sqlBuilder; + + jdbcTemplate.execute(dorisSqlBuilder.dropCatalogIfExists()); + jdbcTemplate.execute(dorisSqlBuilder.createCatalog(connectionUrl, username, password)); } /** diff --git a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/setting/SqlBuilderSettings.java b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/setting/SqlBuilderSettings.java index a62bd5b8835f..877d6b19ec6c 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/setting/SqlBuilderSettings.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/setting/SqlBuilderSettings.java @@ -28,19 +28,15 @@ package org.hisp.dhis.db.setting; import static org.hisp.dhis.commons.util.TextUtils.format; -import static org.hisp.dhis.db.model.Logged.LOGGED; -import static org.hisp.dhis.db.model.Logged.UNLOGGED; import static org.hisp.dhis.external.conf.ConfigurationKey.ANALYTICS_DATABASE; import static org.hisp.dhis.external.conf.ConfigurationKey.ANALYTICS_DATABASE_CATALOG; import static org.hisp.dhis.external.conf.ConfigurationKey.ANALYTICS_DATABASE_DRIVER_FILENAME; -import static org.hisp.dhis.external.conf.ConfigurationKey.ANALYTICS_TABLE_UNLOGGED; import static org.hisp.dhis.util.ObjectUtils.isNull; import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.EnumUtils; import org.apache.commons.lang3.StringUtils; import org.hisp.dhis.db.model.Database; -import org.hisp.dhis.db.model.Logged; import org.hisp.dhis.external.conf.DhisConfigurationProvider; import org.springframework.stereotype.Component; @@ -53,20 +49,6 @@ public class SqlBuilderSettings { private final DhisConfigurationProvider config; - /** - * Returns the setting indicating whether resource and analytics tables should be logged or - * unlogged. - * - * @return the {@link Logged} parameter. - */ - public Logged getTableLogged() { - if (config.isEnabled(ANALYTICS_TABLE_UNLOGGED)) { - return UNLOGGED; - } - - return LOGGED; - } - /** * Returns the analytics database JDBC catalog name. * diff --git a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilder.java b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilder.java index b4677fd81538..ad833fc9cdeb 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilder.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilder.java @@ -34,6 +34,7 @@ import org.apache.commons.lang3.Validate; import org.hisp.dhis.analytics.DataType; import org.hisp.dhis.db.model.Column; +import org.hisp.dhis.db.model.Database; import org.hisp.dhis.db.model.Index; import org.hisp.dhis.db.model.Table; import org.hisp.dhis.db.model.constraint.Nullable; @@ -48,6 +49,13 @@ public class ClickHouseSqlBuilder extends AbstractSqlBuilder { private static final String QUOTE = "\""; + // Database + + @Override + public Database getDatabase() { + return Database.CLICKHOUSE; + } + // Data types @Override @@ -366,16 +374,6 @@ public String createIndex(Index index) { return notSupported(); } - @Override - public String createCatalog(String connectionUrl, String username, String password) { - return notSupported(); - } - - @Override - public String dropCatalogIfExists() { - return notSupported(); - } - /** * @param name the collection name. * @param keyValues the map of key value pairs. diff --git a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/DorisSqlBuilder.java b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/DorisSqlBuilder.java index 1b01c4d1a5e9..82148772e259 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/DorisSqlBuilder.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/DorisSqlBuilder.java @@ -34,6 +34,7 @@ import org.apache.commons.lang3.Validate; import org.hisp.dhis.analytics.DataType; import org.hisp.dhis.db.model.Column; +import org.hisp.dhis.db.model.Database; import org.hisp.dhis.db.model.Index; import org.hisp.dhis.db.model.Table; import org.hisp.dhis.db.model.TablePartition; @@ -50,6 +51,13 @@ public class DorisSqlBuilder extends AbstractSqlBuilder { private static final String QUOTE = "`"; + // Database + + @Override + public Database getDatabase() { + return Database.DORIS; + } + // Data types @Override @@ -436,7 +444,12 @@ public String createIndex(Index index) { return notSupported(); } - @Override + /** + * @param connectionUrl the JDBC connection URL. + * @param username the JDBC connection username. + * @param password the JDBC connection password. + * @return a create catalog statement. + */ public String createCatalog(String connectionUrl, String username, String password) { return replace( """ @@ -457,7 +470,9 @@ public String createCatalog(String connectionUrl, String username, String passwo "driver_filename", driverFilename)); } - @Override + /** + * @return a drop catalog if exists statement. + */ public String dropCatalogIfExists() { return String.format("drop catalog if exists %s;", quote(catalog)); } diff --git a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/PostgreSqlBuilder.java b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/PostgreSqlBuilder.java index a6898fb73e19..56c6b54548a3 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/PostgreSqlBuilder.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/PostgreSqlBuilder.java @@ -32,6 +32,7 @@ import org.hisp.dhis.analytics.DataType; import org.hisp.dhis.db.model.Collation; import org.hisp.dhis.db.model.Column; +import org.hisp.dhis.db.model.Database; import org.hisp.dhis.db.model.Index; import org.hisp.dhis.db.model.Table; import org.hisp.dhis.db.model.constraint.Nullable; @@ -48,6 +49,13 @@ public class PostgreSqlBuilder extends AbstractSqlBuilder { private static final String QUOTE = "\""; + // Database + + @Override + public Database getDatabase() { + return Database.POSTGRESQL; + } + // Data types @Override @@ -424,14 +432,4 @@ unique, quote(index.getName()), quote(tableName), typeName, columns) "create %sindex %s on %s using %s(%s %s);", unique, quote(index.getName()), quote(tableName), typeName, columns, sortOrder); } - - @Override - public String createCatalog(String connectionUrl, String username, String password) { - return notSupported(); - } - - @Override - public String dropCatalogIfExists() { - return notSupported(); - } } diff --git a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/SqlBuilder.java b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/SqlBuilder.java index 5ac7c3c723bb..cc9c5d89480f 100644 --- a/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/SqlBuilder.java +++ b/dhis-2/dhis-support/dhis-support-sql/src/main/java/org/hisp/dhis/db/sql/SqlBuilder.java @@ -29,6 +29,7 @@ import java.util.Collection; import org.hisp.dhis.analytics.DataType; +import org.hisp.dhis.db.model.Database; import org.hisp.dhis.db.model.Index; import org.hisp.dhis.db.model.Table; @@ -39,6 +40,13 @@ */ public interface SqlBuilder { + // Database + + /** + * @return the {@link Database}. + */ + Database getDatabase(); + // Data types /** @@ -476,19 +484,6 @@ String ifThenElse( */ String insertIntoSelectFrom(Table intoTable, String fromTable); - /** - * @param connectionUrl the JDBC connection URL. - * @param username the JDBC connection username. - * @param password the JDBC connection password. - * @return a create catalog statement. - */ - String createCatalog(String connectionUrl, String username, String password); - - /** - * @return a drop catalog if exists statement. - */ - String dropCatalogIfExists(); - /** Enumeration of time units. */ enum DateUnit { DAYS, diff --git a/dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/database/HibernateDatabaseInfoProvider.java b/dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/database/HibernateDatabaseInfoProvider.java index 94e8f9a913bb..df01cc261243 100644 --- a/dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/database/HibernateDatabaseInfoProvider.java +++ b/dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/database/HibernateDatabaseInfoProvider.java @@ -136,7 +136,7 @@ public void init() { .name(internalInfo.getDatabase()) .user(StringUtils.defaultIfEmpty(internalInfo.getUser(), user)) .url(url) - .spatialSupport(spatialSupport) + .spatialSupport(true) // Always true, for backwards compatibility .databaseVersion(internalInfo.getVersion()) .build(); } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/JobCreationHelperForProduction.java b/dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/util/ListBuilder.java similarity index 57% rename from dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/JobCreationHelperForProduction.java rename to dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/util/ListBuilder.java index 7eb431b13b48..fccce9c5c4bd 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/scheduling/JobCreationHelperForProduction.java +++ b/dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/util/ListBuilder.java @@ -25,39 +25,42 @@ * (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.scheduling; - -import java.io.InputStream; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.hisp.dhis.feedback.ConflictException; -import org.hisp.dhis.fileresource.FileResourceService; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.MimeType; - -/** - * @author Morten Svanæs - */ -@Slf4j -@RequiredArgsConstructor -@Service -@Profile("!test") -public class JobCreationHelperForProduction implements JobCreationHelper { - - private final JobConfigurationStore jobConfigurationStore; - private final FileResourceService fileResourceService; - - @Transactional - public String create(JobConfiguration config) throws ConflictException { - return createFromConfig(config, jobConfigurationStore); +package org.hisp.dhis.system.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** Builder of immutable lists. */ +public class ListBuilder { + private final List list; + + public ListBuilder() { + list = new ArrayList<>(); + } + + public ListBuilder(List initial) { + list = new ArrayList<>(initial); + } + + public final ListBuilder addAll(List items) { + this.list.addAll(items); + return this; + } + + @SafeVarargs + public final ListBuilder add(T... items) { + this.list.addAll(Arrays.asList(items)); + return this; + } + + public final ListBuilder add(T item) { + this.list.add(item); + return this; } - @Transactional - public String create(JobConfiguration config, MimeType contentType, InputStream content) - throws ConflictException { - return createFromConfigAndInputStream( - config, contentType, content, jobConfigurationStore, fileResourceService); + public List build() { + return Collections.unmodifiableList(list); } } diff --git a/dhis-2/dhis-support/dhis-support-system/src/test/java/org/hisp/dhis/system/util/ListBuilderTest.java b/dhis-2/dhis-support/dhis-support-system/src/test/java/org/hisp/dhis/system/util/ListBuilderTest.java new file mode 100644 index 000000000000..6e5ae1c44747 --- /dev/null +++ b/dhis-2/dhis-support/dhis-support-system/src/test/java/org/hisp/dhis/system/util/ListBuilderTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.system.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class ListBuilderTest { + @Test + void testAdd() { + List actual = + new ListBuilder().add("one").addAll(List.of("two", "three")).build(); + + List expected = List.of("one", "two", "three"); + + assertEquals(expected, actual); + } + + @Test + void testAddWithInitial() { + List actual = + new ListBuilder(List.of("one")).addAll(List.of("two", "three")).build(); + + List expected = List.of("one", "two", "three"); + + assertEquals(expected, actual); + } +} diff --git a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/TestBase.java b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/TestBase.java index dc740f453a01..f5e549b9218a 100644 --- a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/TestBase.java +++ b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/TestBase.java @@ -78,6 +78,7 @@ import org.hisp.dhis.category.CategoryOptionGroup; import org.hisp.dhis.category.CategoryOptionGroupSet; import org.hisp.dhis.category.CategoryService; +import org.hisp.dhis.common.BaseIdentifiableObject; import org.hisp.dhis.common.CodeGenerator; import org.hisp.dhis.common.DataDimensionType; import org.hisp.dhis.common.DeliveryChannel; @@ -185,6 +186,7 @@ import org.hisp.dhis.setting.SessionUserSettings; import org.hisp.dhis.sqlview.SqlView; import org.hisp.dhis.sqlview.SqlViewType; +import org.hisp.dhis.test.api.TestCategoryMetadata; import org.hisp.dhis.test.utils.Dxf2NamespaceResolver; import org.hisp.dhis.test.utils.RelationshipUtils; import org.hisp.dhis.trackedentity.TrackedEntity; @@ -303,6 +305,7 @@ protected void initServices() { // ------------------------------------------------------------------------- // Convenience methods // ------------------------------------------------------------------------- + public User getCurrentUser() { return userService.getUserByUsername(CurrentUserUtil.getCurrentUsername()); } @@ -996,23 +999,19 @@ public static DataEntryForm createDataEntryForm(char uniqueCharacter, String htm public static OrganisationUnit createOrganisationUnit(char uniqueCharacter) { OrganisationUnit unit = new OrganisationUnit(); unit.setAutoFields(); - unit.setUid(BASE_OU_UID + uniqueCharacter); unit.setName("OrganisationUnit" + uniqueCharacter); unit.setShortName("OrganisationUnitShort" + uniqueCharacter); unit.setCode("OrganisationUnitCode" + uniqueCharacter); unit.setOpeningDate(date); unit.setComment("Comment" + uniqueCharacter); - // unit.getSharing().setPublicAccess("--------"); - + unit.updatePath(); return unit; } public static OrganisationUnit createOrganisationUnit(char uniqueCharacter, Geometry geometry) { OrganisationUnit unit = createOrganisationUnit(uniqueCharacter); - unit.setGeometry(geometry); - return unit; } @@ -1025,37 +1024,36 @@ public static OrganisationUnit createOrganisationUnit( OrganisationUnit unit = createOrganisationUnit(uniqueCharacter); unit.setParent(parent); parent.getChildren().add(unit); - + unit.updatePath(); return unit; } /** + * Deprecated, use {@code createOrganisationUnit(char,OrganisationUnit)}. + * * @param name The name, short name and code of the organisation unit. */ public static OrganisationUnit createOrganisationUnit(String name) { - OrganisationUnit unit = new OrganisationUnit(); - unit.setAutoFields(); - + OrganisationUnit unit = createOrganisationUnit('Y'); unit.setUid(CodeGenerator.generateUid()); unit.setName(name); unit.setShortName(name); unit.setCode(name); - unit.setOpeningDate(date); unit.setComment("Comment " + name); - return unit; } /** + * Deprecated, use {@code createOrganisationUnit(char,OrganisationUnit)}. + * * @param name The name, short name and code of the organisation unit. * @param parent The parent. */ public static OrganisationUnit createOrganisationUnit(String name, OrganisationUnit parent) { OrganisationUnit unit = createOrganisationUnit(name); - unit.setParent(parent); parent.getChildren().add(unit); - + unit.updatePath(); return unit; } @@ -1065,12 +1063,10 @@ public static OrganisationUnit createOrganisationUnit(String name, OrganisationU public static OrganisationUnitGroup createOrganisationUnitGroup(char uniqueCharacter) { OrganisationUnitGroup group = new OrganisationUnitGroup(); group.setAutoFields(); - group.setUid(BASE_UID + uniqueCharacter); group.setName("OrganisationUnitGroup" + uniqueCharacter); group.setShortName("OrganisationUnitGroupShort" + uniqueCharacter); group.setCode("OrganisationUnitGroupCode" + uniqueCharacter); - return group; } @@ -1080,13 +1076,11 @@ public static OrganisationUnitGroup createOrganisationUnitGroup(char uniqueChara public static OrganisationUnitGroupSet createOrganisationUnitGroupSet(char uniqueCharacter) { OrganisationUnitGroupSet groupSet = new OrganisationUnitGroupSet(); groupSet.setAutoFields(); - groupSet.setName("OrganisationUnitGroupSet" + uniqueCharacter); groupSet.setShortName("OrganisationUnitGroupSet" + uniqueCharacter); groupSet.setCode("OrganisationUnitGroupSetCode" + uniqueCharacter); groupSet.setDescription("Description" + uniqueCharacter); groupSet.setCompulsory(true); - return groupSet; } @@ -3047,4 +3041,68 @@ public static User createRandomAdminUserWithEntityManager(EntityManager entityMa return user; } + + protected TestCategoryMetadata setupCategoryMetadata() { + // 8 category options + CategoryOption co1A = createCategoryOption("1A", CodeGenerator.generateUid()); + CategoryOption co1B = createCategoryOption("1B", CodeGenerator.generateUid()); + CategoryOption co2A = createCategoryOption("2A", CodeGenerator.generateUid()); + CategoryOption co2B = createCategoryOption("2B", CodeGenerator.generateUid()); + CategoryOption co3A = createCategoryOption("3A", CodeGenerator.generateUid()); + CategoryOption co3B = createCategoryOption("3B", CodeGenerator.generateUid()); + CategoryOption co4A = createCategoryOption("4A", CodeGenerator.generateUid()); + CategoryOption co4B = createCategoryOption("4B", CodeGenerator.generateUid()); + categoryService.addCategoryOption(co1A); + categoryService.addCategoryOption(co1B); + categoryService.addCategoryOption(co2A); + categoryService.addCategoryOption(co2B); + categoryService.addCategoryOption(co3A); + categoryService.addCategoryOption(co3B); + categoryService.addCategoryOption(co4A); + categoryService.addCategoryOption(co4B); + + // 4 categories (each with 2 category options) + Category cat1 = createCategory('1', co1A, co1B); + Category cat2 = createCategory('2', co2A, co2B); + Category cat3 = createCategory('3', co3A, co3B); + Category cat4 = createCategory('4', co4A, co4B); + categoryService.addCategory(cat1); + categoryService.addCategory(cat2); + categoryService.addCategory(cat3); + categoryService.addCategory(cat4); + + CategoryCombo cc1 = createCategoryCombo('1', cat1, cat2); + CategoryCombo cc2 = createCategoryCombo('2', cat3, cat4); + categoryService.addCategoryCombo(cc1); + categoryService.addCategoryCombo(cc2); + + categoryService.generateOptionCombos(cc1); + categoryService.generateOptionCombos(cc2); + + CategoryOptionCombo coc1A2A = getCocWithOptions("1A", "2A"); + CategoryOptionCombo coc1B2B = getCocWithOptions("1A", "2B"); + CategoryOptionCombo coc3A4A = getCocWithOptions("3A", "4A"); + CategoryOptionCombo coc3A4B = getCocWithOptions("3A", "4B"); + + return new TestCategoryMetadata( + cc1, cc2, cat1, cat2, cat3, cat4, co1A, co1B, co2A, co2B, co3A, co3B, co4A, co4B, coc1A2A, + coc1B2B, coc3A4A, coc3A4B); + } + + private CategoryOptionCombo getCocWithOptions(String co1, String co2) { + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + return allCategoryOptionCombos.stream() + .filter( + coc -> { + Set categoryOptions = + coc.getCategoryOptions().stream() + .map(BaseIdentifiableObject::getName) + .collect(Collectors.toSet()); + return categoryOptions.containsAll(List.of(co1, co2)); + }) + .toList() + .get(0); + } } diff --git a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/api/TestCategoryMetadata.java b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/api/TestCategoryMetadata.java new file mode 100644 index 000000000000..c2ac011e2030 --- /dev/null +++ b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/api/TestCategoryMetadata.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.test.api; + +import org.hisp.dhis.category.Category; +import org.hisp.dhis.category.CategoryCombo; +import org.hisp.dhis.category.CategoryOption; +import org.hisp.dhis.category.CategoryOptionCombo; + +public record TestCategoryMetadata( + CategoryCombo cc1, + CategoryCombo cc2, + Category c1, + Category c2, + Category c3, + Category c4, + CategoryOption co1, + CategoryOption co2, + CategoryOption co3, + CategoryOption co4, + CategoryOption co5, + CategoryOption co6, + CategoryOption co7, + CategoryOption co8, + CategoryOptionCombo coc1, + CategoryOptionCombo coc2, + CategoryOptionCombo coc3, + CategoryOptionCombo coc4) {} diff --git a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/config/H2TestConfig.java b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/config/H2TestConfig.java index 667bf165c1e1..264d591d766a 100644 --- a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/config/H2TestConfig.java +++ b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/config/H2TestConfig.java @@ -31,7 +31,7 @@ import java.sql.SQLException; import javax.sql.DataSource; import org.hisp.dhis.datasource.DatabasePoolUtils; -import org.hisp.dhis.datasource.model.PoolConfig; +import org.hisp.dhis.datasource.model.DbPoolConfig; import org.hisp.dhis.external.conf.ConfigurationKey; import org.hisp.dhis.external.conf.DhisConfigurationProvider; import org.hisp.dhis.test.h2.H2SqlFunction; @@ -68,7 +68,7 @@ public DataSource actualDataSource(DhisConfigurationProvider config) throws SQLException, PropertyVetoException { String dbPoolType = config.getProperty(ConfigurationKey.DB_POOL_TYPE); - PoolConfig.PoolConfigBuilder builder = PoolConfig.builder(); + DbPoolConfig.DbPoolConfigBuilder builder = DbPoolConfig.builder(); builder.dhisConfig(config); builder.dbPoolType(dbPoolType); diff --git a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/utils/Assertions.java b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/utils/Assertions.java index 043f4389d685..b072a24aa164 100644 --- a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/utils/Assertions.java +++ b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/utils/Assertions.java @@ -69,6 +69,24 @@ public static void assertContainsOnly(Collection expected, Collection assertContainsOnly(expected, actual, "assertContainsOnly found mismatch"); } + /** + * Asserts that the given collection contains exactly the given items in any order. Collections + * will be mapped by {@code map} before passing it to {@link #assertContainsOnly(Collection, + * Collection)}. + * + * @param expected the expected items. + * @param actual the actual collection. + * @param map map the items of expected and actual collections to the type that will be used for + * comparison + */ + public static void assertContainsOnly( + Collection expected, Collection actual, Function map) { + assertContainsOnly( + expected.stream().map(map).toList(), + actual.stream().map(map).toList(), + "assertContainsOnly found mismatch"); + } + /** * Asserts that the given collection contains exactly the given items in any order. * diff --git a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/webapi/json/domain/JsonProperty.java b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/webapi/json/domain/JsonProperty.java index e3bb6c08751d..27275dff55cf 100644 --- a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/webapi/json/domain/JsonProperty.java +++ b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/webapi/json/domain/JsonProperty.java @@ -90,6 +90,10 @@ default boolean isSimple() { return getBoolean("simple").booleanValue(); } + default boolean isSortable() { + return getBoolean("sortable").booleanValue(); + } + default boolean isRequired() { return getBoolean("required").booleanValue(); } diff --git a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/webapi/json/domain/JsonUserRole.java b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/webapi/json/domain/JsonUserRole.java new file mode 100644 index 000000000000..d468c6623a79 --- /dev/null +++ b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/webapi/json/domain/JsonUserRole.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2004-2025, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.test.webapi.json.domain; + +import org.hisp.dhis.jsontree.JsonList; +import org.hisp.dhis.jsontree.JsonString; + +public interface JsonUserRole extends JsonIdentifiableObject { + default JsonList getUsers() { + return getList("users", JsonUser.class); + } + + default JsonList getAuthorities() { + return getList("authorities", JsonString.class); + } +} diff --git a/dhis-2/dhis-support/pom.xml b/dhis-2/dhis-support/pom.xml index 5366126ee289..df09fcd878e7 100644 --- a/dhis-2/dhis-support/pom.xml +++ b/dhis-2/dhis-support/pom.xml @@ -17,11 +17,11 @@ dhis-support-db-migration dhis-support-test dhis-support-external + dhis-support-sql dhis-support-hibernate dhis-support-audit dhis-support-system dhis-support-jdbc - dhis-support-sql dhis-support-expression-parser dhis-support-artemis dhis-support-cache-invalidation diff --git a/dhis-2/dhis-test-e2e/pom.xml b/dhis-2/dhis-test-e2e/pom.xml index 5aef4f352e51..0b952e1fe3f4 100644 --- a/dhis-2/dhis-test-e2e/pom.xml +++ b/dhis-2/dhis-test-e2e/pom.xml @@ -9,7 +9,7 @@ .. UTF-8 - 2.43.0 + 2.44.1 3.13.0 3.5.2 1.4.0 @@ -22,7 +22,7 @@ 1.5 5.2.1 1.0.2 - 5.9 + 5.10 1.0.12 4.4 3.17.0 diff --git a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/datastore/UserDatastoreUpdateTest.java b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/datastore/UserDatastoreUpdateTest.java index 55cb143843d6..0edc8d2793bd 100644 --- a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/datastore/UserDatastoreUpdateTest.java +++ b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/datastore/UserDatastoreUpdateTest.java @@ -91,7 +91,8 @@ void testUpdateEntry_RootWithNonNullValue() { userDatastoreActions .update( "/" + NAMESPACE + "/key7", - toJsonObject(""" + toJsonObject( + """ {"a": 11} """)) .validate() @@ -101,7 +102,8 @@ void testUpdateEntry_RootWithNonNullValue() { userDatastoreActions .update( "/" + NAMESPACE + "/key7", - toJsonObject(""" + toJsonObject( + """ {"a": 99} """)) .validate() @@ -122,7 +124,8 @@ void testUpdateEntry_PathWithNullValue() { userDatastoreActions .update( "/" + NAMESPACE + "/key6", - toJsonObject(""" + toJsonObject( + """ {"a": 42} """)) .validate() @@ -146,7 +149,8 @@ void testUpdateEntry_PathWithNonNullValue() { userDatastoreActions .update( "/" + NAMESPACE + "/key5", - toJsonObject(""" + toJsonObject( + """ {"a": 11} """)) .validate() @@ -277,7 +281,8 @@ void testUpdateEntry_RollPathValueIsNull() { userDatastoreActions .update( "/" + NAMESPACE + "/key55", - toJsonObject(""" + toJsonObject( + """ {"a": null} """)) .validate() @@ -304,7 +309,8 @@ void testUpdateEntry_RollPathValueIsUndefined() { userDatastoreActions .update( "/" + NAMESPACE + "/key24", - toJsonObject(""" + toJsonObject( + """ {"a": null} """)) .validate() @@ -332,7 +338,8 @@ void testUpdateEntry_RollPathValueIsArray() { userDatastoreActions .update( "/" + NAMESPACE + "/key2", - toJsonObject(""" + toJsonObject( + """ {"a":{"b":[]}} """)) .validate() @@ -399,7 +406,8 @@ void testUpdateEntry_RollPathValueIsOther() { userDatastoreActions .update( "/" + NAMESPACE + "/key3", - toJsonObject(""" + toJsonObject( + """ {"a":[{}]} """)) .validate() diff --git a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/helpers/extensions/MetadataSetupExtension.java b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/helpers/extensions/MetadataSetupExtension.java index 07079ff7b6d7..a87aa36ee979 100644 --- a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/helpers/extensions/MetadataSetupExtension.java +++ b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/helpers/extensions/MetadataSetupExtension.java @@ -39,8 +39,10 @@ import org.hisp.dhis.test.e2e.Constants; import org.hisp.dhis.test.e2e.TestRunStorage; import org.hisp.dhis.test.e2e.actions.LoginActions; +import org.hisp.dhis.test.e2e.actions.MaintenanceActions; import org.hisp.dhis.test.e2e.actions.UserActions; import org.hisp.dhis.test.e2e.actions.metadata.MetadataActions; +import org.hisp.dhis.test.e2e.helpers.QueryParamsBuilder; import org.hisp.dhis.test.e2e.helpers.config.TestConfiguration; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; @@ -55,6 +57,7 @@ public class MetadataSetupExtension private static final Map createdData = new LinkedHashMap<>(); private static final Logger logger = LogManager.getLogger(MetadataSetupExtension.class.getName()); + private static final MaintenanceActions maintenanceApiActions = new MaintenanceActions(); @Override public void beforeAll(ExtensionContext context) { @@ -130,10 +133,11 @@ public void close() { if (TestConfiguration.get().shouldCleanUp()) { TestCleanUp testCleanUp = new TestCleanUp(); - iterateCreatedData( - id -> { - testCleanUp.deleteEntity(createdData.get(id), id); - }); + iterateCreatedData(id -> testCleanUp.deleteEntity(createdData.get(id), id)); + // clean-up category option combos, which are autogenerated (not tracked by e2e tests) + maintenanceApiActions + .post("categoryOptionComboUpdate", new QueryParamsBuilder().build()) + .validateStatus(204); } } } diff --git a/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/merge/CategoryOptionComboMergeTest.java b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/merge/CategoryOptionComboMergeTest.java new file mode 100644 index 000000000000..5c440982bbee --- /dev/null +++ b/dhis-2/dhis-test-e2e/src/test/java/org/hisp/dhis/merge/CategoryOptionComboMergeTest.java @@ -0,0 +1,1303 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.merge; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.hasSize; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import io.restassured.response.ValidatableResponse; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import lombok.extern.slf4j.Slf4j; +import org.awaitility.Awaitility; +import org.hisp.dhis.ApiTest; +import org.hisp.dhis.test.e2e.actions.LoginActions; +import org.hisp.dhis.test.e2e.actions.RestApiActions; +import org.hisp.dhis.test.e2e.actions.UserActions; +import org.hisp.dhis.test.e2e.actions.metadata.MetadataActions; +import org.hisp.dhis.test.e2e.dto.ApiResponse; +import org.hisp.dhis.test.e2e.helpers.JsonObjectBuilder; +import org.hisp.dhis.test.e2e.helpers.JsonParserUtils; +import org.hisp.dhis.test.e2e.helpers.QueryParamsBuilder; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@Slf4j +class CategoryOptionComboMergeTest extends ApiTest { + + private RestApiActions categoryOptionComboApiActions; + private RestApiActions dataElementApiActions; + private RestApiActions minMaxActions; + private MetadataActions metadataActions; + private RestApiActions maintenanceApiActions; + private RestApiActions dataValueSetActions; + private UserActions userActions; + private LoginActions loginActions; + private String sourceUid1; + private String sourceUid2; + private String targetUid; + private String randomCocUid1; + private String randomCocUid2; + private String mergeUserId; + + @BeforeAll + public void before() { + userActions = new UserActions(); + loginActions = new LoginActions(); + dataElementApiActions = new RestApiActions("dataElements"); + minMaxActions = new RestApiActions("minMaxDataElements"); + categoryOptionComboApiActions = new RestApiActions("categoryOptionCombos"); + metadataActions = new MetadataActions(); + maintenanceApiActions = new RestApiActions("maintenance"); + dataValueSetActions = new RestApiActions("dataValueSets"); + loginActions.loginAsSuperUser(); + + // add user with required merge auth + mergeUserId = + userActions.addUserFull( + "user", "auth", "userWithMergeAuth", "Test1234!", "F_CATEGORY_OPTION_COMBO_MERGE"); + } + + @BeforeEach + public void setup() { + loginActions.loginAsSuperUser(); + setupMetadata(); + } + + @AfterAll + public void resetSuperUserOrgUnit() { + loginActions.loginAsSuperUser(); + // reset super user to have same org unit access as setup data + addOrgUnitAccessForUser( + loginActions.getLoggedInUserId(), + "ImspTQPwCqd", + "O6uvpzGd5pu", + "g8upMTyEZGZ", + "YuQRtpLP10I"); + } + + @Test + @DisplayName( + "Valid CategoryOptionCombo merge completes successfully with all source CategoryOptionCombo refs replaced with target CategoryOptionCombo") + void validCategoryOptionComboMergeTest() { + // given + // generate category option combos + String emptyParams = new QueryParamsBuilder().build(); + maintenanceApiActions + .post("categoryOptionComboUpdate/categoryCombo/CatComUid01", emptyParams) + .validateStatus(200); + maintenanceApiActions + .post("categoryOptionComboUpdate/categoryCombo/CatComUid02", emptyParams) + .validateStatus(200); + + // get cat opt combo uids for sources and target, after generating + sourceUid1 = getCocWithOptions("1A", "2A"); + sourceUid2 = getCocWithOptions("1B", "2B"); + targetUid = getCocWithOptions("3A", "4B"); + + // confirm state before merge + ValidatableResponse preMergeState = + categoryOptionComboApiActions.get(targetUid).validateStatus(200).validate(); + + preMergeState + .body("categoryCombo", hasEntry("id", "CatComUid02")) + .body("categoryOptions", hasSize(equalTo(2))) + .body("categoryOptions", hasItem(hasEntry("id", "CatOptUid4B"))) + .body("categoryOptions", hasItem(hasEntry("id", "CatOptUid3A"))); + + // login as merge user + loginActions.loginAsUser("userWithMergeAuth", "Test1234!"); + + // when a category option combo request is submitted, deleting sources + ApiResponse response = + categoryOptionComboApiActions.post("merge", getMergeBody("DISCARD")).validateStatus(200); + + // then a success response received, sources are deleted & source references were merged + response + .validate() + .statusCode(200) + .body("httpStatus", equalTo("OK")) + .body("response.mergeReport.message", equalTo("CategoryOptionCombo merge complete")) + .body("response.mergeReport.mergeErrors", empty()) + .body("response.mergeReport.mergeType", equalTo("CategoryOptionCombo")) + .body("response.mergeReport.sourcesDeleted", hasItems(sourceUid1, sourceUid2)); + + categoryOptionComboApiActions.get(sourceUid1).validateStatus(404); + categoryOptionComboApiActions.get(sourceUid2).validateStatus(404); + ValidatableResponse postMergeState = + categoryOptionComboApiActions.get(targetUid).validateStatus(200).validate(); + + postMergeState + .body("categoryCombo", hasEntry("id", "CatComUid02")) + .body("categoryOptions", hasSize(equalTo(6))) + .body( + "categoryOptions", + hasItems( + hasEntry("id", "CatOptUid1A"), + hasEntry("id", "CatOptUid2B"), + hasEntry("id", "CatOptUid3A"), + hasEntry("id", "CatOptUid4B"), + hasEntry("id", "CatOptUid2A"), + hasEntry("id", "CatOptUid1B"))); + } + + @Test + @DisplayName( + "CategoryOptionCombo merge completes successfully with DataValues (cat opt combo) handled correctly") + void cocMergeDataValuesTest() { + // Given + // Generate category option combos + maintenanceApiActions + .post("categoryOptionComboUpdate", new QueryParamsBuilder().build()) + .validateStatus(204); + + // Get cat opt combo uids for sources and target, after generating + sourceUid1 = getCocWithOptions("1A", "2A"); + sourceUid2 = getCocWithOptions("1A", "2B"); + targetUid = getCocWithOptions("3A", "4A"); + randomCocUid1 = getCocWithOptions("1B", "2A"); + randomCocUid2 = getCocWithOptions("1B", "2B"); + + addOrgUnitAccessForUser(loginActions.getLoggedInUserId(), "OrgUnitUid1"); + addOrgUnitAccessForUser(mergeUserId, "OrgUnitUid1"); + + // Add data values + addDataValuesCoc(); + + // Wait 2 seconds, so that lastUpdated values have a different value, + // which is crucial for choosing which data values to keep/delete + Awaitility.await().pollDelay(2, TimeUnit.SECONDS).until(() -> true); + + // Update some data values, ensures different 'lastUpdated' values for duplicate logic + updateDataValuesCoc(); + + // Confirm Data Value state before merge + ValidatableResponse preMergeState = + dataValueSetActions + .get(getDataValueSetQueryParams("OrgUnitUid1")) + .validateStatus(200) + .validate(); + + preMergeState.body("dataValues", hasSize(14)); + Set uniqueDates = + new HashSet<>(preMergeState.extract().jsonPath().getList("dataValues.lastUpdated")); + assertTrue(uniqueDates.size() > 1, "There should be more than 1 unique date present"); + + // Login as merge user + loginActions.loginAsUser("userWithMergeAuth", "Test1234!"); + + // When a merge request using the data merge strategy 'LAST_UPDATED' is submitted + ApiResponse response = + categoryOptionComboApiActions + .post("merge", getMergeBody("LAST_UPDATED")) + .validateStatus(200); + + // Then a success response received, sources are deleted & source references were merged + response + .validate() + .statusCode(200) + .body("httpStatus", equalTo("OK")) + .body("response.mergeReport.message", equalTo("CategoryOptionCombo merge complete")) + .body("response.mergeReport.mergeErrors", empty()) + .body("response.mergeReport.mergeType", equalTo("CategoryOptionCombo")) + .body("response.mergeReport.sourcesDeleted", hasItems(sourceUid1, sourceUid2)); + + // And sources should no longer exist + categoryOptionComboApiActions.get(sourceUid1).validateStatus(404); + categoryOptionComboApiActions.get(sourceUid2).validateStatus(404); + + // And last updated duplicates are kept and earlier duplicates deleted + ValidatableResponse postMergeState = + dataValueSetActions + .get(getDataValueSetQueryParams("OrgUnitUid1")) + .validateStatus(200) + .validate(); + + postMergeState.body("dataValues", hasSize(8)); + + // Check for expected values + List datValues = postMergeState.extract().jsonPath().getList("dataValues.value"); + assertTrue(datValues.contains("UPDATED source 1 DV 3 - duplicate later - KEEP")); + assertTrue(datValues.contains("UPDATED source 2 DV 2 - duplicate later - KEEP")); + assertTrue(datValues.contains("UPDATED target DV 4 - duplicate later - KEEP")); + + assertFalse(datValues.contains("source 1, DV 2 - duplicate earlier - REMOVE")); + assertFalse(datValues.contains("source 1, DV 4 - duplicate earlier - REMOVE")); + assertFalse(datValues.contains("source 2, DV 3 - duplicate earlier - REMOVE")); + assertFalse(datValues.contains("source 2, DV 4 - duplicate earlier - REMOVE")); + assertFalse(datValues.contains("target DV 1 - duplicate earlier - REMOVE")); + assertFalse(datValues.contains("target DV 2 - duplicate earlier - REMOVE")); + + Set dvCocs = + new HashSet<>( + postMergeState.extract().jsonPath().getList("dataValues.categoryOptionCombo")); + assertTrue(dvCocs.contains(targetUid), "Target COC is present"); + assertFalse(dvCocs.contains(sourceUid1), "Source COC 1 should not be present"); + assertFalse(dvCocs.contains(sourceUid2), "Source COC 2 should not be present"); + } + + @Test + @DisplayName( + "CategoryOptionCombo merge completes successfully with DataValues (attr opt combo) handled correctly") + void aocMergeDataValuesTest() { + // Given + // Generate category option combos + maintenanceApiActions + .post("categoryOptionComboUpdate", new QueryParamsBuilder().build()) + .validateStatus(204); + + // Get cat opt combo uids for sources and target, after generating + sourceUid1 = getCocWithOptions("1A", "2A"); + sourceUid2 = getCocWithOptions("1A", "2B"); + targetUid = getCocWithOptions("3A", "4A"); + randomCocUid1 = getCocWithOptions("1B", "2A"); + randomCocUid2 = getCocWithOptions("1B", "2B"); + + addOrgUnitAccessForUser(loginActions.getLoggedInUserId(), "OrgUnitUid2"); + addOrgUnitAccessForUser(mergeUserId, "OrgUnitUid2"); + + // Add data values + addDataValuesAoc(); + + // Wait 2 seconds, so that lastUpdated values have a different value, + // which is crucial for choosing which data values to keep/delete + Awaitility.await().pollDelay(2, TimeUnit.SECONDS).until(() -> true); + + // Update some data values, ensures different 'lastUpdated' values for duplicate logic + updateDataValuesAoc(); + + // Confirm Data Value state before merge + ValidatableResponse preMergeState = + dataValueSetActions + .get(getDataValueSetQueryParams("OrgUnitUid2")) + .validateStatus(200) + .validate(); + + preMergeState.body("dataValues", hasSize(14)); + Set uniqueDates = + new HashSet<>(preMergeState.extract().jsonPath().getList("dataValues.lastUpdated")); + assertTrue(uniqueDates.size() > 1, "There should be more than 1 unique date present"); + + // Login as merge user + loginActions.loginAsUser("userWithMergeAuth", "Test1234!"); + + // When a merge request using the data merge strategy 'LAST_UPDATED' is submitted + ApiResponse response = + categoryOptionComboApiActions + .post("merge", getMergeBody("LAST_UPDATED")) + .validateStatus(200); + + // Then a success response received, sources are deleted & source references were merged + response + .validate() + .statusCode(200) + .body("httpStatus", equalTo("OK")) + .body("response.mergeReport.message", equalTo("CategoryOptionCombo merge complete")) + .body("response.mergeReport.mergeErrors", empty()) + .body("response.mergeReport.mergeType", equalTo("CategoryOptionCombo")) + .body("response.mergeReport.sourcesDeleted", hasItems(sourceUid1, sourceUid2)); + + // And sources should no longer exist + categoryOptionComboApiActions.get(sourceUid1).validateStatus(404); + categoryOptionComboApiActions.get(sourceUid2).validateStatus(404); + + // And last updated duplicates are kept and earlier duplicates deleted + loginActions.loginAsSuperUser(); + ValidatableResponse postMergeState = + dataValueSetActions + .get(getDataValueSetQueryParamsWithAoc("OrgUnitUid2")) + .validateStatus(200) + .validate(); + + postMergeState.body("dataValues", hasSize(6)); + + // Check for expected values + List datValues = postMergeState.extract().jsonPath().getList("dataValues.value"); + assertTrue(datValues.contains("UPDATED source 1 DV 3 - duplicate later - KEEP")); + assertTrue(datValues.contains("UPDATED source 2 DV 2 - duplicate later - KEEP")); + assertTrue(datValues.contains("UPDATED target DV 4 - duplicate later - KEEP")); + + assertFalse(datValues.contains("source 1, DV 2 - duplicate earlier - REMOVE")); + assertFalse(datValues.contains("source 1, DV 4 - duplicate earlier - REMOVE")); + assertFalse(datValues.contains("source 2, DV 3 - duplicate earlier - REMOVE")); + assertFalse(datValues.contains("source 2, DV 4 - duplicate earlier - REMOVE")); + assertFalse(datValues.contains("target DV 1 - duplicate earlier - REMOVE")); + assertFalse(datValues.contains("target DV 2 - duplicate earlier - REMOVE")); + + Set dvAocs = + new HashSet<>( + postMergeState.extract().jsonPath().getList("dataValues.attributeOptionCombo")); + assertTrue(dvAocs.contains(targetUid), "Target COC is present"); + assertFalse(dvAocs.contains(sourceUid1), "Source COC 1 should not be present"); + assertFalse(dvAocs.contains(sourceUid2), "Source COC 2 should not be present"); + } + + private void addDataValuesCoc() { + dataValueSetActions + .post( + dataValueSetImportCoc(sourceUid1, sourceUid2, targetUid, randomCocUid1, randomCocUid2), + getDataValueQueryParams()) + .validateStatus(200) + .validate() + .body("response.importCount.imported", equalTo(14)); + } + + private void addDataValuesAoc() { + dataValueSetActions + .post( + dataValueSetImportAoc(sourceUid1, sourceUid2, targetUid, randomCocUid1, randomCocUid2), + getDataValueQueryParams()) + .validateStatus(200); + } + + private void updateDataValuesCoc() { + dataValueSetActions + .post( + dataValueSetImportUpdateCoc(sourceUid1, sourceUid2, targetUid), + getDataValueQueryParams()) + .validateStatus(200) + .validate() + .body("response.importCount.updated", equalTo(4)); + } + + private void updateDataValuesAoc() { + dataValueSetActions + .post( + dataValueSetImportUpdateAoc(sourceUid1, sourceUid2, targetUid), + getDataValueQueryParams()) + .validateStatus(200) + .validate() + .body("response.importCount.updated", equalTo(4)); + } + + private QueryParamsBuilder getDataValueQueryParams() { + return new QueryParamsBuilder() + .add("async=false") + .add("dryRun=false") + .add("strategy=NEW_AND_UPDATES") + .add("preheatCache=false") + .add("dataElementIdScheme=UID") + .add("orgUnitIdScheme=UID") + .add("idScheme=UID") + .add("format=json") + .add("skipExistingCheck=false"); + } + + private String getDataValueSetQueryParams(String orgUnit) { + return new QueryParamsBuilder() + .add("orgUnit=%s") + .add("startDate=2024-01-01") + .add("endDate=2050-01-30") + .add("dataElement=deUid000001") + .build() + .formatted(orgUnit); + } + + private String getDataValueSetQueryParamsWithAoc(String orgUnit) { + return new QueryParamsBuilder() + .add("orgUnit=%s") + .add("startDate=2024-01-01") + .add("endDate=2050-01-30") + .add("dataElement=deUid000001") + .add("attributeOptionCombo=" + targetUid) + .build() + .formatted(orgUnit); + } + + private void addOrgUnitAccessForUser(String loggedInUserId, String... orgUnitUids) { + JsonArray orgUnits = new JsonArray(); + for (String orgUnit : orgUnitUids) { + orgUnits.add(JsonObjectBuilder.jsonObject().addProperty("id", orgUnit).build()); + } + JsonObject userPatch = + JsonObjectBuilder.jsonObject() + .addProperty("op", "add") + .addProperty("path", "/organisationUnits") + .addArray("value", orgUnits) + .build(); + + userActions.patch(loggedInUserId, Collections.singletonList(userPatch)).validateStatus(200); + } + + @Test + @DisplayName("CategoryOptionCombo merge fails when user has not got the required authority") + void categoryOptionComboMergeNoRequiredAuthTest() { + userActions.addUserFull("basic", "User", "basicUser", "Test1234!", "NO_AUTH"); + loginActions.loginAsUser("basicUser", "Test1234!"); + + // when + ApiResponse response = + categoryOptionComboApiActions.post("merge", getMergeBody("DISCARD")).validateStatus(403); + + // then + response + .validate() + .statusCode(403) + .body("httpStatus", equalTo("Forbidden")) + .body("status", equalTo("ERROR")) + .body( + "message", + equalTo( + "Access is denied, requires one Authority from [F_CATEGORY_OPTION_COMBO_MERGE]")); + } + + @Test + @DisplayName("Category Option Combo merge fails when min max DE DB unique key constraint met") + void dbConstraintMinMaxTest() { + // given + maintenanceApiActions + .post("categoryOptionComboUpdate", new QueryParamsBuilder().build()) + .validateStatus(204); + + // get cat opt combo uids for sources and target, after generating + sourceUid1 = getCocWithOptions("1A", "2A"); + sourceUid2 = getCocWithOptions("1B", "2B"); + targetUid = getCocWithOptions("3A", "4B"); + + String dataElement = setupDataElement(); + + setupMinMaxDataElements(sourceUid1, sourceUid2, targetUid, dataElement); + + // login as user with merge auth + loginActions.loginAsUser("userWithMergeAuth", "Test1234!"); + + // when + ApiResponse response = + categoryOptionComboApiActions.post("merge", getMergeBody("DISCARD")).validateStatus(409); + + // then + response + .validate() + .statusCode(409) + .body("httpStatus", equalTo("Conflict")) + .body("status", equalTo("ERROR")) + .body("message", containsString("ERROR: duplicate key value violates unique constraint")) + .body("message", containsString("minmaxdataelement_unique_key")); + } + + private void setupMetadata() { + metadataActions.post(metadata()).validateStatus(200); + } + + private void setupMinMaxDataElements( + String sourceUid1, String sourceUid2, String targetUid, String dataElement) { + minMaxActions.post(minMaxDataElements(sourceUid1, dataElement)); + minMaxActions.post(minMaxDataElements(sourceUid2, dataElement)); + minMaxActions.post(minMaxDataElements(targetUid, dataElement)); + } + + private String minMaxDataElements(String coc, String de) { + return """ + { + "min": 2, + "max": 11, + "generated": false, + "source": { + "id": "OrgUnitUid1" + }, + "dataElement": { + "id": "%s" + }, + "optionCombo": { + "id": "%s" + } + } + """ + .formatted(de, coc); + } + + private String setupDataElement() { + return dataElementApiActions + .post( + """ + { + "aggregationType": "DEFAULT", + "domainType": "AGGREGATE", + "name": "source 19", + "shortName": "source 19", + "displayName": "source 19", + "valueType": "TEXT" + } + """) + .validateStatus(201) + .extractUid(); + } + + private JsonObject getMergeBody(String dataMergeStrategy) { + JsonObject json = new JsonObject(); + JsonArray sources = new JsonArray(); + sources.add(sourceUid1); + sources.add(sourceUid2); + json.add("sources", sources); + json.addProperty("target", targetUid); + json.addProperty("deleteSources", true); + json.addProperty("dataMergeStrategy", dataMergeStrategy); + return json; + } + + private String getCocWithOptions(String co1, String co2) { + + return categoryOptionComboApiActions + .get( + new QueryParamsBuilder() + .addAll("filter=name:like:%s".formatted(co1), "filter=name:like:%s".formatted(co2))) + .validate() + .extract() + .jsonPath() + .get("categoryOptionCombos[0].id") + .toString(); + } + + private String metadata() { + return """ + { + "categoryOptions": [ + { + "id": "CatOptUid1A", + "name": "cat opt 1A", + "shortName": "cat opt 1A", + "organisationUnits": [ + { + "id": "OrgUnitUid1" + }, + { + "id": "OrgUnitUid2" + } + ] + }, + { + "id": "CatOptUid1B", + "name": "cat opt 1B", + "shortName": "cat opt 1B", + "organisationUnits": [ + { + "id": "OrgUnitUid1" + }, + { + "id": "OrgUnitUid2" + } + ] + }, + { + "id": "CatOptUid2A", + "name": "cat opt 2A", + "shortName": "cat opt 2A", + "organisationUnits": [ + { + "id": "OrgUnitUid1" + }, + { + "id": "OrgUnitUid2" + } + ] + }, + { + "id": "CatOptUid2B", + "name": "cat opt 2B", + "shortName": "cat opt 2B", + "organisationUnits": [ + { + "id": "OrgUnitUid1" + }, + { + "id": "OrgUnitUid2" + } + ] + }, + { + "id": "CatOptUid3A", + "name": "cat opt 3A", + "shortName": "cat opt 3A", + "organisationUnits": [ + { + "id": "OrgUnitUid1" + }, + { + "id": "OrgUnitUid2" + } + ] + }, + { + "id": "CatOptUid3B", + "name": "cat opt 3B", + "shortName": "cat opt 3B", + "organisationUnits": [ + { + "id": "OrgUnitUid1" + }, + { + "id": "OrgUnitUid2" + } + ] + }, + { + "id": "CatOptUid4A", + "name": "cat opt 4A", + "shortName": "cat opt 4A", + "organisationUnits": [ + { + "id": "OrgUnitUid1" + }, + { + "id": "OrgUnitUid2" + } + ] + }, + { + "id": "CatOptUid4B", + "name": "cat opt 4B", + "shortName": "cat opt 4B", + "organisationUnits": [ + { + "id": "OrgUnitUid1" + }, + { + "id": "OrgUnitUid2" + } + ] + } + ], + "categories": [ + { + "id": "CategoUid01", + "name": "cat 1", + "shortName": "cat 1", + "dataDimensionType": "DISAGGREGATION", + "categoryOptions": [ + { + "id": "CatOptUid1A" + }, + { + "id": "CatOptUid1B" + } + ] + }, + { + "id": "CategoUid02", + "name": "cat 2", + "shortName": "cat 2", + "dataDimensionType": "DISAGGREGATION", + "categoryOptions": [ + { + "id": "CatOptUid2A" + }, + { + "id": "CatOptUid2B" + } + ] + }, + { + "id": "CategoUid03", + "name": "cat 3", + "shortName": "cat 3", + "dataDimensionType": "DISAGGREGATION", + "categoryOptions": [ + { + "id": "CatOptUid3A" + }, + { + "id": "CatOptUid3B" + } + ] + }, + { + "id": "CategoUid04", + "name": "cat 4", + "shortName": "cat 4", + "dataDimensionType": "DISAGGREGATION", + "categoryOptions": [ + { + "id": "CatOptUid4A" + }, + { + "id": "CatOptUid4B" + } + ] + } + ], + "organisationUnits": [ + { + "id": "OrgUnitUid1", + "name": "org 1", + "shortName": "org 1", + "openingDate": "2023-06-15", + "parent": { + "id": "DiszpKrYNg8" + } + }, + { + "id": "OrgUnitUid2", + "name": "org 2", + "shortName": "org 2", + "openingDate": "2024-06-15", + "parent": { + "id": "DiszpKrYNg8" + } + }, + { + "id": "OrgUnitUid3", + "name": "org 3", + "shortName": "org 3", + "openingDate": "2023-09-15", + "parent": { + "id": "DiszpKrYNg8" + } + }, + { + "id": "OrgUnitUid4", + "name": "org 4", + "shortName": "org 4", + "openingDate": "2023-06-25", + "parent": { + "id": "DiszpKrYNg8" + } + } + ], + "categoryOptionGroups": [ + { + "id": "CatOptGrp01", + "name": "cog 1", + "shortName": "cog 1", + "dataDimensionType": "DISAGGREGATION", + "categoryOptions": [ + { + "id": "CatOptUid1A" + }, + { + "id": "CatOptUid1B" + } + ] + }, + { + "id": "CatOptGrp02", + "name": "cog 2", + "shortName": "cog 2", + "dataDimensionType": "DISAGGREGATION", + "categoryOptions": [ + { + "id": "CatOptUid2A" + }, + { + "id": "CatOptUid2B" + } + ] + }, + { + "id": "CatOptGrp03", + "name": "cog 3", + "shortName": "cog 3", + "dataDimensionType": "DISAGGREGATION", + "categoryOptions": [ + { + "id": "CatOptUid3A" + }, + { + "id": "CatOptUid3B" + } + ] + }, + { + "id": "CatOptGrp04", + "name": "cog 4", + "shortName": "cog 4", + "dataDimensionType": "DISAGGREGATION", + "categoryOptions": [ + { + "id": "CatOptUid4A" + }, + { + "id": "CatOptUid4B" + } + ] + } + ], + "categoryCombos": [ + { + "id": "CatComUid01", + "name": "cat combo 1", + "dataDimensionType": "DISAGGREGATION", + "categories": [ + { + "id": "CategoUid01" + }, + { + "id": "CategoUid02" + } + ] + }, + { + "id": "CatComUid02", + "name": "cat combo 2", + "dataDimensionType": "DISAGGREGATION", + "categories": [ + { + "id": "CategoUid03" + }, + { + "id": "CategoUid04" + } + ] + } + ], + "dataElements": [ + { + "id": "deUid000001", + "aggregationType": "DEFAULT", + "domainType": "AGGREGATE", + "name": "DE for DVs", + "shortName": "DE for DVs", + "valueType": "TEXT" + } + ] + } + """; + } + + private JsonObject dataValueSetImportCoc( + String source1Coc, + String source2Coc, + String targetCoc, + String randomCoc1, + String randomCoc2) { + return JsonParserUtils.toJsonObject( + """ + { + "dataValues": [ + { + "dataElement": "deUid000001", + "period": "202405", + "orgUnit": "OrgUnitUid1", + "categoryOptionCombo": "%s", + "attributeOptionCombo": "HllvX50cXC0", + "value": "source 1, DV 1 - non duplicate earlier - KEEP", + "comment": "source 1, DV 1 - non duplicate earlier - KEEP" + }, + { + "dataElement": "deUid000001", + "period": "202408", + "orgUnit": "OrgUnitUid1", + "categoryOptionCombo": "%s", + "attributeOptionCombo": "HllvX50cXC0", + "value": "source 1, DV 2 - duplicate earlier - REMOVE", + "comment": "source 1, DV 2 - duplicate earlier - REMOVE" + }, + { + "dataElement": "deUid000001", + "period": "202409", + "orgUnit": "OrgUnitUid1", + "categoryOptionCombo": "%s", + "attributeOptionCombo": "HllvX50cXC0", + "value": "source 1, DV 3 - duplicate later - KEEP", + "comment": "source 1, DV 3 - duplicate later - KEEP" + }, + { + "dataElement": "deUid000001", + "period": "202407", + "orgUnit": "OrgUnitUid1", + "categoryOptionCombo": "%s", + "attributeOptionCombo": "HllvX50cXC0", + "value": "source 1, DV 4 - duplicate earlier - REMOVE", + "comment": "source 1, DV 4 - duplicate earlier - REMOVE" + }, + { + "dataElement": "deUid000001", + "period": "202410", + "orgUnit": "OrgUnitUid1", + "categoryOptionCombo": "%s", + "attributeOptionCombo": "HllvX50cXC0", + "value": "source 2, DV 1 - non duplicate later - KEEP", + "comment": "source 2, DV 1 - non duplicate later - KEEP" + }, + { + "dataElement": "deUid000001", + "period": "202408", + "orgUnit": "OrgUnitUid1", + "categoryOptionCombo": "%s", + "attributeOptionCombo": "HllvX50cXC0", + "value": "source 2, DV 2 - duplicate later - KEEP", + "comment": "source 2, DV 2 - duplicate later - KEEP" + }, + { + "dataElement": "deUid000001", + "period": "202409", + "orgUnit": "OrgUnitUid1", + "categoryOptionCombo": "%s", + "attributeOptionCombo": "HllvX50cXC0", + "value": "source 2, DV 3 - duplicate earlier - REMOVE", + "comment": "source 2, DV 3 - duplicate earlier - REMOVE" + }, + { + "dataElement": "deUid000001", + "period": "202407", + "orgUnit": "OrgUnitUid1", + "categoryOptionCombo": "%s", + "attributeOptionCombo": "HllvX50cXC0", + "value": "source 2, DV 4 - duplicate earlier - REMOVE", + "comment": "source 2, DV 4 - duplicate earlier - REMOVE" + }, + { + "dataElement": "deUid000001", + "period": "202408", + "orgUnit": "OrgUnitUid1", + "categoryOptionCombo": "%s", + "attributeOptionCombo": "HllvX50cXC0", + "value": "target DV 1 - duplicate earlier - REMOVE", + "comment": "target DV 1 - duplicate earlier - REMOVE" + }, + { + "dataElement": "deUid000001", + "period": "202409", + "orgUnit": "OrgUnitUid1", + "categoryOptionCombo": "%s", + "attributeOptionCombo": "HllvX50cXC0", + "value": "target DV 2 - duplicate earlier - REMOVE", + "comment": "target DV 2 - duplicate earlier - REMOVE" + }, + { + "dataElement": "deUid000001", + "period": "202403", + "orgUnit": "OrgUnitUid1", + "categoryOptionCombo": "%s", + "attributeOptionCombo": "HllvX50cXC0", + "value": "target DV 3 - not impacted - KEEP", + "comment": "target DV 3 - not impacted - KEEP" + }, + { + "dataElement": "deUid000001", + "period": "202407", + "orgUnit": "OrgUnitUid1", + "categoryOptionCombo": "%s", + "attributeOptionCombo": "HllvX50cXC0", + "value": "target DV 4 - duplicate later- KEEP", + "comment": "target DV 4 - duplicate later - KEEP" + }, + { + "dataElement": "deUid000001", + "period": "202408", + "orgUnit": "OrgUnitUid1", + "categoryOptionCombo": "%s", + "attributeOptionCombo": "HllvX50cXC0", + "value": "random 1, DV 1 - not impacted", + "comment": "random 1, DV 1 - not impacted" + }, + { + "dataElement": "deUid000001", + "period": "202408", + "orgUnit": "OrgUnitUid1", + "categoryOptionCombo": "%s", + "attributeOptionCombo": "HllvX50cXC0", + "value": "random 2, DV 2 - not impacted", + "comment": "random 2, DV 2 - not impacted" + } + ] + } + """ + .formatted( + source1Coc, + source1Coc, + source1Coc, + source1Coc, + source2Coc, + source2Coc, + source2Coc, + source2Coc, + targetCoc, + targetCoc, + targetCoc, + targetCoc, + randomCoc1, + randomCoc2)); + } + + private JsonObject dataValueSetImportAoc( + String source1Coc, + String source2Coc, + String targetCoc, + String randomCoc1, + String randomCoc2) { + return JsonParserUtils.toJsonObject( + """ + { + "dataValues": [ + { + "dataElement": "deUid000001", + "period": "202405", + "orgUnit": "OrgUnitUid2", + "categoryOptionCombo": "HllvX50cXC0", + "attributeOptionCombo": "%s", + "value": "source 1, DV 1 - non duplicate earlier - KEEP", + "comment": "source 1, DV 1 - non duplicate earlier - KEEP" + }, + { + "dataElement": "deUid000001", + "period": "202408", + "orgUnit": "OrgUnitUid2", + "categoryOptionCombo": "HllvX50cXC0", + "attributeOptionCombo": "%s", + "value": "source 1, DV 2 - duplicate earlier - REMOVE", + "comment": "source 1, DV 2 - duplicate earlier - REMOVE" + }, + { + "dataElement": "deUid000001", + "period": "202409", + "orgUnit": "OrgUnitUid2", + "categoryOptionCombo": "HllvX50cXC0", + "attributeOptionCombo": "%s", + "value": "source 1, DV 3 - duplicate later - KEEP", + "comment": "source 1, DV 3 - duplicate later - KEEP" + }, + { + "dataElement": "deUid000001", + "period": "202407", + "orgUnit": "OrgUnitUid2", + "categoryOptionCombo": "HllvX50cXC0", + "attributeOptionCombo": "%s", + "value": "source 1, DV 4 - duplicate earlier - REMOVE", + "comment": "source 1, DV 4 - duplicate earlier - REMOVE" + }, + { + "dataElement": "deUid000001", + "period": "202410", + "orgUnit": "OrgUnitUid2", + "categoryOptionCombo": "HllvX50cXC0", + "attributeOptionCombo": "%s", + "value": "source 2, DV 1 - non duplicate later - KEEP", + "comment": "source 2, DV 1 - non duplicate later - KEEP" + }, + { + "dataElement": "deUid000001", + "period": "202408", + "orgUnit": "OrgUnitUid2", + "categoryOptionCombo": "HllvX50cXC0", + "attributeOptionCombo": "%s", + "value": "source 2, DV 2 - duplicate later - KEEP", + "comment": "source 2, DV 2 - duplicate later - KEEP" + }, + { + "dataElement": "deUid000001", + "period": "202409", + "orgUnit": "OrgUnitUid2", + "categoryOptionCombo": "HllvX50cXC0", + "attributeOptionCombo": "%s", + "value": "source 2, DV 3 - duplicate earlier - REMOVE", + "comment": "source 2, DV 3 - duplicate earlier - REMOVE" + }, + { + "dataElement": "deUid000001", + "period": "202407", + "orgUnit": "OrgUnitUid2", + "categoryOptionCombo": "HllvX50cXC0", + "attributeOptionCombo": "%s", + "value": "source 2, DV 4 - duplicate earlier - REMOVE", + "comment": "source 2, DV 4 - duplicate earlier - REMOVE" + }, + { + "dataElement": "deUid000001", + "period": "202408", + "orgUnit": "OrgUnitUid2", + "categoryOptionCombo": "HllvX50cXC0", + "attributeOptionCombo": "%s", + "value": "target DV 1 - duplicate earlier - REMOVE", + "comment": "target DV 1 - duplicate earlier - REMOVE" + }, + { + "dataElement": "deUid000001", + "period": "202409", + "orgUnit": "OrgUnitUid2", + "categoryOptionCombo": "HllvX50cXC0", + "attributeOptionCombo": "%s", + "value": "target DV 2 - duplicate earlier - REMOVE", + "comment": "target DV 2 - duplicate earlier - REMOVE" + }, + { + "dataElement": "deUid000001", + "period": "202403", + "orgUnit": "OrgUnitUid2", + "categoryOptionCombo": "HllvX50cXC0", + "attributeOptionCombo": "%s", + "value": "target DV 3 - not impacted - KEEP", + "comment": "target DV 3 - not impacted - KEEP" + }, + { + "dataElement": "deUid000001", + "period": "202407", + "orgUnit": "OrgUnitUid2", + "categoryOptionCombo": "HllvX50cXC0", + "attributeOptionCombo": "%s", + "value": "target DV 4 - duplicate later- KEEP", + "comment": "target DV 4 - duplicate later - KEEP" + }, + { + "dataElement": "deUid000001", + "period": "202408", + "orgUnit": "OrgUnitUid2", + "categoryOptionCombo": "HllvX50cXC0", + "attributeOptionCombo": "%s", + "value": "random 1, DV 1 - not impacted", + "comment": "random 1, DV 1 - not impacted" + }, + { + "dataElement": "deUid000001", + "period": "202408", + "orgUnit": "OrgUnitUid2", + "categoryOptionCombo": "HllvX50cXC0", + "attributeOptionCombo": "%s", + "value": "random 2, DV 2 - not impacted", + "comment": "random 2, DV 2 - not impacted" + } + ] + } + """ + .formatted( + source1Coc, + source1Coc, + source1Coc, + source1Coc, + source2Coc, + source2Coc, + source2Coc, + source2Coc, + targetCoc, + targetCoc, + targetCoc, + targetCoc, + randomCoc1, + randomCoc2)); + } + + private JsonObject dataValueSetImportUpdateAoc( + String source1Coc, String source2Coc, String targetCoc) { + return JsonParserUtils.toJsonObject( + """ + { + "dataValues": [ + { + "dataElement": "deUid000001", + "period": "202409", + "orgUnit": "OrgUnitUid2", + "categoryOptionCombo": "HllvX50cXC0", + "attributeOptionCombo": "%s", + "value": "UPDATED source 1 DV 3 - duplicate later - KEEP", + "comment": "source 1, DV 3 - duplicate later - KEEP" + }, + { + "dataElement": "deUid000001", + "period": "202410", + "orgUnit": "OrgUnitUid2", + "categoryOptionCombo": "HllvX50cXC0", + "attributeOptionCombo": "%s", + "value": "UPDATED source 2 DV 1 - non duplicate later - KEEP", + "comment": "source 2, DV 1 - non duplicate later - KEEP" + }, + { + "dataElement": "deUid000001", + "period": "202408", + "orgUnit": "OrgUnitUid2", + "categoryOptionCombo": "HllvX50cXC0", + "attributeOptionCombo": "%s", + "value": "UPDATED source 2 DV 2 - duplicate later - KEEP", + "comment": "source 2, DV 2 - duplicate later - KEEP" + }, + { + "dataElement": "deUid000001", + "period": "202407", + "orgUnit": "OrgUnitUid2", + "categoryOptionCombo": "HllvX50cXC0", + "attributeOptionCombo": "%s", + "value": "UPDATED target DV 4 - duplicate later - KEEP", + "comment": "target DV 4 - duplicate later - KEEP" + } + ] + } + """ + .formatted(source1Coc, source2Coc, source2Coc, targetCoc)); + } + + private JsonObject dataValueSetImportUpdateCoc( + String source1Coc, String source2Coc, String targetCoc) { + return JsonParserUtils.toJsonObject( + """ + { + "dataValues": [ + { + "dataElement": "deUid000001", + "period": "202409", + "orgUnit": "OrgUnitUid1", + "categoryOptionCombo": "%s", + "attributeOptionCombo": "HllvX50cXC0", + "value": "UPDATED source 1 DV 3 - duplicate later - KEEP", + "comment": "source 1, DV 3 - duplicate later - KEEP" + }, + { + "dataElement": "deUid000001", + "period": "202410", + "orgUnit": "OrgUnitUid1", + "categoryOptionCombo": "%s", + "attributeOptionCombo": "HllvX50cXC0", + "value": "UPDATED source 2 DV 1 - non duplicate later - KEEP", + "comment": "source 2, DV 1 - non duplicate later - KEEP" + }, + { + "dataElement": "deUid000001", + "period": "202408", + "orgUnit": "OrgUnitUid1", + "categoryOptionCombo": "%s", + "attributeOptionCombo": "HllvX50cXC0", + "value": "UPDATED source 2 DV 2 - duplicate later - KEEP", + "comment": "source 2, DV 2 - duplicate later - KEEP" + }, + { + "dataElement": "deUid000001", + "period": "202407", + "orgUnit": "OrgUnitUid1", + "categoryOptionCombo": "%s", + "attributeOptionCombo": "HllvX50cXC0", + "value": "UPDATED target DV 4 - duplicate later - KEEP", + "comment": "target DV 4 - duplicate later - KEEP" + } + ] + } + """ + .formatted(source1Coc, source2Coc, source2Coc, targetCoc)); + } +} diff --git a/dhis-2/dhis-test-e2e/src/test/resources/tracker/programs_with_program_rules.json b/dhis-2/dhis-test-e2e/src/test/resources/tracker/programs_with_program_rules.json index 9e1757d55eed..2713213dfc93 100644 --- a/dhis-2/dhis-test-e2e/src/test/resources/tracker/programs_with_program_rules.json +++ b/dhis-2/dhis-test-e2e/src/test/resources/tracker/programs_with_program_rules.json @@ -711,7 +711,7 @@ }, "favorites": [], "translations": [], - + "attributeValues": [] }], "translations": [], @@ -960,7 +960,8 @@ "created": "2020-12-08T09:10:20.341", "name": "TA tracker program rule - showerror on stage", "displayName": "TA tracker program rule - showerror", - "condition": "V{enrollment_date} > V{current_date}", + "externalAccess": false, + "condition": "d2:daysBetween(V{enrollment_date},V{current_date}) < 0", "favorite": false, "program": { "id": "U5HE4IRrZ7S" @@ -1002,7 +1003,7 @@ "users": {}, "userGroups": {} }, - "condition": "V{event_date} < V{current_date}", + "condition": "d2:daysBetween(V{event_date},V{current_date}) > 0", "program": { "id": "U5HE4IRrZ7S" }, @@ -1031,7 +1032,7 @@ "users": {}, "userGroups": {} }, - "condition": "V{event_date} < V{current_date}", + "condition": "d2:daysBetween(V{event_date},V{current_date}) > 0", "program": { "id": "U5HE4IRrZ7S" }, @@ -1082,7 +1083,8 @@ "created": "2020-12-08T09:10:20.341", "name": "TA tracker program rule - warnings", "displayName": "TA tracker program rule - warnings", - "condition": "V{enrollment_date} < V{current_date}", + "externalAccess": false, + "condition": "d2:daysBetween(V{enrollment_date},V{current_date}) > 0", "favorite": false, "program": { "id": "U5HE4IRrZ7S" diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/analytics/data/QueryPlannerTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/analytics/data/QueryPlannerTest.java index f1b9b9001e31..19c4c85f3f7e 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/analytics/data/QueryPlannerTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/analytics/data/QueryPlannerTest.java @@ -565,14 +565,10 @@ void planQueryB() { */ @Test void planQueryC() { - ouB.setParent(ouA); - ouC.setParent(ouA); - ouD.setParent(ouB); - ouE.setParent(ouC); - ouA.getChildren().add(ouB); - ouA.getChildren().add(ouC); - ouD.getChildren().add(ouB); - ouC.getChildren().add(ouE); + ouB.updateParent(ouA); + ouC.updateParent(ouA); + ouD.updateParent(ouB); + ouE.updateParent(ouC); organisationUnitService.updateOrganisationUnit(ouA); organisationUnitService.updateOrganisationUnit(ouB); organisationUnitService.updateOrganisationUnit(ouC); diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/analytics/event/data/EventDataQueryServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/analytics/event/data/EventDataQueryServiceTest.java index 9bd97b9ec042..97484aad9035 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/analytics/event/data/EventDataQueryServiceTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/analytics/event/data/EventDataQueryServiceTest.java @@ -503,17 +503,18 @@ void testGetCoordinateField() { assertEquals( List.of("eventgeometry"), dataQueryService.getCoordinateFields( - prA.getUid(), EventQueryParams.EVENT_COORDINATE_FIELD, null, false)); + toRequest(prA.getUid(), EventQueryParams.EVENT_COORDINATE_FIELD, null, false))); assertEquals( List.of("enrollmentgeometry"), dataQueryService.getCoordinateFields( - prA.getUid(), EventQueryParams.ENROLLMENT_COORDINATE_FIELD, null, false)); + toRequest(prA.getUid(), EventQueryParams.ENROLLMENT_COORDINATE_FIELD, null, false))); assertEquals( List.of("eventgeometry"), - dataQueryService.getCoordinateFields(prA.getUid(), null, "eventgeometry", false)); + dataQueryService.getCoordinateFields( + toRequest(prA.getUid(), null, "eventgeometry", false))); assertEquals( List.of(deC.getUid()), - dataQueryService.getCoordinateFields(prA.getUid(), deC.getUid(), null, false)); + dataQueryService.getCoordinateFields(toRequest(prA.getUid(), deC.getUid(), null, false))); } @Test @@ -525,30 +526,30 @@ void testGetBackwardCompatibleCoordinateField() { assertEquals( List.of(COL_NAME_EVENT_GEOMETRY), dataQueryService.getCoordinateFields( - prA.getUid(), OLD_COL_NAME_EVENT_GEOMETRY, null, false)); + toRequest(prA.getUid(), OLD_COL_NAME_EVENT_GEOMETRY, null, false))); assertEquals( List.of(COL_NAME_ENROLLMENT_GEOMETRY), dataQueryService.getCoordinateFields( - prA.getUid(), OLD_COL_NAME_ENROLLMENT_GEOMETRY, null, false)); + toRequest(prA.getUid(), OLD_COL_NAME_ENROLLMENT_GEOMETRY, null, false))); assertEquals( List.of(COL_NAME_TRACKED_ENTITY_GEOMETRY), dataQueryService.getCoordinateFields( - prA.getUid(), OLD_COL_NAME_TRACKED_ENTITY_GEOMETRY, null, false)); + toRequest(prA.getUid(), OLD_COL_NAME_TRACKED_ENTITY_GEOMETRY, null, false))); assertEquals( List.of(COL_NAME_EVENT_GEOMETRY), dataQueryService.getCoordinateFields( - prA.getUid(), null, OLD_COL_NAME_EVENT_GEOMETRY, false)); + toRequest(prA.getUid(), null, OLD_COL_NAME_EVENT_GEOMETRY, false))); assertEquals( List.of(COL_NAME_ENROLLMENT_GEOMETRY), dataQueryService.getCoordinateFields( - prA.getUid(), null, OLD_COL_NAME_ENROLLMENT_GEOMETRY, false)); + toRequest(prA.getUid(), null, OLD_COL_NAME_ENROLLMENT_GEOMETRY, false))); assertEquals( List.of(COL_NAME_TRACKED_ENTITY_GEOMETRY), dataQueryService.getCoordinateFields( - prA.getUid(), null, OLD_COL_NAME_TRACKED_ENTITY_GEOMETRY, false)); + toRequest(prA.getUid(), null, OLD_COL_NAME_TRACKED_ENTITY_GEOMETRY, false))); assertEquals( List.of(deC.getUid()), - dataQueryService.getCoordinateFields(prA.getUid(), deC.getUid(), null, false)); + dataQueryService.getCoordinateFields(toRequest(prA.getUid(), deC.getUid(), null, false))); } @Test @@ -560,7 +561,7 @@ void testGetInvalidCoordinateFieldException() { // Then assertThrows( IllegalQueryException.class, - () -> dataQueryService.getCoordinateFields(programUid, "badfield", null, false)); + () -> dataQueryService.getCoordinateFields(toRequest(programUid, "badfield", null, false))); } @Test @@ -572,6 +573,21 @@ void testGetNonCoordinateValueTypeCoordinateFieldException() { // Then assertThrows( IllegalQueryException.class, - () -> dataQueryService.getCoordinateFields(programUid, "tegeometry", "badfallback", false)); + () -> + dataQueryService.getCoordinateFields( + toRequest(programUid, "tegeometry", "badfallback", false))); + } + + private EventDataQueryRequest toRequest( + String program, + String coordinateField, + String fallbackCoordinateField, + boolean isDefaultCoordinateFallback) { + return EventDataQueryRequest.builder() + .program(program) + .coordinateField(coordinateField) + .fallbackCoordinateField(fallbackCoordinateField) + .defaultCoordinateFallback(isDefaultCoordinateFallback) + .build(); } } diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/category/CategoryComboStoreTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/category/CategoryComboStoreTest.java new file mode 100644 index 000000000000..3868569b7471 --- /dev/null +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/category/CategoryComboStoreTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.category; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.hisp.dhis.common.BaseIdentifiableObject; +import org.hisp.dhis.common.CodeGenerator; +import org.hisp.dhis.common.UID; +import org.hisp.dhis.test.integration.PostgresIntegrationTestBase; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +class CategoryComboStoreTest extends PostgresIntegrationTestBase { + @Autowired private CategoryComboStore categoryComboStore; + + @Test + @DisplayName("Retrieving CategoryCombos by CategoryOptionCombos returns the expected entries") + void getCatOptionComboTest() { + // given + CategoryOption co1 = createCategoryOption("1A", CodeGenerator.generateUid()); + CategoryOption co2 = createCategoryOption("1B", CodeGenerator.generateUid()); + CategoryOption co3 = createCategoryOption("2A", CodeGenerator.generateUid()); + CategoryOption co4 = createCategoryOption("2B", CodeGenerator.generateUid()); + CategoryOption co5 = createCategoryOption("3A", CodeGenerator.generateUid()); + CategoryOption co6 = createCategoryOption("4A", CodeGenerator.generateUid()); + categoryService.addCategoryOption(co1); + categoryService.addCategoryOption(co2); + categoryService.addCategoryOption(co3); + categoryService.addCategoryOption(co4); + categoryService.addCategoryOption(co5); + categoryService.addCategoryOption(co6); + + Category c1 = createCategory('1', co1, co2); + Category c2 = createCategory('2', co3, co4); + Category c3 = createCategory('3', co5); + Category c4 = createCategory('4', co6); + categoryService.addCategory(c1); + categoryService.addCategory(c2); + categoryService.addCategory(c3); + categoryService.addCategory(c4); + + CategoryCombo cc1 = createCategoryCombo('1', c1, c2); + CategoryCombo cc2 = createCategoryCombo('2', c3, c4); + categoryService.addCategoryCombo(cc1); + categoryService.addCategoryCombo(cc2); + + categoryService.generateOptionCombos(cc1); + categoryService.generateOptionCombos(cc2); + + CategoryOptionCombo coc1 = getCocWithOptions("1A", "2B"); + CategoryOptionCombo coc2 = getCocWithOptions("2A", "1B"); + + // when + List catCombosByCategoryOptionCombo = + categoryComboStore.getByCategoryOptionCombo(UID.of(coc1.getUid(), coc2.getUid())); + + // then + assertEquals(1, catCombosByCategoryOptionCombo.size(), "1 CategoryCombo should be present"); + List categoryCombos = + catCombosByCategoryOptionCombo.stream().map(BaseIdentifiableObject::getUid).toList(); + + assertTrue( + categoryCombos.contains(cc1.getUid()), + "Retrieved CategoryCombo UID should equal the expected value"); + } + + private CategoryOptionCombo getCocWithOptions(String co1, String co2) { + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + return allCategoryOptionCombos.stream() + .filter( + coc -> { + List categoryOptions = + coc.getCategoryOptions().stream().map(BaseIdentifiableObject::getName).toList(); + return categoryOptions.containsAll(List.of(co1, co2)); + }) + .toList() + .get(0); + } +} diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/category/CategoryOptionStoreTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/category/CategoryOptionStoreTest.java new file mode 100644 index 000000000000..b5b92e5e69fd --- /dev/null +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/category/CategoryOptionStoreTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.category; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.hisp.dhis.common.BaseIdentifiableObject; +import org.hisp.dhis.common.CodeGenerator; +import org.hisp.dhis.common.UID; +import org.hisp.dhis.test.integration.PostgresIntegrationTestBase; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +class CategoryOptionStoreTest extends PostgresIntegrationTestBase { + @Autowired private CategoryOptionStore categoryOptionStore; + + @Test + @DisplayName("Retrieving CategoryOptions by CategoryOptionCombos returns the expected entries") + void getCatOptionComboTest() { + // given + CategoryOption co1 = createCategoryOption("1A", CodeGenerator.generateUid()); + CategoryOption co2 = createCategoryOption("1B", CodeGenerator.generateUid()); + CategoryOption co3 = createCategoryOption("2A", CodeGenerator.generateUid()); + CategoryOption co4 = createCategoryOption("2B", CodeGenerator.generateUid()); + CategoryOption co5 = createCategoryOption("3A", CodeGenerator.generateUid()); + CategoryOption co6 = createCategoryOption("4A", CodeGenerator.generateUid()); + categoryService.addCategoryOption(co1); + categoryService.addCategoryOption(co2); + categoryService.addCategoryOption(co3); + categoryService.addCategoryOption(co4); + categoryService.addCategoryOption(co5); + categoryService.addCategoryOption(co6); + + Category c1 = createCategory('1', co1, co2); + Category c2 = createCategory('2', co3, co4); + Category c3 = createCategory('3', co5); + Category c4 = createCategory('4', co6); + categoryService.addCategory(c1); + categoryService.addCategory(c2); + categoryService.addCategory(c3); + categoryService.addCategory(c4); + + CategoryCombo cc1 = createCategoryCombo('1', c1, c2); + CategoryCombo cc2 = createCategoryCombo('2', c3, c4); + categoryService.addCategoryCombo(cc1); + categoryService.addCategoryCombo(cc2); + + categoryService.generateOptionCombos(cc1); + categoryService.generateOptionCombos(cc2); + + CategoryOptionCombo coc1 = getCocWithOptions("1A", "2B"); + CategoryOptionCombo coc2 = getCocWithOptions("2A", "1B"); + + // when + List catOptionsByCategoryOptionCombo = + categoryOptionStore.getByCategoryOptionCombo(UID.of(coc1.getUid(), coc2.getUid())); + + // then + assertEquals(4, catOptionsByCategoryOptionCombo.size(), "4 CategoryOptions should be present"); + List categoryOptions = + catOptionsByCategoryOptionCombo.stream().map(BaseIdentifiableObject::getUid).toList(); + + assertTrue( + categoryOptions.containsAll( + List.of(co1.getUid(), co2.getUid(), co3.getUid(), co4.getUid())), + "Retrieved CategoryOption UIDs should have expected UIDs"); + } + + private CategoryOptionCombo getCocWithOptions(String co1, String co2) { + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + return allCategoryOptionCombos.stream() + .filter( + coc -> { + List categoryOptions = + coc.getCategoryOptions().stream().map(BaseIdentifiableObject::getName).toList(); + return categoryOptions.containsAll(List.of(co1, co2)); + }) + .toList() + .get(0); + } +} diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/category/CategoryServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/category/CategoryServiceTest.java index f62af477abc7..fc2aba9673d3 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/category/CategoryServiceTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/category/CategoryServiceTest.java @@ -160,22 +160,6 @@ void testDeleteCategoryOption() { assertTrue(categoryService.getCategoryOption(optionIdA).getCategories().contains(categoryB)); } - @Test - void testGetAll() { - categoryA = createCategory('A'); - categoryB = createCategory('B'); - categoryC = createCategory('C'); - categoryService.addCategory(categoryA); - categoryService.addCategory(categoryB); - categoryService.addCategory(categoryC); - List categories = categoryService.getAllDataElementCategories(); - // Including default - assertEquals(4, categories.size()); - assertTrue(categories.contains(categoryA)); - assertTrue(categories.contains(categoryB)); - assertTrue(categories.contains(categoryC)); - } - @Test void testAddGetCategoryGroup() { CategoryOptionGroup groupA = createCategoryOptionGroup('A'); @@ -304,18 +288,6 @@ void testGetDisaggregationCategoryOptionGroupSetsNoAcl() { assertEquals(1, categoryService.getDisaggregationCategoryOptionGroupSetsNoAcl().size()); } - @Test - void testGetDisaggregationCategories() { - categoryA = createCategory('A', categoryOptionA, categoryOptionB, categoryOptionC); - categoryA.setDataDimensionType(DataDimensionType.DISAGGREGATION); - categoryService.addCategory(categoryA); - // Default Category is created so count should be equal 2 - assertEquals(2, categoryService.getDisaggregationCategories().size()); - assertEquals(1, categoryStore.getCategories(DataDimensionType.DISAGGREGATION, true).size()); - assertEquals( - 1, categoryStore.getCategoriesNoAcl(DataDimensionType.DISAGGREGATION, true).size()); - } - @Test void testAddAndPruneAllCategoryCombos() { categoryA = createCategory('A', categoryOptionA, categoryOptionB); diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataapproval/DataApprovalAuditStoreTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataapproval/DataApprovalAuditStoreTest.java index b09308715cc0..5d66e204f7b7 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataapproval/DataApprovalAuditStoreTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataapproval/DataApprovalAuditStoreTest.java @@ -49,6 +49,7 @@ import org.hisp.dhis.test.integration.PostgresIntegrationTestBase; import org.hisp.dhis.user.User; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; @@ -246,4 +247,45 @@ void TestGetDataApprovalAudits() { assertEquals(1, audits.size()); assertEquals(auditB, audits.get(0)); } + + @Test + @DisplayName("Deleting audits by category option combo deletes the correct entries") + void deleteByCocTest() { + // given + CategoryOptionCombo coc1 = createCategoryOptionCombo('1'); + coc1.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(coc1); + + CategoryOptionCombo coc2 = createCategoryOptionCombo('2'); + coc2.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(coc2); + + CategoryOptionCombo coc3 = createCategoryOptionCombo('3'); + coc3.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(coc3); + + DataApproval approvalX = + new DataApproval(level1, workflowA, periodA, sourceA, coc1, false, dateA, userA); + DataApproval approvalY = + new DataApproval(level2, workflowB, periodB, sourceB, coc2, false, dateB, userA); + DataApproval approvalZ = + new DataApproval(level2, workflowB, periodB, sourceA, coc3, false, dateB, userA); + + DataApprovalAudit auditA = new DataApprovalAudit(approvalX, APPROVE); + DataApprovalAudit auditB = new DataApprovalAudit(approvalY, UNAPPROVE); + DataApprovalAudit auditC = new DataApprovalAudit(approvalZ, UNAPPROVE); + dataApprovalAuditStore.save(auditA); + dataApprovalAuditStore.save(auditB); + dataApprovalAuditStore.save(auditC); + + // when + dataApprovalAuditStore.deleteDataApprovalAudits(coc1); + dataApprovalAuditStore.deleteDataApprovalAudits(coc2); + + // then + List audits = + dataApprovalAuditStore.getDataApprovalAudits(new DataApprovalAuditQueryParams()); + assertEquals(1, audits.size()); + assertEquals(auditC, audits.get(0)); + } } diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataapproval/DataApprovalStoreTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataapproval/DataApprovalStoreTest.java index 7ce448334a45..73a31773287f 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataapproval/DataApprovalStoreTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataapproval/DataApprovalStoreTest.java @@ -31,11 +31,14 @@ import static org.junit.jupiter.api.Assertions.assertEquals; 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.Date; +import java.util.List; import org.hisp.dhis.category.CategoryCombo; import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.category.CategoryService; +import org.hisp.dhis.common.UID; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.organisationunit.OrganisationUnitService; import org.hisp.dhis.period.Period; @@ -44,6 +47,7 @@ import org.hisp.dhis.test.integration.PostgresIntegrationTestBase; import org.hisp.dhis.user.User; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; @@ -250,4 +254,43 @@ void testDeleteDataApproval() { level2, workflowB12, periodB, sourceB, categoryOptionCombo); assertNull(dataApprovalB); } + + @Test + @DisplayName("Retrieving DataApprovals by CategoryOptionCombo returns expected results") + void getByCocTest() { + // given + CategoryOptionCombo coc1 = createCategoryOptionCombo('1'); + coc1.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(coc1); + + CategoryOptionCombo coc2 = createCategoryOptionCombo('2'); + coc2.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(coc2); + + CategoryOptionCombo coc3 = createCategoryOptionCombo('3'); + coc3.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(coc3); + + Date date = new Date(); + DataApproval dataApprovalA = + new DataApproval(level1, workflowA12, periodA, sourceA, coc1, false, date, userA); + DataApproval dataApprovalB = + new DataApproval(level2, workflowA12, periodA, sourceB, coc2, false, date, userA); + DataApproval dataApprovalC = + new DataApproval(level1, workflowA12, periodB, sourceA, coc3, false, date, userA); + + dataApprovalStore.addDataApproval(dataApprovalA); + dataApprovalStore.addDataApproval(dataApprovalB); + dataApprovalStore.addDataApproval(dataApprovalC); + + // when + List allByCoc = + dataApprovalStore.getByCategoryOptionCombo(UID.of(coc1.getUid(), coc2.getUid())); + + // then + assertEquals(2, allByCoc.size()); + assertTrue( + allByCoc.containsAll(List.of(dataApprovalA, dataApprovalB)), + "Retrieved result set should contain both DataApprovals"); + } } diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataelement/DataElementOperandStoreTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataelement/DataElementOperandStoreTest.java index 86e75b782688..1d5058120810 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataelement/DataElementOperandStoreTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataelement/DataElementOperandStoreTest.java @@ -32,7 +32,9 @@ import java.util.List; import org.hisp.dhis.category.CategoryCombo; +import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.common.IdentifiableObjectManager; +import org.hisp.dhis.common.UID; import org.hisp.dhis.test.integration.PostgresIntegrationTestBase; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -78,12 +80,49 @@ void dataElementOperandByDataElementTest() { .containsAll(List.of(deW.getUid(), deX.getUid()))); } + @Test + @DisplayName("retrieving DataElementOperands by CategoryOptionCombo returns expected entries") + void dataElementOperandByCatOptComboTest() { + // given + CategoryCombo cc = createCategoryCombo("1", "CatComUid01"); + manager.save(cc); + + CategoryOptionCombo coc1 = createCategoryOptionCombo(cc); + CategoryOptionCombo coc2 = createCategoryOptionCombo(cc); + CategoryOptionCombo coc3 = createCategoryOptionCombo(cc); + CategoryOptionCombo coc4 = createCategoryOptionCombo(cc); + manager.save(List.of(coc1, coc2, coc3, coc4)); + + createDataElementOperandAndSave(coc1); + createDataElementOperandAndSave(coc2); + createDataElementOperandAndSave(coc3); + createDataElementOperandAndSave(coc4); + + // when + List dataElementOperands = + dataElementOperandStore.getByCategoryOptionCombo(UID.of(coc1.getUid(), coc2.getUid())); + + // then + assertEquals(2, dataElementOperands.size()); + assertTrue( + dataElementOperands.stream() + .map(deo -> deo.getCategoryOptionCombo().getUid()) + .toList() + .containsAll(List.of(coc1.getUid(), coc2.getUid()))); + } + private void createDataElementOperandAndSave(DataElement de) { DataElementOperand deo = new DataElementOperand(); deo.setDataElement(de); manager.save(deo); } + private void createDataElementOperandAndSave(CategoryOptionCombo coc) { + DataElementOperand deo = new DataElementOperand(); + deo.setCategoryOptionCombo(coc); + manager.save(deo); + } + private DataElement createDataElementAndSave(char c) { CategoryCombo cc = createCategoryCombo(c); manager.save(cc); diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataelement/DataElementServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataelement/DataElementServiceTest.java index 6b1e40c9f922..eb5283bc8b2e 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataelement/DataElementServiceTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataelement/DataElementServiceTest.java @@ -251,31 +251,6 @@ void testGetAllDataElements() { assertTrue(dataElements.containsAll(dataElementsRef)); } - @Test - void testGetAllDataElementsByType() { - assertEquals(0, dataElementService.getAllDataElements().size()); - DataElement dataElementA = createDataElement('A'); - DataElement dataElementB = createDataElement('B'); - DataElement dataElementC = createDataElement('C'); - DataElement dataElementD = createDataElement('D'); - dataElementA.setValueType(ValueType.FILE_RESOURCE); - dataElementB.setValueType(ValueType.EMAIL); - dataElementC.setValueType(ValueType.BOOLEAN); - dataElementD.setValueType(ValueType.FILE_RESOURCE); - dataElementService.addDataElement(dataElementA); - dataElementService.addDataElement(dataElementB); - dataElementService.addDataElement(dataElementC); - dataElementService.addDataElement(dataElementD); - List dataElementsRef = new ArrayList<>(); - dataElementsRef.add(dataElementA); - dataElementsRef.add(dataElementD); - List dataElements = - dataElementService.getAllDataElementsByValueType(ValueType.FILE_RESOURCE); - assertNotNull(dataElements); - assertEquals(dataElementsRef.size(), dataElements.size()); - assertTrue(dataElements.containsAll(dataElementsRef)); - } - // ------------------------------------------------------------------------- // DataElementGroup // ------------------------------------------------------------------------- diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataelement/DataElementStoreTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataelement/DataElementStoreTest.java index 42313bcd8286..03013401fef8 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataelement/DataElementStoreTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataelement/DataElementStoreTest.java @@ -154,28 +154,6 @@ void testGetAllDataElements() { assertTrue(dataElements.containsAll(dataElementsRef)); } - @Test - void testGetDataElementsByDomainType() { - assertEquals( - 0, dataElementStore.getDataElementsByDomainType(DataElementDomain.AGGREGATE).size()); - assertEquals(0, dataElementStore.getDataElementsByDomainType(DataElementDomain.TRACKER).size()); - DataElement dataElementA = createDataElement('A'); - dataElementA.setDomainType(DataElementDomain.AGGREGATE); - DataElement dataElementB = createDataElement('B'); - dataElementB.setDomainType(DataElementDomain.TRACKER); - DataElement dataElementC = createDataElement('C'); - dataElementC.setDomainType(DataElementDomain.TRACKER); - DataElement dataElementD = createDataElement('D'); - dataElementD.setDomainType(DataElementDomain.TRACKER); - dataElementStore.save(dataElementA); - dataElementStore.save(dataElementB); - dataElementStore.save(dataElementC); - dataElementStore.save(dataElementD); - assertEquals( - 1, dataElementStore.getDataElementsByDomainType(DataElementDomain.AGGREGATE).size()); - assertEquals(3, dataElementStore.getDataElementsByDomainType(DataElementDomain.TRACKER).size()); - } - @Test void testGetDataElementAggregationLevels() { List aggregationLevels = Arrays.asList(3, 5); @@ -233,22 +211,6 @@ void testGetDataElementsByAggregationLevel() { assertTrue(dataElements.contains(dataElementB)); } - @Test - void testGetDataElementsZeroIsSignificant() { - DataElement dataElementA = createDataElement('A'); - DataElement dataElementB = createDataElement('B'); - DataElement dataElementC = createDataElement('C'); - DataElement dataElementD = createDataElement('D'); - dataElementA.setZeroIsSignificant(true); - dataElementB.setZeroIsSignificant(true); - dataElementStore.save(dataElementA); - dataElementStore.save(dataElementB); - dataElementStore.save(dataElementC); - dataElementStore.save(dataElementD); - List dataElements = dataElementStore.getDataElementsByZeroIsSignificant(true); - assertTrue(equals(dataElements, dataElementA, dataElementB)); - } - @Test void testDataElementByNonUniqueAttributeValue() throws NonUniqueAttributeValueException { Attribute attribute = new Attribute("cid", ValueType.TEXT); diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataset/CompleteDataSetRegistrationStoreTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataset/CompleteDataSetRegistrationStoreTest.java new file mode 100644 index 000000000000..49a76639f08f --- /dev/null +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataset/CompleteDataSetRegistrationStoreTest.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.dataset; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Date; +import java.util.List; +import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.category.CategoryService; +import org.hisp.dhis.common.IdentifiableObjectManager; +import org.hisp.dhis.common.UID; +import org.hisp.dhis.dataelement.DataElement; +import org.hisp.dhis.organisationunit.OrganisationUnit; +import org.hisp.dhis.period.MonthlyPeriodType; +import org.hisp.dhis.period.Period; +import org.hisp.dhis.period.PeriodService; +import org.hisp.dhis.test.integration.PostgresIntegrationTestBase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author david mackessy + */ +@Transactional +class CompleteDataSetRegistrationStoreTest extends PostgresIntegrationTestBase { + + @Autowired private CompleteDataSetRegistrationService completeDataSetRegistrationService; + @Autowired private CompleteDataSetRegistrationStore completeDataSetRegistrationStore; + @Autowired private DataSetService dataSetService; + @Autowired private PeriodService periodService; + @Autowired private IdentifiableObjectManager manager; + @Autowired private CategoryService categoryService; + + private DataElement elementA; + private DataElement elementB; + private DataElement elementC; + private DataSet dataSetA; + private DataSet dataSetB; + private DataSet dataSetC; + private Period periodA; + private Period periodB; + private OrganisationUnit sourceA; + private OrganisationUnit sourceB; + private OrganisationUnit sourceC; + + @BeforeEach + void setUp() { + sourceA = createOrganisationUnit('A'); + sourceB = createOrganisationUnit('B'); + sourceC = createOrganisationUnit('C'); + manager.save(List.of(sourceA, sourceB, sourceC)); + + periodA = createPeriod(new MonthlyPeriodType(), getDate(2000, 1, 1), getDate(2000, 1, 31)); + periodB = createPeriod(new MonthlyPeriodType(), getDate(2000, 2, 1), getDate(2000, 2, 28)); + periodService.addPeriod(periodA); + periodService.addPeriod(periodB); + + elementA = createDataElement('A'); + elementB = createDataElement('B'); + elementC = createDataElement('C'); + manager.save(List.of(elementA, elementB, elementC)); + + dataSetA = createDataSet('A', new MonthlyPeriodType()); + dataSetB = createDataSet('B', new MonthlyPeriodType()); + dataSetC = createDataSet('C', new MonthlyPeriodType()); + dataSetA.addDataSetElement(elementA); + dataSetB.addDataSetElement(elementB); + dataSetC.addDataSetElement(elementC); + + dataSetA.getSources().add(sourceA); + dataSetB.getSources().add(sourceB); + dataSetC.getSources().add(sourceA); + dataSetService.addDataSet(dataSetA); + dataSetService.addDataSet(dataSetB); + dataSetService.addDataSet(dataSetC); + } + + @Test + @DisplayName("Get all CompleteDataSetRegistration by CategoryOptionCombo") + void testSaveGet() { + // given + CategoryOptionCombo aoc1 = createCategoryOptionCombo('1'); + aoc1.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(aoc1); + + CategoryOptionCombo aoc2 = createCategoryOptionCombo('2'); + aoc2.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(aoc2); + + CategoryOptionCombo aoc3 = createCategoryOptionCombo('3'); + aoc3.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(aoc3); + + CompleteDataSetRegistration registrationA = + new CompleteDataSetRegistration( + dataSetA, periodA, sourceA, aoc1, new Date(), "", new Date(), "", true); + CompleteDataSetRegistration registrationB = + new CompleteDataSetRegistration( + dataSetB, periodB, sourceA, aoc2, new Date(), "", new Date(), "", true); + CompleteDataSetRegistration registrationC = + new CompleteDataSetRegistration( + dataSetC, periodB, sourceB, aoc3, new Date(), "", new Date(), "", true); + completeDataSetRegistrationService.saveCompleteDataSetRegistration(registrationA); + completeDataSetRegistrationService.saveCompleteDataSetRegistration(registrationB); + completeDataSetRegistrationService.saveCompleteDataSetRegistration(registrationC); + + // when + List allByCategoryOptionCombo = + completeDataSetRegistrationStore.getAllByCategoryOptionCombo( + UID.of(aoc1.getUid(), aoc2.getUid())); + assertEquals(2, allByCategoryOptionCombo.size()); + assertTrue( + allByCategoryOptionCombo.containsAll(List.of(registrationA, registrationB)), + "retrieved registrations contains 2 registrations referencing the 2 attribute opt combos passed in"); + assertFalse( + allByCategoryOptionCombo.contains(registrationC), + "retrieved registrations do not contain a registration referencing a AOC not used in the query"); + } +} diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataset/DataSetStoreTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataset/DataSetStoreTest.java index 5d5f97b31d6e..69785683fbdc 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataset/DataSetStoreTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/dataset/DataSetStoreTest.java @@ -127,17 +127,6 @@ void testGetAllDataSets() { assertContainsOnly(List.of(dataSetA, dataSetB), dataSetStore.getAll()); } - @Test - void testGetDataSetByPeriodType() { - List types = PeriodType.getAvailablePeriodTypes(); - PeriodType periodType1 = types.get(0); - PeriodType periodType2 = types.get(1); - DataSet dataSetA = addDataSet('A', periodType1); - DataSet dataSetB = addDataSet('B', periodType2); - assertContainsOnly(List.of(dataSetA), dataSetStore.getDataSetsByPeriodType(periodType1)); - assertContainsOnly(List.of(dataSetB), dataSetStore.getDataSetsByPeriodType(periodType2)); - } - @Test void testGetByDataEntryForm() { DataSet dataSetA = addDataSet('A'); diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/datavalue/DataValueAuditStoreTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/datavalue/DataValueAuditStoreTest.java new file mode 100644 index 000000000000..027c142d4d9d --- /dev/null +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/datavalue/DataValueAuditStoreTest.java @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.datavalue; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; +import org.hisp.dhis.audit.AuditOperationType; +import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.category.CategoryService; +import org.hisp.dhis.common.IdentifiableObjectManager; +import org.hisp.dhis.dataelement.DataElement; +import org.hisp.dhis.organisationunit.OrganisationUnit; +import org.hisp.dhis.period.MonthlyPeriodType; +import org.hisp.dhis.period.Period; +import org.hisp.dhis.period.PeriodService; +import org.hisp.dhis.test.integration.PostgresIntegrationTestBase; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +@Transactional +class DataValueAuditStoreTest extends PostgresIntegrationTestBase { + + @Autowired private DataValueAuditService dataValueAuditService; + @Autowired private DataValueAuditStore dataValueAuditStore; + @Autowired private DataValueService dataValueService; + @Autowired private IdentifiableObjectManager manager; + @Autowired private CategoryService categoryService; + @Autowired private PeriodService periodService; + + private DataValue dataValueA1; + private DataValue dataValueA2; + private DataValue dataValueB1; + private DataValue dataValueB2; + private DataValue dataValueC1; + private DataValue dataValueC2; + private CategoryOptionCombo coc1; + private CategoryOptionCombo coc2; + private CategoryOptionCombo coc3; + + @BeforeEach + void setUp() { + coc1 = createCategoryOptionCombo('1'); + coc1.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(coc1); + + coc2 = createCategoryOptionCombo('2'); + coc2.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(coc2); + + coc3 = createCategoryOptionCombo('3'); + coc3.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(coc3); + + DataElement dataElementA = createDataElement('A'); + DataElement dataElementB = createDataElement('B'); + DataElement dataElementC = createDataElement('C'); + manager.save(List.of(dataElementA, dataElementB, dataElementC)); + + Period periodA = + createPeriod(new MonthlyPeriodType(), getDate(2017, 1, 1), getDate(2017, 1, 31)); + Period periodB = + createPeriod(new MonthlyPeriodType(), getDate(2018, 1, 1), getDate(2017, 1, 31)); + Period periodC = + createPeriod(new MonthlyPeriodType(), getDate(2019, 1, 1), getDate(2017, 1, 31)); + periodService.addPeriod(periodA); + periodService.addPeriod(periodB); + periodService.addPeriod(periodC); + + OrganisationUnit orgUnitA = createOrganisationUnit('A'); + OrganisationUnit orgUnitB = createOrganisationUnit('B'); + OrganisationUnit orgUnitC = createOrganisationUnit('C'); + manager.save(List.of(orgUnitA, orgUnitB, orgUnitC)); + + dataValueA1 = createDataValue(dataElementA, periodA, orgUnitA, coc1, coc1, "1"); + dataValueA2 = createDataValue(dataElementA, periodB, orgUnitA, coc1, coc1, "2"); + dataValueB1 = createDataValue(dataElementB, periodB, orgUnitB, coc2, coc2, "3"); + dataValueB2 = createDataValue(dataElementB, periodC, orgUnitB, coc2, coc2, "4"); + dataValueC1 = createDataValue(dataElementC, periodC, orgUnitC, coc3, coc3, "5"); + dataValueC2 = createDataValue(dataElementC, periodA, orgUnitC, coc3, coc3, "6"); + dataValueService.addDataValue(dataValueA1); + dataValueService.addDataValue(dataValueA2); + dataValueService.addDataValue(dataValueB1); + dataValueService.addDataValue(dataValueB2); + dataValueService.addDataValue(dataValueC1); + dataValueService.addDataValue(dataValueC2); + } + + @Test + @DisplayName("Deleting audits by category option combo deletes the correct entries") + void testAddGetDataValueAuditFromDataValue() { + // given + DataValueAudit dataValueAuditA1 = + new DataValueAudit( + dataValueA1, + dataValueA1.getValue(), + dataValueA1.getStoredBy(), + AuditOperationType.UPDATE); + dataValueAuditA1.setCategoryOptionCombo(coc1); + DataValueAudit dataValueAuditA2 = + new DataValueAudit( + dataValueA2, + dataValueA2.getValue(), + dataValueA2.getStoredBy(), + AuditOperationType.UPDATE); + dataValueAuditA2.setAttributeOptionCombo(coc1); + DataValueAudit dataValueAuditB1 = + new DataValueAudit( + dataValueB1, + dataValueB1.getValue(), + dataValueB1.getStoredBy(), + AuditOperationType.UPDATE); + dataValueAuditB1.setCategoryOptionCombo(coc2); + DataValueAudit dataValueAuditB2 = + new DataValueAudit( + dataValueB2, + dataValueB2.getValue(), + dataValueB2.getStoredBy(), + AuditOperationType.UPDATE); + dataValueAuditB2.setAttributeOptionCombo(coc2); + DataValueAudit dataValueAuditC1 = + new DataValueAudit( + dataValueC1, + dataValueC1.getValue(), + dataValueC1.getStoredBy(), + AuditOperationType.UPDATE); + dataValueAuditC1.setCategoryOptionCombo(coc3); + DataValueAudit dataValueAuditC2 = + new DataValueAudit( + dataValueC2, + dataValueC2.getValue(), + dataValueC2.getStoredBy(), + AuditOperationType.UPDATE); + dataValueAuditC2.setAttributeOptionCombo(coc3); + + dataValueAuditService.addDataValueAudit(dataValueAuditA1); + dataValueAuditService.addDataValueAudit(dataValueAuditA2); + dataValueAuditService.addDataValueAudit(dataValueAuditB1); + dataValueAuditService.addDataValueAudit(dataValueAuditB2); + dataValueAuditService.addDataValueAudit(dataValueAuditC1); + dataValueAuditService.addDataValueAudit(dataValueAuditC2); + + // state before delete + List dvaCoc1Before = + dataValueAuditStore.getDataValueAudits( + new DataValueAuditQueryParams().setCategoryOptionCombo(coc1)); + List dvaCoc2Before = + dataValueAuditStore.getDataValueAudits( + new DataValueAuditQueryParams().setAttributeOptionCombo(coc2)); + List dvaCoc3Before = + dataValueAuditStore.getDataValueAudits( + new DataValueAuditQueryParams() + .setCategoryOptionCombo(coc3) + .setAttributeOptionCombo(coc3)); + + assertEquals(2, dvaCoc1Before.size(), "There should be 2 audits referencing Cat Opt Combo 1"); + assertEquals(2, dvaCoc2Before.size(), "There should be 2 audits referencing Cat Opt Combo 2"); + assertEquals(2, dvaCoc3Before.size(), "There should be 2 audits referencing Cat Opt Combo 3"); + + // when + dataValueAuditStore.deleteDataValueAudits(coc1); + dataValueAuditStore.deleteDataValueAudits(coc2); + + // then + List dvaCoc1After = + dataValueAuditStore.getDataValueAudits( + new DataValueAuditQueryParams().setCategoryOptionCombo(coc1)); + List dvaCoc2After = + dataValueAuditStore.getDataValueAudits( + new DataValueAuditQueryParams().setAttributeOptionCombo(coc2)); + List dvaCoc3After = + dataValueAuditStore.getDataValueAudits( + new DataValueAuditQueryParams() + .setCategoryOptionCombo(coc3) + .setAttributeOptionCombo(coc3)); + + assertTrue(dvaCoc1After.isEmpty(), "There should be 0 audits referencing Cat Opt Combo 1"); + assertTrue(dvaCoc2After.isEmpty(), "There should be 0 audits referencing Cat Opt Combo 2"); + assertEquals(2, dvaCoc3After.size(), "There should be 2 audits referencing Cat Opt Combo 3"); + assertTrue( + dvaCoc3After.containsAll(List.of(dataValueAuditC1, dataValueAuditC2)), + "Retrieved entries should contain both audits referencing cat opt combo 3"); + } +} diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/datavalue/DataValueStoreTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/datavalue/DataValueStoreTest.java index 413d87e35c8e..2a2db46c84f9 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/datavalue/DataValueStoreTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/datavalue/DataValueStoreTest.java @@ -36,14 +36,17 @@ import jakarta.persistence.PersistenceContext; import java.util.Date; import java.util.List; +import java.util.stream.Collectors; import org.hisp.dhis.category.CategoryOption; import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.common.UID; import org.hisp.dhis.common.ValueType; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.period.Period; import org.hisp.dhis.period.PeriodType; import org.hisp.dhis.period.PeriodTypeEnum; +import org.hisp.dhis.test.api.TestCategoryMetadata; import org.hisp.dhis.test.integration.PostgresIntegrationTestBase; import org.hisp.dhis.util.DateUtils; import org.junit.jupiter.api.DisplayName; @@ -126,6 +129,381 @@ void getDataValuesByDataElement() { "retrieved data values do not contain a data value referencing any of the 2 data elements passed in"); } + @Test + @DisplayName("Get all DataValues by CategoryOptionCombo") + void getDataValuesByCoc() { + // given + Period p1 = + createPeriod(DateUtils.getDate(2023, 1, 1, 1, 1), DateUtils.getDate(2023, 2, 1, 1, 1)); + Period p2 = + createPeriod(DateUtils.getDate(2023, 3, 1, 1, 1), DateUtils.getDate(2023, 4, 1, 1, 1)); + Period p3 = + createPeriod(DateUtils.getDate(2023, 5, 1, 1, 1), DateUtils.getDate(2023, 6, 1, 1, 1)); + + CategoryOptionCombo coc1 = createCategoryOptionCombo('1'); + coc1.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(coc1); + + CategoryOptionCombo coc2 = createCategoryOptionCombo('2'); + coc2.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(coc2); + + CategoryOptionCombo coc3 = createCategoryOptionCombo('3'); + coc3.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(coc3); + + DataValue dv1 = createDataValue('B', p1, "dv test 1"); + dv1.setCategoryOptionCombo(coc1); + DataValue dv2 = createDataValue('C', p2, "dv test 2"); + dv2.setCategoryOptionCombo(coc2); + DataValue dv3 = createDataValue('D', p3, "dv test 3"); + dv3.setCategoryOptionCombo(coc3); + + dataValueStore.addDataValue(dv1); + dataValueStore.addDataValue(dv2); + dataValueStore.addDataValue(dv3); + + // when + List allDataValuesByCoc = + dataValueStore.getAllDataValuesByCatOptCombo(UID.of(coc1, coc2)); + + // then + assertEquals(2, allDataValuesByCoc.size()); + assertTrue( + allDataValuesByCoc.containsAll(List.of(dv1, dv2)), + "retrieved data values contain 2 data values referencing the 2 category opt combos passed in"); + assertFalse( + allDataValuesByCoc.contains(dv3), + "retrieved data values do not contain a data value referencing a COC not used in the query"); + } + + @Test + @DisplayName("Get all DataValues by AttributeOptionCombo") + void getDataValuesByAoc() { + // given + Period p1 = + createPeriod(DateUtils.getDate(2023, 1, 1, 1, 1), DateUtils.getDate(2023, 2, 1, 1, 1)); + Period p2 = + createPeriod(DateUtils.getDate(2023, 3, 1, 1, 1), DateUtils.getDate(2023, 4, 1, 1, 1)); + Period p3 = + createPeriod(DateUtils.getDate(2023, 5, 1, 1, 1), DateUtils.getDate(2023, 6, 1, 1, 1)); + + CategoryOptionCombo aoc1 = createCategoryOptionCombo('1'); + aoc1.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(aoc1); + + CategoryOptionCombo aoc2 = createCategoryOptionCombo('2'); + aoc2.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(aoc2); + + CategoryOptionCombo aoc3 = createCategoryOptionCombo('3'); + aoc3.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(aoc3); + + DataValue dv1 = createDataValue('B', p1, "dv test 1"); + dv1.setAttributeOptionCombo(aoc1); + DataValue dv2 = createDataValue('C', p2, "dv test 2"); + dv2.setAttributeOptionCombo(aoc2); + DataValue dv3 = createDataValue('D', p3, "dv test 3"); + dv3.setAttributeOptionCombo(aoc3); + + dataValueStore.addDataValue(dv1); + dataValueStore.addDataValue(dv2); + dataValueStore.addDataValue(dv3); + + // when + List allDataValuesByAoc = + dataValueStore.getAllDataValuesByAttrOptCombo(UID.of(aoc1, aoc2)); + + // then + assertEquals(2, allDataValuesByAoc.size()); + assertTrue( + allDataValuesByAoc.containsAll(List.of(dv1, dv2)), + "retrieved data values contain 2 data values referencing the 2 attribute opt combos passed in"); + assertFalse( + allDataValuesByAoc.contains(dv3), + "retrieved data values do not contain a data value referencing a AOC not used in the query"); + } + + @Test + @DisplayName( + "Merging duplicate DataValues (cat opt combos) leaves only the last updated (source) value remaining") + void mergeDvWithDuplicatesKeepSource() { + // given + TestCategoryMetadata categoryMetadata = setupCategoryMetadata(); + + Period p1 = createPeriod(DateUtils.getDate(2024, 1, 1), DateUtils.getDate(2023, 2, 1)); + + DataElement de = createDataElement('z'); + manager.persist(de); + + OrganisationUnit ou = createOrganisationUnit("org u 1"); + manager.persist(ou); + + // data values with same period, org unit, data element and attr opt combo + // which will be identified as duplicates during merging + DataValue dv1 = createDataValue('1', p1, "dv test 1"); + dv1.setCategoryOptionCombo(categoryMetadata.coc1()); + dv1.setAttributeOptionCombo(categoryMetadata.coc4()); + dv1.setDataElement(de); + dv1.setSource(ou); + dv1.setLastUpdated(DateUtils.parseDate("2024-12-01")); + + DataValue dv2 = createDataValue('2', p1, "dv test 2 - last updated"); + dv2.setCategoryOptionCombo(categoryMetadata.coc2()); + dv2.setAttributeOptionCombo(categoryMetadata.coc4()); + dv2.setDataElement(de); + dv2.setSource(ou); + dv2.setLastUpdated(DateUtils.parseDate("2025-01-08")); + + DataValue dv3 = createDataValue('3', p1, "dv test 3"); + dv3.setCategoryOptionCombo(categoryMetadata.coc3()); + dv3.setAttributeOptionCombo(categoryMetadata.coc4()); + dv3.setDataElement(de); + dv3.setSource(ou); + dv3.setLastUpdated(DateUtils.parseDate("2024-12-06")); + + DataValue dv4 = createDataValue('4', p1, "dv test 4, untouched"); + dv4.setCategoryOptionCombo(categoryMetadata.coc4()); + dv4.setAttributeOptionCombo(categoryMetadata.coc4()); + dv4.setDataElement(de); + dv4.setSource(ou); + dv4.setLastUpdated(DateUtils.parseDate("2024-11-02")); + + addDataValues(dv1, dv2, dv3, dv4); + + // check pre merge state + List preMergeState = dataValueStore.getAllDataValuesByDataElement(List.of(de)); + + assertEquals(4, preMergeState.size(), "there should be 4 data values"); + checkCocIdsPresent( + preMergeState, + List.of( + categoryMetadata.coc1().getId(), + categoryMetadata.coc2().getId(), + categoryMetadata.coc3().getId(), + categoryMetadata.coc4().getId())); + + // when + mergeDataValues( + categoryMetadata.coc3(), List.of(categoryMetadata.coc1(), categoryMetadata.coc2())); + + // then + List postMergeState = dataValueStore.getAllDataValuesByDataElement(List.of(de)); + + assertEquals(2, postMergeState.size(), "there should be 2 data values"); + checkCocIdsPresent( + preMergeState, List.of(categoryMetadata.coc3().getId(), categoryMetadata.coc4().getId())); + + checkDataValuesPresent( + postMergeState, List.of("dv test 2 - last updated", "dv test 4, untouched")); + + checkDatesPresent( + postMergeState, + List.of(DateUtils.parseDate("2025-01-08"), DateUtils.parseDate("2024-11-02"))); + } + + @Test + @DisplayName( + "Merging duplicate DataValues (cat opt combos) leaves only the last updated (target) value remaining") + void mergeDvWithDuplicatesKeepTarget() { + // given + TestCategoryMetadata categoryMetadata = setupCategoryMetadata(); + + Period p1 = createPeriod(DateUtils.getDate(2024, 1, 1), DateUtils.getDate(2023, 2, 1)); + + DataElement de = createDataElement('z'); + manager.persist(de); + + OrganisationUnit ou = createOrganisationUnit("org u 1"); + manager.persist(ou); + + // data values with same period, org unit, data element and attr opt combo + // which will be identified as duplicates during merging + DataValue dv1 = createDataValue('1', p1, "dv test 1"); + dv1.setCategoryOptionCombo(categoryMetadata.coc1()); + dv1.setAttributeOptionCombo(categoryMetadata.coc4()); + dv1.setDataElement(de); + dv1.setSource(ou); + dv1.setLastUpdated(DateUtils.parseDate("2024-12-01")); + + DataValue dv2 = createDataValue('2', p1, "dv test 2"); + dv2.setCategoryOptionCombo(categoryMetadata.coc2()); + dv2.setAttributeOptionCombo(categoryMetadata.coc4()); + dv2.setDataElement(de); + dv2.setSource(ou); + dv2.setLastUpdated(DateUtils.parseDate("2025-01-02")); + + DataValue dv3 = createDataValue('3', p1, "dv test 3 - last updated"); + dv3.setCategoryOptionCombo(categoryMetadata.coc3()); + dv3.setAttributeOptionCombo(categoryMetadata.coc4()); + dv3.setDataElement(de); + dv3.setSource(ou); + dv3.setLastUpdated(DateUtils.parseDate("2025-01-06")); + + DataValue dv4 = createDataValue('4', p1, "dv test 4, untouched"); + dv4.setCategoryOptionCombo(categoryMetadata.coc4()); + dv4.setAttributeOptionCombo(categoryMetadata.coc4()); + dv4.setDataElement(de); + dv4.setSource(ou); + dv4.setLastUpdated(DateUtils.parseDate("2024-11-02")); + + addDataValues(dv1, dv2, dv3, dv4); + + // check pre merge state + List preMergeState = dataValueStore.getAllDataValuesByDataElement(List.of(de)); + + assertEquals(4, preMergeState.size(), "there should be 4 data values"); + checkCocIdsPresent( + preMergeState, + List.of( + categoryMetadata.coc1().getId(), + categoryMetadata.coc2().getId(), + categoryMetadata.coc3().getId(), + categoryMetadata.coc4().getId())); + + // when + mergeDataValues( + categoryMetadata.coc3(), List.of(categoryMetadata.coc1(), categoryMetadata.coc2())); + + // then + List postMergeState = dataValueStore.getAllDataValuesByDataElement(List.of(de)); + + assertEquals(2, postMergeState.size(), "there should be 2 data values"); + checkCocIdsPresent( + postMergeState, List.of(categoryMetadata.coc3().getId(), categoryMetadata.coc4().getId())); + + checkDataValuesPresent( + postMergeState, List.of("dv test 3 - last updated", "dv test 4, untouched")); + + checkDatesPresent( + postMergeState, + List.of(DateUtils.parseDate("2025-01-06"), DateUtils.parseDate("2024-11-02"))); + } + + @Test + @DisplayName( + "Merging non-duplicate DataValues (cat opt combos) updates the cat opt combo value only") + void mergeDvWithNoDuplicates() { + // given + TestCategoryMetadata categoryMetadata = setupCategoryMetadata(); + + Period p1 = createPeriod(DateUtils.getDate(2024, 1, 1), DateUtils.getDate(2023, 2, 1)); + Period p2 = createPeriod(DateUtils.getDate(2024, 2, 1), DateUtils.getDate(2023, 3, 1)); + Period p3 = createPeriod(DateUtils.getDate(2024, 3, 1), DateUtils.getDate(2023, 4, 1)); + Period p4 = createPeriod(DateUtils.getDate(2024, 4, 1), DateUtils.getDate(2023, 5, 1)); + + DataElement de = createDataElement('z'); + manager.persist(de); + + OrganisationUnit ou = createOrganisationUnit("org u 1"); + manager.persist(ou); + + // data values with different period, so no duplicates detected during merging + DataValue dv1 = createDataValue('1', p1, "dv test 1"); + dv1.setCategoryOptionCombo(categoryMetadata.coc1()); + dv1.setAttributeOptionCombo(categoryMetadata.coc4()); + dv1.setDataElement(de); + dv1.setSource(ou); + dv1.setLastUpdated(DateUtils.parseDate("2024-12-01")); + + DataValue dv2 = createDataValue('2', p2, "dv test 2 - last updated"); + dv2.setCategoryOptionCombo(categoryMetadata.coc2()); + dv2.setAttributeOptionCombo(categoryMetadata.coc4()); + dv2.setDataElement(de); + dv2.setSource(ou); + dv2.setLastUpdated(DateUtils.parseDate("2025-01-08")); + + DataValue dv3 = createDataValue('3', p3, "dv test 3"); + dv3.setCategoryOptionCombo(categoryMetadata.coc3()); + dv3.setAttributeOptionCombo(categoryMetadata.coc4()); + dv3.setDataElement(de); + dv3.setSource(ou); + dv3.setLastUpdated(DateUtils.parseDate("2024-12-06")); + + DataValue dv4 = createDataValue('4', p4, "dv test 4, untouched"); + dv4.setCategoryOptionCombo(categoryMetadata.coc4()); + dv4.setAttributeOptionCombo(categoryMetadata.coc4()); + dv4.setDataElement(de); + dv4.setSource(ou); + dv4.setLastUpdated(DateUtils.parseDate("2024-11-02")); + + addDataValues(dv1, dv2, dv3, dv4); + + // check pre merge state + List preMergeState = dataValueStore.getAllDataValuesByDataElement(List.of(de)); + + assertEquals(4, preMergeState.size(), "there should be 4 data values"); + checkCocIdsPresent( + preMergeState, + List.of( + categoryMetadata.coc1().getId(), + categoryMetadata.coc2().getId(), + categoryMetadata.coc3().getId(), + categoryMetadata.coc4().getId())); + + // when + mergeDataValues( + categoryMetadata.coc3(), List.of(categoryMetadata.coc1(), categoryMetadata.coc2())); + + // then + List postMergeState = dataValueStore.getAllDataValuesByDataElement(List.of(de)); + + assertEquals(4, postMergeState.size(), "there should still be 4 data values"); + checkCocIdsPresent( + postMergeState, List.of(categoryMetadata.coc3().getId(), categoryMetadata.coc4().getId())); + + checkDataValuesPresent( + postMergeState, + List.of("dv test 1", "dv test 2 - last updated", "dv test 3", "dv test 4, untouched")); + + checkDatesPresent( + postMergeState, + List.of( + DateUtils.parseDate("2025-01-08"), + DateUtils.parseDate("2024-11-02"), + DateUtils.parseDate("2024-12-01"), + DateUtils.parseDate("2024-12-06"))); + } + + private void checkDatesPresent(List dataValues, List dates) { + assertTrue( + dataValues.stream() + .map(DataValue::getLastUpdated) + .collect(Collectors.toSet()) + .containsAll(dates), + "Expected dates should be present"); + } + + private void checkDataValuesPresent(List dataValues, List values) { + assertTrue( + dataValues.stream() + .map(DataValue::getValue) + .collect(Collectors.toSet()) + .containsAll(values), + "Expected DataValues should be present"); + } + + private void checkCocIdsPresent(List dataValues, List cocIds) { + assertTrue( + dataValues.stream() + .map(dv -> dv.getCategoryOptionCombo().getId()) + .collect(Collectors.toSet()) + .containsAll(cocIds), + "Data values have expected category option combos"); + } + + private void mergeDataValues(CategoryOptionCombo target, List sources) { + dataValueStore.mergeDataValuesWithCategoryOptionCombos(target, sources); + entityManager.flush(); + entityManager.clear(); + } + + private void addDataValues(DataValue... dvs) { + for (DataValue dv : dvs) dataValueStore.addDataValue(dv); + entityManager.flush(); + } + private DataValue createDataValue(char uniqueChar, Period period, String value) { DataElement dataElement = createDataElement(uniqueChar); dataElement.setValueType(ValueType.TEXT); diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/category/CategoryOptionComboMergeServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/category/CategoryOptionComboMergeServiceTest.java new file mode 100644 index 000000000000..c76f5f8ae5c7 --- /dev/null +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/category/CategoryOptionComboMergeServiceTest.java @@ -0,0 +1,1822 @@ +/* + * Copyright (c) 2004-2024, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.merge.category; + +import static org.hisp.dhis.dataapproval.DataApprovalAction.APPROVE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.hisp.dhis.audit.AuditOperationType; +import org.hisp.dhis.category.Category; +import org.hisp.dhis.category.CategoryCombo; +import org.hisp.dhis.category.CategoryComboStore; +import org.hisp.dhis.category.CategoryOption; +import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.category.CategoryOptionStore; +import org.hisp.dhis.category.CategoryService; +import org.hisp.dhis.common.BaseIdentifiableObject; +import org.hisp.dhis.common.CodeGenerator; +import org.hisp.dhis.common.IdentifiableObjectManager; +import org.hisp.dhis.common.UID; +import org.hisp.dhis.dataapproval.DataApproval; +import org.hisp.dhis.dataapproval.DataApprovalAudit; +import org.hisp.dhis.dataapproval.DataApprovalAuditQueryParams; +import org.hisp.dhis.dataapproval.DataApprovalAuditStore; +import org.hisp.dhis.dataapproval.DataApprovalLevel; +import org.hisp.dhis.dataapproval.DataApprovalStore; +import org.hisp.dhis.dataapproval.DataApprovalWorkflow; +import org.hisp.dhis.dataelement.DataElement; +import org.hisp.dhis.dataelement.DataElementOperand; +import org.hisp.dhis.dataelement.DataElementOperandStore; +import org.hisp.dhis.dataset.CompleteDataSetRegistration; +import org.hisp.dhis.dataset.CompleteDataSetRegistrationStore; +import org.hisp.dhis.dataset.DataSet; +import org.hisp.dhis.datavalue.DataValue; +import org.hisp.dhis.datavalue.DataValueAudit; +import org.hisp.dhis.datavalue.DataValueAuditQueryParams; +import org.hisp.dhis.datavalue.DataValueAuditStore; +import org.hisp.dhis.datavalue.DataValueStore; +import org.hisp.dhis.expression.Expression; +import org.hisp.dhis.feedback.ConflictException; +import org.hisp.dhis.feedback.MergeReport; +import org.hisp.dhis.merge.DataMergeStrategy; +import org.hisp.dhis.merge.MergeParams; +import org.hisp.dhis.merge.MergeService; +import org.hisp.dhis.minmax.MinMaxDataElement; +import org.hisp.dhis.minmax.MinMaxDataElementStore; +import org.hisp.dhis.organisationunit.OrganisationUnit; +import org.hisp.dhis.organisationunit.OrganisationUnitLevel; +import org.hisp.dhis.period.Period; +import org.hisp.dhis.period.PeriodService; +import org.hisp.dhis.period.PeriodType; +import org.hisp.dhis.period.PeriodTypeEnum; +import org.hisp.dhis.predictor.Predictor; +import org.hisp.dhis.predictor.PredictorStore; +import org.hisp.dhis.program.Enrollment; +import org.hisp.dhis.program.Event; +import org.hisp.dhis.program.EventStore; +import org.hisp.dhis.program.Program; +import org.hisp.dhis.program.ProgramStage; +import org.hisp.dhis.sms.command.SMSCommand; +import org.hisp.dhis.sms.command.code.SMSCode; +import org.hisp.dhis.sms.command.hibernate.SMSCommandStore; +import org.hisp.dhis.test.integration.PostgresIntegrationTestBase; +import org.hisp.dhis.trackedentity.TrackedEntity; +import org.hisp.dhis.util.DateUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.transaction.annotation.Transactional; + +/** + * All the tests in this class basically follow the same approach: + * + *

- Create metadata which have source CategoryOptionCombo references + * + *

- Perform a CategoryOptionCombo merge, passing a target CategoryOptionCombo + * + *

- Check that source CategoryOptionCombos have had their references removed/replaced with the + * target CategoryOptionCombo + */ +@Transactional +class CategoryOptionComboMergeServiceTest extends PostgresIntegrationTestBase { + + @Autowired private CategoryService categoryService; + @Autowired private CategoryOptionStore categoryOptionStore; + @Autowired private CategoryComboStore categoryComboStore; + @Autowired private DataElementOperandStore dataElementOperandStore; + @Autowired private MinMaxDataElementStore minMaxDataElementStore; + @Autowired private PredictorStore predictorStore; + @Autowired private SMSCommandStore smsCommandStore; + @Autowired private IdentifiableObjectManager manager; + @Autowired private MergeService categoryOptionComboMergeService; + @Autowired private PeriodService periodService; + @Autowired private DataValueStore dataValueStore; + @Autowired private CompleteDataSetRegistrationStore completeDataSetRegistrationStore; + @Autowired private DataValueAuditStore dataValueAuditStore; + @Autowired private DataApprovalAuditStore dataApprovalAuditStore; + @Autowired private DataApprovalStore dataApprovalStore; + @Autowired private EventStore eventStore; + + private CategoryCombo cc1; + private CategoryOptionCombo cocSource1; + private CategoryOptionCombo cocSource2; + private CategoryOptionCombo cocTarget; + private CategoryOptionCombo cocRandom; + private OrganisationUnit ou1; + private OrganisationUnit ou2; + private OrganisationUnit ou3; + private DataElement de1; + private DataElement de2; + private DataElement de3; + private Program program; + private Period p1; + private Period p2; + private Period p3; + + @BeforeEach + public void setUp() { + // 8 category options + CategoryOption co1A = createCategoryOption("1A", CodeGenerator.generateUid()); + CategoryOption co1B = createCategoryOption("1B", CodeGenerator.generateUid()); + CategoryOption co2A = createCategoryOption("2A", CodeGenerator.generateUid()); + CategoryOption co2B = createCategoryOption("2B", CodeGenerator.generateUid()); + CategoryOption co3A = createCategoryOption("3A", CodeGenerator.generateUid()); + CategoryOption co3B = createCategoryOption("3B", CodeGenerator.generateUid()); + CategoryOption co4A = createCategoryOption("4A", CodeGenerator.generateUid()); + CategoryOption co4B = createCategoryOption("4B", CodeGenerator.generateUid()); + categoryService.addCategoryOption(co1A); + categoryService.addCategoryOption(co1B); + categoryService.addCategoryOption(co2A); + categoryService.addCategoryOption(co2B); + categoryService.addCategoryOption(co3A); + categoryService.addCategoryOption(co3B); + categoryService.addCategoryOption(co4A); + categoryService.addCategoryOption(co4B); + + // 4 categories (each with 2 category options) + Category cat1 = createCategory('1', co1A, co1B); + Category cat2 = createCategory('2', co2A, co2B); + Category cat3 = createCategory('3', co3A, co3B); + Category cat4 = createCategory('4', co4A, co4B); + categoryService.addCategory(cat1); + categoryService.addCategory(cat2); + categoryService.addCategory(cat3); + categoryService.addCategory(cat4); + + cc1 = createCategoryCombo('1', cat1, cat2); + CategoryCombo cc2 = createCategoryCombo('2', cat3, cat4); + categoryService.addCategoryCombo(cc1); + categoryService.addCategoryCombo(cc2); + + categoryService.generateOptionCombos(cc1); + categoryService.generateOptionCombos(cc2); + + cocSource1 = getCocWithOptions("1A", "2A"); + cocSource2 = getCocWithOptions("1B", "2B"); + cocTarget = getCocWithOptions("3A", "4B"); + cocRandom = getCocWithOptions("3B", "4A"); + + ou1 = createOrganisationUnit('A'); + ou2 = createOrganisationUnit('B'); + ou3 = createOrganisationUnit('C'); + manager.save(List.of(ou1, ou2, ou3)); + + de1 = createDataElement('1'); + de2 = createDataElement('2'); + de3 = createDataElement('3'); + manager.save(List.of(de1, de2, de3)); + + program = createProgram('q'); + manager.save(program); + + p1 = createPeriod(DateUtils.parseDate("2024-1-4"), DateUtils.parseDate("2024-1-4")); + p1.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + p2 = createPeriod(DateUtils.parseDate("2024-2-4"), DateUtils.parseDate("2024-2-4")); + p2.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + p3 = createPeriod(DateUtils.parseDate("2024-3-4"), DateUtils.parseDate("2024-3-4")); + p3.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + periodService.addPeriod(p1); + periodService.addPeriod(p2); + periodService.addPeriod(p3); + } + + // ----------------------------- + // ------ CategoryOption ------- + // ----------------------------- + @Test + @DisplayName("CategoryOption refs to source CategoryOptionCombos are replaced, sources deleted") + void categoryOptionRefsReplacedSourcesDeletedTest() throws ConflictException { + // given category option combo state before merge + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + List allCategoryOptions = categoryService.getAllCategoryOptions(); + + assertEquals(9, allCategoryOptionCombos.size(), "9 COCs including 1 default"); + assertEquals(9, allCategoryOptions.size(), "9 COs including 1 default"); + + List coSourcesBefore = + categoryOptionStore.getByCategoryOptionCombo( + List.of(UID.of(cocSource1.getUid()), UID.of(cocSource2.getUid()))); + List coTargetBefore = + categoryOptionStore.getByCategoryOptionCombo(List.of(UID.of(cocTarget.getUid()))); + + assertEquals( + 4, + coSourcesBefore.size(), + "Expect 4 category options with source category option combo refs"); + assertEquals( + 2, + coTargetBefore.size(), + "Expect 2 category options with target category option combo refs"); + + // when + MergeParams mergeParams = getMergeParams(); + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + List coSourcesAfter = + categoryOptionStore.getByCategoryOptionCombo( + List.of(UID.of(cocSource1), UID.of(cocSource2))); + List coTargetAfter = + categoryOptionStore.getByCategoryOptionCombo(List.of(UID.of(cocTarget))); + + assertFalse(report.hasErrorMessages()); + assertEquals( + 0, coSourcesAfter.size(), "Expect 0 entries with source category option combo refs"); + assertEquals( + 6, coTargetAfter.size(), "Expect 6 entries with target category option combo refs"); + + assertTrue( + categoryService.getCategoryOptionCombosByUid(UID.of(cocSource1, cocSource2)).isEmpty(), + "There should be no source COCs after deletion during merge"); + } + + // ----------------------------- + // ------ CategoryCombo ------- + // ----------------------------- + @Test + @DisplayName("CategoryCombo refs to source CategoryOptionCombos are replaced, sources deleted") + void categoryComboRefsReplacedSourcesDeletedTest() throws ConflictException { + // given category option combo state before merge + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + List allCategoryCombos = categoryService.getAllCategoryCombos(); + + assertEquals(9, allCategoryOptionCombos.size(), "9 COCs including 1 default"); + assertEquals(3, allCategoryCombos.size(), "3 CCs including 1 default"); + + List ccSourcesBefore = + categoryComboStore.getByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List ccTargetBefore = + categoryComboStore.getByCategoryOptionCombo(Set.of(UID.of(cocTarget.getUid()))); + + assertEquals( + 1, + ccSourcesBefore.size(), + "Expect 1 category combo with source category option combo refs"); + assertEquals( + 1, ccTargetBefore.size(), "Expect 1 category combo with target category option combo refs"); + assertEquals( + 4, + ccTargetBefore.get(0).getOptionCombos().size(), + "Expect 4 COCs with target category combo"); + + // when + MergeParams mergeParams = getMergeParams(); + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + List allCOCsAfter = categoryService.getAllCategoryOptionCombos(); + List allCCsAfter = categoryService.getAllCategoryCombos(); + + assertEquals(7, allCOCsAfter.size(), "7 COCs including 1 default"); + assertEquals(3, allCCsAfter.size(), "3 CCs including 1 default"); + + // then + List ccSourcesAfter = + categoryComboStore.getByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List ccTargetAfter = + categoryComboStore.getByCategoryOptionCombo(Set.of(UID.of(cocTarget))); + CategoryCombo catCombo1 = categoryComboStore.getByUid(cc1.getUid()); + + assertFalse(report.hasErrorMessages()); + assertEquals( + 0, ccSourcesAfter.size(), "Expect 0 entries with source category option combo refs"); + assertEquals( + 1, ccTargetAfter.size(), "Expect 2 entries with target category option combo refs"); + assertEquals(5, catCombo1.getOptionCombos().size(), "Expect 5 COCs for CC1"); + assertEquals( + 4, + ccTargetAfter.get(0).getOptionCombos().size(), + "Expect 4 COCs with target category combo"); + + assertTrue( + categoryService.getCategoryOptionCombosByUid(UID.of(cocSource1, cocSource2)).isEmpty(), + "There should be no source COCs after deletion during merge"); + } + + // ----------------------------- + // ---- DataElementOperand ----- + // ----------------------------- + @Test + @DisplayName( + "DataElementOperand refs to source CategoryOptionCombos are replaced, sources deleted") + void dataElementOperandRefsReplacedSourcesDeletedTest() throws ConflictException { + DataElementOperand deo1 = new DataElementOperand(); + deo1.setDataElement(de1); + deo1.setCategoryOptionCombo(cocSource1); + + DataElementOperand deo2 = new DataElementOperand(); + deo2.setDataElement(de2); + deo2.setCategoryOptionCombo(cocSource2); + + DataElementOperand deo3 = new DataElementOperand(); + deo3.setDataElement(de3); + deo3.setCategoryOptionCombo(cocTarget); + + manager.save(List.of(de1, de2, de3)); + manager.save(List.of(deo1, deo2, deo3)); + + // given state before merge + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertEquals(9, allCategoryOptionCombos.size(), "9 COCs including 1 default"); + + List deoSourcesBefore = + dataElementOperandStore.getByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List deoTargetBefore = + dataElementOperandStore.getByCategoryOptionCombo(Set.of(UID.of(cocTarget.getUid()))); + + assertEquals( + 2, + deoSourcesBefore.size(), + "Expect 2 data element operands with source category option combo refs"); + assertEquals( + 1, + deoTargetBefore.size(), + "Expect 1 data element operand with target category option combo refs"); + + // when + MergeParams mergeParams = getMergeParams(); + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + List allCOCsAfter = categoryService.getAllCategoryOptionCombos(); + + assertEquals(7, allCOCsAfter.size(), "7 COCs including 1 default"); + + // then + List deoSourcesAfter = + dataElementOperandStore.getByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List deoTargetAfter = + dataElementOperandStore.getByCategoryOptionCombo(Set.of(UID.of(cocTarget.getUid()))); + + assertFalse(report.hasErrorMessages()); + assertEquals( + 0, deoSourcesAfter.size(), "Expect 0 entries with source category option combo refs"); + assertEquals( + 3, deoTargetAfter.size(), "Expect 3 entries with target category option combo refs"); + } + + // ----------------------------- + // ---- MinMaxDataElement ----- + // ----------------------------- + @Test + @DisplayName( + "MinMaxDataElement refs to source CategoryOptionCombos are replaced, sources deleted") + void minMaxDataElementRefsReplacedSourcesDeletedTest() throws ConflictException { + OrganisationUnit ou1 = createOrganisationUnit('1'); + OrganisationUnit ou2 = createOrganisationUnit('2'); + OrganisationUnit ou3 = createOrganisationUnit('3'); + manager.save(List.of(ou1, ou2, ou3)); + + MinMaxDataElement mmde1 = new MinMaxDataElement(de1, ou1, cocSource1, 0, 100, false); + MinMaxDataElement mmde2 = new MinMaxDataElement(de2, ou2, cocSource2, 0, 100, false); + MinMaxDataElement mmde3 = new MinMaxDataElement(de3, ou3, cocTarget, 0, 100, false); + minMaxDataElementStore.save(mmde1); + minMaxDataElementStore.save(mmde2); + minMaxDataElementStore.save(mmde3); + + // given state before merge + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertEquals(9, allCategoryOptionCombos.size(), "9 COCs including 1 default"); + + List mmdeSourcesBefore = + minMaxDataElementStore.getByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List mmdeTargetBefore = + minMaxDataElementStore.getByCategoryOptionCombo(Set.of(UID.of(cocTarget.getUid()))); + + assertEquals( + 2, + mmdeSourcesBefore.size(), + "Expect 2 min max data elements with source category option combo refs"); + assertEquals( + 1, + mmdeTargetBefore.size(), + "Expect 1 min max data element with target category option combo refs"); + + // when + MergeParams mergeParams = getMergeParams(); + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + List allCOCsAfter = categoryService.getAllCategoryOptionCombos(); + + assertEquals(7, allCOCsAfter.size(), "7 COCs including 1 default"); + + // then + List mmdeSourcesAfter = + minMaxDataElementStore.getByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List mmdeTargetAfter = + minMaxDataElementStore.getByCategoryOptionCombo(Set.of(UID.of(cocTarget.getUid()))); + + assertFalse(report.hasErrorMessages()); + assertEquals( + 0, mmdeSourcesAfter.size(), "Expect 0 entries with source category option combo refs"); + assertEquals( + 3, mmdeTargetAfter.size(), "Expect 3 entries with target category option combo refs"); + } + + // ---------------------- + // ---- Predictor ----- + // ---------------------- + @Test + @DisplayName("Predictor refs to source CategoryOptionCombos are replaced, sources deleted") + void predictorRefsReplacedSourcesDeletedTest() throws ConflictException { + OrganisationUnitLevel ouLevel = new OrganisationUnitLevel(1, "Level 1"); + manager.save(ouLevel); + + Expression exp1 = new Expression("#{uid00001}", de1.getUid()); + Expression exp2 = new Expression("#{uid00002}", de2.getUid()); + Expression exp3 = new Expression("#{uid00003}", de3.getUid()); + + Predictor p1 = + createPredictor( + de1, + cocSource1, + "1", + exp1, + exp1, + PeriodType.getPeriodTypeByName("Monthly"), + ouLevel, + 0, + 1, + 1); + + Predictor p2 = + createPredictor( + de2, + cocSource2, + "2", + exp2, + exp2, + PeriodType.getPeriodTypeByName("Monthly"), + ouLevel, + 0, + 0, + 0); + + Predictor p3 = + createPredictor( + de3, + cocTarget, + "3", + exp3, + exp3, + PeriodType.getPeriodTypeByName("Monthly"), + ouLevel, + 1, + 3, + 2); + + manager.save(List.of(p1, p2, p3)); + + // given state before merge + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertEquals(9, allCategoryOptionCombos.size(), "9 COCs including 1 default"); + + List pSourcesBefore = + predictorStore.getByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List pTargetBefore = + predictorStore.getByCategoryOptionCombo(Set.of(UID.of(cocTarget.getUid()))); + + assertEquals( + 2, pSourcesBefore.size(), "Expect 2 predictors with source category option combo refs"); + assertEquals( + 1, pTargetBefore.size(), "Expect 1 predictor with target category option combo refs"); + + // when + MergeParams mergeParams = getMergeParams(); + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + List allCOCsAfter = categoryService.getAllCategoryOptionCombos(); + + assertEquals(7, allCOCsAfter.size(), "7 COCs including 1 default"); + + // then + List pSourcesAfter = + predictorStore.getByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List pTargetAfter = + predictorStore.getByCategoryOptionCombo(Set.of(UID.of(cocTarget.getUid()))); + + assertFalse(report.hasErrorMessages()); + assertEquals( + 0, pSourcesAfter.size(), "Expect 0 entries with source category option combo refs"); + assertEquals(3, pTargetAfter.size(), "Expect 3 entries with target category option combo refs"); + } + + // -------------------- + // ---- SMSCode ----- + // -------------------- + @Test + @DisplayName("SMSCode refs to source CategoryOptionCombos are replaced, sources deleted") + void smsCodeRefsReplacedSourcesDeletedTest() throws ConflictException { + SMSCode smsCode1 = new SMSCode(); + smsCode1.setDataElement(de1); + smsCode1.setOptionId(cocSource1); + + SMSCode smsCode2 = new SMSCode(); + smsCode2.setDataElement(de2); + smsCode2.setOptionId(cocSource2); + + SMSCode smsCode3 = new SMSCode(); + smsCode3.setDataElement(de3); + smsCode3.setOptionId(cocTarget); + + SMSCommand smsCommand = new SMSCommand(); + smsCommand.setCodes(Set.of(smsCode1, smsCode2, smsCode3)); + + smsCommandStore.save(smsCommand); + + // given state before merge + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertEquals(9, allCategoryOptionCombos.size(), "9 COCs including 1 default"); + + List cSourcesBefore = + smsCommandStore.getCodesByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List cTargetBefore = + smsCommandStore.getCodesByCategoryOptionCombo(Set.of(UID.of(cocTarget.getUid()))); + + assertEquals(2, cSourcesBefore.size(), "Expect 2 code with source category option combo refs"); + assertEquals(1, cTargetBefore.size(), "Expect 1 code with target category option combo refs"); + + // when + MergeParams mergeParams = getMergeParams(); + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + List allCOCsAfter = categoryService.getAllCategoryOptionCombos(); + + assertEquals(7, allCOCsAfter.size(), "7 COCs including 1 default"); + + // then + List cSourcesAfter = + smsCommandStore.getCodesByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List cTargetAfter = + smsCommandStore.getCodesByCategoryOptionCombo(Set.of(UID.of(cocTarget.getUid()))); + + assertFalse(report.hasErrorMessages()); + assertEquals( + 0, cSourcesAfter.size(), "Expect 0 entries with source category option combo refs"); + assertEquals(3, cTargetAfter.size(), "Expect 3 entries with target category option combo refs"); + } + + // ------------------------------------- + // -- DataValue Category Option Combo -- + // ------------------------------------- + @Test + @DisplayName( + "Non-duplicate DataValues with references to source COCs are replaced with target COC using LAST_UPDATED strategy") + void dataValueMergeCocLastUpdatedTest() throws ConflictException { + // given + DataValue dv1 = createDataValue(de1, p1, ou1, cocSource1, cocRandom, "value1"); + DataValue dv2 = createDataValue(de2, p2, ou1, cocSource2, cocRandom, "value2"); + DataValue dv3 = createDataValue(de3, p3, ou1, cocTarget, cocRandom, "value3"); + + dataValueStore.addDataValue(dv1); + dataValueStore.addDataValue(dv2); + dataValueStore.addDataValue(dv3); + + // params + MergeParams mergeParams = getMergeParams(); + mergeParams.setDataMergeStrategy(DataMergeStrategy.LAST_UPDATED); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + List sourceItems = + dataValueStore.getAllDataValuesByCatOptCombo(UID.of(cocSource1, cocSource2)); + List targetItems = + dataValueStore.getAllDataValuesByCatOptCombo(List.of(UID.of(cocTarget))); + + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertFalse(report.hasErrorMessages()); + assertEquals(0, sourceItems.size(), "Expect 0 entries with source COC refs"); + assertEquals(3, targetItems.size(), "Expect 3 entries with target COC refs"); + assertEquals(7, allCategoryOptionCombos.size(), "Expect 7 COCs present"); + assertTrue(allCategoryOptionCombos.contains(cocTarget), "Target COC should be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource1), "Source COC should not be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource2), "Source COC should not be present"); + } + + @Test + @DisplayName("DataValues with references to source COCs are deleted using DISCARD strategy") + void dataValueMergeCocDiscardTest() throws ConflictException { + // given + DataValue dv1 = createDataValue(de1, p1, ou1, cocSource1, cocRandom, "value1"); + DataValue dv2 = createDataValue(de2, p2, ou1, cocSource2, cocRandom, "value2"); + DataValue dv3 = createDataValue(de3, p3, ou1, cocTarget, cocRandom, "value3"); + + dataValueStore.addDataValue(dv1); + dataValueStore.addDataValue(dv2); + dataValueStore.addDataValue(dv3); + + // params + MergeParams mergeParams = getMergeParams(); + mergeParams.setDataMergeStrategy(DataMergeStrategy.DISCARD); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + List sourceItems = + dataValueStore.getAllDataValuesByCatOptCombo(UID.of(cocSource1, cocSource2)); + List targetItems = + dataValueStore.getAllDataValuesByCatOptCombo(List.of(UID.of(cocTarget))); + + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertFalse(report.hasErrorMessages()); + assertEquals(0, sourceItems.size(), "Expect 0 entries with source COC refs"); + assertEquals(1, targetItems.size(), "Expect 1 entry with target COC ref only"); + assertEquals(7, allCategoryOptionCombos.size(), "Expect 7 COCs present"); + assertTrue(allCategoryOptionCombos.contains(cocTarget), "Target COC should be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource1), "Source COC should not be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource2), "Source COC should not be present"); + } + + // -------------------------------------- + // -- DataValue Attribute Option Combo -- + // -------------------------------------- + @Test + @DisplayName( + "Non-duplicate DataValues with references to source AOCs are replaced with target AOC using LAST_UPDATED strategy") + void dataValueMergeAocLastUpdatedTest() throws ConflictException { + // given + DataValue dv1 = createDataValue(de1, p1, ou1, cocRandom, cocSource1, "value1"); + DataValue dv2 = createDataValue(de2, p2, ou1, cocRandom, cocSource2, "value2"); + DataValue dv3 = createDataValue(de3, p3, ou1, cocRandom, cocTarget, "value3"); + + dataValueStore.addDataValue(dv1); + dataValueStore.addDataValue(dv2); + dataValueStore.addDataValue(dv3); + + // params + MergeParams mergeParams = getMergeParams(); + mergeParams.setDataMergeStrategy(DataMergeStrategy.LAST_UPDATED); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + List sourceItems = + dataValueStore.getAllDataValuesByAttrOptCombo(UID.of(cocSource1, cocSource2)); + List targetItems = + dataValueStore.getAllDataValuesByAttrOptCombo(List.of(UID.of(cocTarget))); + + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertFalse(report.hasErrorMessages()); + assertEquals(0, sourceItems.size(), "Expect 0 entries with source COC refs"); + assertEquals(3, targetItems.size(), "Expect 3 entries with target COC refs"); + assertEquals(7, allCategoryOptionCombos.size(), "Expect 7 COCs present"); + assertTrue(allCategoryOptionCombos.contains(cocTarget), "Target COC should be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource1), "Source COC should not be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource2), "Source COC should not be present"); + } + + @Test + @DisplayName("DataValues with references to source AOCs are deleted, using DISCARD strategy") + void dataValueMergeAocDiscardTest() throws ConflictException { + // given + DataValue dv1 = createDataValue(de1, p1, ou1, cocRandom, cocSource1, "value1"); + DataValue dv2 = createDataValue(de2, p2, ou1, cocRandom, cocSource2, "value2"); + DataValue dv3 = createDataValue(de3, p3, ou1, cocRandom, cocTarget, "value3"); + + dataValueStore.addDataValue(dv1); + dataValueStore.addDataValue(dv2); + dataValueStore.addDataValue(dv3); + + // params + MergeParams mergeParams = getMergeParams(); + mergeParams.setDataMergeStrategy(DataMergeStrategy.DISCARD); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + List sourceItems = + dataValueStore.getAllDataValuesByAttrOptCombo(UID.of(cocSource1, cocSource2)); + List targetItems = + dataValueStore.getAllDataValuesByAttrOptCombo(List.of(UID.of(cocTarget))); + + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertFalse(report.hasErrorMessages()); + assertEquals(0, sourceItems.size(), "Expect 0 entries with source COC refs"); + assertEquals(1, targetItems.size(), "Expect 1 entry with target COC ref only"); + assertEquals(7, allCategoryOptionCombos.size(), "Expect 7 COCs present"); + assertTrue(allCategoryOptionCombos.contains(cocTarget), "Target COC should be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource1), "Source COC should not be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource2), "Source COC should not be present"); + } + + // ------------------------ + // -- DataValueAudit -- + // ------------------------ + @Test + @DisplayName( + "DataValueAudits with references to source COCs are not changed or deleted when sources not deleted") + void dataValueAuditMergeTest() throws ConflictException { + // given + DataValueAudit dva1 = createDataValueAudit(cocSource1, "1", p1); + DataValueAudit dva2 = createDataValueAudit(cocSource1, "2", p1); + DataValueAudit dva3 = createDataValueAudit(cocSource2, "1", p1); + DataValueAudit dva4 = createDataValueAudit(cocSource2, "2", p1); + DataValueAudit dva5 = createDataValueAudit(cocTarget, "1", p1); + + dataValueAuditStore.addDataValueAudit(dva1); + dataValueAuditStore.addDataValueAudit(dva2); + dataValueAuditStore.addDataValueAudit(dva3); + dataValueAuditStore.addDataValueAudit(dva4); + dataValueAuditStore.addDataValueAudit(dva5); + + // params + MergeParams mergeParams = getMergeParams(); + mergeParams.setDeleteSources(false); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + DataValueAuditQueryParams source1DvaQueryParams = getQueryParams(cocSource1); + DataValueAuditQueryParams source2DvaQueryParams = getQueryParams(cocSource2); + DataValueAuditQueryParams targetDvaQueryParams = getQueryParams(cocTarget); + + List source1Audits = + dataValueAuditStore.getDataValueAudits(source1DvaQueryParams); + List source2Audits = + dataValueAuditStore.getDataValueAudits(source2DvaQueryParams); + + List targetItems = dataValueAuditStore.getDataValueAudits(targetDvaQueryParams); + + assertFalse(report.hasErrorMessages()); + assertEquals( + 4, source1Audits.size() + source2Audits.size(), "Expect 4 entries with source COC refs"); + assertEquals(1, targetItems.size(), "Expect 1 entry with target COC ref"); + } + + @Test + @DisplayName( + "DataValueAudits with references to source COCs are deleted when sources are deleted") + void dataValueAuditMergeDeleteTest() throws ConflictException { + // given + DataValueAudit dva1 = createDataValueAudit(cocSource1, "1", p1); + DataValueAudit dva2 = createDataValueAudit(cocSource1, "2", p1); + DataValueAudit dva3 = createDataValueAudit(cocSource2, "1", p1); + DataValueAudit dva4 = createDataValueAudit(cocSource2, "2", p1); + DataValueAudit dva5 = createDataValueAudit(cocTarget, "1", p1); + + dataValueAuditStore.addDataValueAudit(dva1); + dataValueAuditStore.addDataValueAudit(dva2); + dataValueAuditStore.addDataValueAudit(dva3); + dataValueAuditStore.addDataValueAudit(dva4); + dataValueAuditStore.addDataValueAudit(dva5); + + // params + MergeParams mergeParams = getMergeParams(); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + DataValueAuditQueryParams source1DvaQueryParams = getQueryParams(cocSource1); + DataValueAuditQueryParams source2DvaQueryParams = getQueryParams(cocSource2); + DataValueAuditQueryParams targetDvaQueryParams = getQueryParams(cocTarget); + + List source1Audits = + dataValueAuditStore.getDataValueAudits(source1DvaQueryParams); + List source2Audits = + dataValueAuditStore.getDataValueAudits(source2DvaQueryParams); + + List targetItems = dataValueAuditStore.getDataValueAudits(targetDvaQueryParams); + + assertFalse(report.hasErrorMessages()); + assertEquals( + 0, source1Audits.size() + source2Audits.size(), "Expect 0 entries with source COC refs"); + assertEquals(1, targetItems.size(), "Expect 1 entry with target COC ref"); + } + + // ------------------------ + // -- DataApprovalAudit -- + // ------------------------ + @Test + @DisplayName( + "DataApprovalAudits with references to source COCs are not changed or deleted when sources not deleted") + void dataApprovalAuditMergeTest() throws ConflictException { + // given + DataApprovalLevel level1 = new DataApprovalLevel(); + level1.setLevel(1); + level1.setName("DAL1"); + manager.save(level1); + + DataApprovalLevel level2 = new DataApprovalLevel(); + level2.setLevel(2); + level2.setName("DAL2"); + manager.save(level2); + + DataApprovalWorkflow daw = new DataApprovalWorkflow(); + daw.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + daw.setName("DAW"); + daw.setCategoryCombo(cc1); + manager.save(daw); + + DataApprovalAudit daa1 = createDataApprovalAudit(cocSource1, level1, daw, p1); + DataApprovalAudit daa2 = createDataApprovalAudit(cocSource1, level2, daw, p2); + DataApprovalAudit daa3 = createDataApprovalAudit(cocSource2, level1, daw, p1); + DataApprovalAudit daa4 = createDataApprovalAudit(cocSource2, level2, daw, p2); + DataApprovalAudit daa5 = createDataApprovalAudit(cocTarget, level1, daw, p1); + + dataApprovalAuditStore.save(daa1); + dataApprovalAuditStore.save(daa2); + dataApprovalAuditStore.save(daa3); + dataApprovalAuditStore.save(daa4); + dataApprovalAuditStore.save(daa5); + + // params + MergeParams mergeParams = getMergeParams(); + mergeParams.setDeleteSources(false); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + DataApprovalAuditQueryParams targetDaaQueryParams = + new DataApprovalAuditQueryParams() + .setAttributeOptionCombos(new HashSet<>(Collections.singletonList(cocTarget))) + .setLevels(Set.of(level1)); + + List sourceAudits = dataApprovalAuditStore.getAll(); + List targetItems = + dataApprovalAuditStore.getDataApprovalAudits(targetDaaQueryParams); + + assertFalse(report.hasErrorMessages()); + assertEquals(5, sourceAudits.size(), "Expect 4 entries with source COC refs"); + assertEquals(1, targetItems.size(), "Expect 1 entry with target COC ref"); + } + + @Test + @DisplayName( + "DataApprovalAudits with references to source COCs are deleted when sources are deleted") + void dataApprovalAuditMergeDeleteTest() throws ConflictException { + // given + DataApprovalLevel dataApprovalLevel = new DataApprovalLevel(); + dataApprovalLevel.setLevel(1); + dataApprovalLevel.setName("DAL"); + manager.save(dataApprovalLevel); + + DataApprovalWorkflow daw = new DataApprovalWorkflow(); + daw.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + daw.setName("DAW"); + daw.setCategoryCombo(cc1); + manager.save(daw); + + DataApprovalAudit daa1 = createDataApprovalAudit(cocSource1, dataApprovalLevel, daw, p1); + DataApprovalAudit daa2 = createDataApprovalAudit(cocSource1, dataApprovalLevel, daw, p1); + DataApprovalAudit daa3 = createDataApprovalAudit(cocSource2, dataApprovalLevel, daw, p1); + DataApprovalAudit daa4 = createDataApprovalAudit(cocSource2, dataApprovalLevel, daw, p1); + DataApprovalAudit daa5 = createDataApprovalAudit(cocTarget, dataApprovalLevel, daw, p1); + + dataApprovalAuditStore.save(daa1); + dataApprovalAuditStore.save(daa2); + dataApprovalAuditStore.save(daa3); + dataApprovalAuditStore.save(daa4); + dataApprovalAuditStore.save(daa5); + + // params + MergeParams mergeParams = getMergeParams(); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + DataApprovalAuditQueryParams source1DaaQueryParams = + new DataApprovalAuditQueryParams() + .setAttributeOptionCombos(new HashSet<>(Arrays.asList(cocSource1, cocSource2))); + DataApprovalAuditQueryParams targetDaaQueryParams = + new DataApprovalAuditQueryParams() + .setAttributeOptionCombos(new HashSet<>(Collections.singletonList(cocTarget))); + + List sourceAudits = + dataApprovalAuditStore.getDataApprovalAudits(source1DaaQueryParams); + List targetItems = + dataApprovalAuditStore.getDataApprovalAudits(targetDaaQueryParams); + + assertFalse(report.hasErrorMessages()); + assertEquals(0, sourceAudits.size(), "Expect 0 entries with source COC refs"); + assertEquals(1, targetItems.size(), "Expect 1 entry with target COC ref"); + } + + // ----------------------- + // ---- DataApproval ---- + // ----------------------- + @Test + @DisplayName( + "Non-duplicate DataApprovals with references to source COCs are replaced with target COC using LAST_UPDATED strategy") + void dataApprovalMergeCocLastUpdatedTest() throws ConflictException { + // given + DataApprovalLevel level1 = new DataApprovalLevel(); + level1.setLevel(1); + level1.setName("DAL1"); + manager.save(level1); + + DataApprovalLevel level2 = new DataApprovalLevel(); + level2.setLevel(2); + level2.setName("DAL2"); + manager.save(level2); + + DataApprovalWorkflow daw1 = new DataApprovalWorkflow(); + daw1.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + daw1.setName("DAW1"); + daw1.setCategoryCombo(cc1); + manager.save(daw1); + + DataApprovalWorkflow daw2 = new DataApprovalWorkflow(); + daw2.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + daw2.setName("DAW2"); + daw2.setCategoryCombo(cc1); + manager.save(daw2); + + DataApproval da1 = createDataApproval(cocSource1, level1, daw1, p1, ou1); + DataApproval da2 = createDataApproval(cocSource2, level2, daw1, p2, ou1); + DataApproval da3 = createDataApproval(cocTarget, level2, daw2, p2, ou2); + DataApproval da4 = createDataApproval(cocRandom, level2, daw2, p3, ou3); + + dataApprovalStore.addDataApproval(da1); + dataApprovalStore.addDataApproval(da2); + dataApprovalStore.addDataApproval(da3); + dataApprovalStore.addDataApproval(da4); + + // pre-merge state + List sourcesPreMerge = + dataApprovalStore.getByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List targetPreMerge = + dataApprovalStore.getByCategoryOptionCombo(List.of(UID.of(cocTarget))); + assertEquals(2, sourcesPreMerge.size(), "Expect 2 entries with source COC refs"); + assertEquals(1, targetPreMerge.size(), "Expect 1 entries with target COC refs"); + + // params + MergeParams mergeParams = getMergeParams(); + mergeParams.setDataMergeStrategy(DataMergeStrategy.LAST_UPDATED); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + List sourceItems = + dataApprovalStore.getByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List targetItems = + dataApprovalStore.getByCategoryOptionCombo(List.of(UID.of(cocTarget))); + + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertFalse(report.hasErrorMessages()); + assertEquals(0, sourceItems.size(), "Expect 0 entries with source COC refs"); + assertEquals(3, targetItems.size(), "Expect 3 entries with target COC refs"); + assertEquals(7, allCategoryOptionCombos.size(), "Expect 7 COCs present"); + assertTrue(allCategoryOptionCombos.contains(cocTarget), "Target COC should be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource1), "Source COC should not be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource2), "Source COC should not be present"); + } + + @Test + @DisplayName( + "Duplicate DataApprovals are replaced with target COC using LAST_UPDATED strategy, target has latest lastUpdated value") + void duplicateDataApprovalMergeCocLastUpdatedTest() throws ConflictException { + // given + DataApprovalLevel level1 = new DataApprovalLevel(); + level1.setLevel(1); + level1.setName("DAL1"); + manager.save(level1); + + DataApprovalLevel level2 = new DataApprovalLevel(); + level2.setLevel(2); + level2.setName("DAL2"); + manager.save(level2); + + DataApprovalWorkflow daw1 = new DataApprovalWorkflow(); + daw1.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + daw1.setName("DAW1"); + daw1.setCategoryCombo(cc1); + manager.save(daw1); + + DataApprovalWorkflow daw2 = new DataApprovalWorkflow(); + daw2.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + daw2.setName("DAW2"); + daw2.setCategoryCombo(cc1); + manager.save(daw2); + + DataApproval da1a = createDataApproval(cocSource1, level1, daw1, p1, ou1); + da1a.setLastUpdated(DateUtils.parseDate("2024-6-8")); + DataApproval da1b = createDataApproval(cocSource1, level1, daw1, p2, ou1); + da1b.setLastUpdated(DateUtils.parseDate("2024-10-8")); + DataApproval da2a = createDataApproval(cocSource2, level1, daw1, p1, ou1); + da2a.setLastUpdated(DateUtils.parseDate("2024-6-8")); + DataApproval da2b = createDataApproval(cocSource2, level1, daw1, p2, ou1); + da2b.setLastUpdated(DateUtils.parseDate("2024-10-8")); + DataApproval da3a = createDataApproval(cocTarget, level1, daw1, p1, ou1); + da3a.setLastUpdated(DateUtils.parseDate("2024-12-8")); + DataApproval da3b = createDataApproval(cocTarget, level1, daw1, p2, ou1); + da3b.setLastUpdated(DateUtils.parseDate("2024-12-9")); + DataApproval da4a = createDataApproval(cocRandom, level1, daw1, p1, ou1); + DataApproval da4b = createDataApproval(cocRandom, level1, daw1, p2, ou1); + + dataApprovalStore.addDataApproval(da1a); + dataApprovalStore.addDataApproval(da1b); + dataApprovalStore.addDataApproval(da2a); + dataApprovalStore.addDataApproval(da2b); + dataApprovalStore.addDataApproval(da3a); + dataApprovalStore.addDataApproval(da3b); + dataApprovalStore.addDataApproval(da4a); + dataApprovalStore.addDataApproval(da4b); + + // pre-merge state + List sourcesPreMerge = + dataApprovalStore.getByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List targetPreMerge = + dataApprovalStore.getByCategoryOptionCombo(List.of(UID.of(cocTarget))); + assertEquals(4, sourcesPreMerge.size(), "Expect 4 entries with source COC refs"); + assertEquals(2, targetPreMerge.size(), "Expect 2 entries with target COC refs"); + + // params + MergeParams mergeParams = getMergeParams(); + mergeParams.setDataMergeStrategy(DataMergeStrategy.LAST_UPDATED); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + List sourceItems = + dataApprovalStore.getByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List targetItems = + dataApprovalStore.getByCategoryOptionCombo(List.of(UID.of(cocTarget))); + + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertFalse(report.hasErrorMessages()); + assertEquals(0, sourceItems.size(), "Expect 0 entries with source COC refs"); + assertEquals(2, targetItems.size(), "Expect 2 entries with target COC refs"); + assertEquals( + Set.of("2024-12-08", "2024-12-09"), + targetItems.stream() + .map(da -> DateUtils.toMediumDate(da.getLastUpdated())) + .collect(Collectors.toSet()), + "target items should contain the original target Data Approvals lastUpdated dates"); + assertEquals(7, allCategoryOptionCombos.size(), "Expect 7 COCs present"); + assertTrue(allCategoryOptionCombos.contains(cocTarget), "Target COC should be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource1), "Source COC should not be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource2), "Source COC should not be present"); + } + + @Test + @DisplayName( + "Duplicate & non-duplicate DataApprovals are replaced with target COC using LAST_UPDATED strategy") + void duplicateAndNonDuplicateDataApprovalMergeTest() throws ConflictException { + // given + DataApprovalLevel level1 = new DataApprovalLevel(); + level1.setLevel(1); + level1.setName("DAL1"); + manager.save(level1); + + DataApprovalLevel level2 = new DataApprovalLevel(); + level2.setLevel(2); + level2.setName("DAL2"); + manager.save(level2); + + DataApprovalWorkflow daw1 = new DataApprovalWorkflow(); + daw1.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + daw1.setName("DAW1"); + daw1.setCategoryCombo(cc1); + manager.save(daw1); + + DataApprovalWorkflow daw2 = new DataApprovalWorkflow(); + daw2.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + daw2.setName("DAW2"); + daw2.setCategoryCombo(cc1); + manager.save(daw2); + + DataApproval da1a = createDataApproval(cocSource1, level1, daw1, p1, ou1); + da1a.setLastUpdated(DateUtils.parseDate("2024-12-8")); + DataApproval da1b = createDataApproval(cocSource1, level1, daw1, p2, ou1); + da1b.setLastUpdated(DateUtils.parseDate("2024-10-8")); + DataApproval da2a = createDataApproval(cocSource2, level1, daw1, p1, ou1); + da2a.setLastUpdated(DateUtils.parseDate("2024-6-8")); + DataApproval da2b = createDataApproval(cocSource2, level1, daw1, p2, ou1); + da2b.setLastUpdated(DateUtils.parseDate("2024-10-8")); + DataApproval da3a = createDataApproval(cocTarget, level1, daw1, p1, ou1); + da3a.setLastUpdated(DateUtils.parseDate("2024-12-1")); + DataApproval da3b = createDataApproval(cocTarget, level1, daw1, p2, ou1); + da3b.setLastUpdated(DateUtils.parseDate("2024-12-9")); + DataApproval da4a = createDataApproval(cocRandom, level1, daw1, p1, ou1); + DataApproval da4b = createDataApproval(cocRandom, level1, daw1, p2, ou1); + + dataApprovalStore.addDataApproval(da1a); + dataApprovalStore.addDataApproval(da1b); + dataApprovalStore.addDataApproval(da2a); + dataApprovalStore.addDataApproval(da2b); + dataApprovalStore.addDataApproval(da3a); + dataApprovalStore.addDataApproval(da3b); + dataApprovalStore.addDataApproval(da4a); + dataApprovalStore.addDataApproval(da4b); + + // pre-merge state + List sourcesPreMerge = + dataApprovalStore.getByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List targetPreMerge = + dataApprovalStore.getByCategoryOptionCombo(List.of(UID.of(cocTarget))); + assertEquals(4, sourcesPreMerge.size(), "Expect 4 entries with source COC refs"); + assertEquals(2, targetPreMerge.size(), "Expect 2 entries with target COC refs"); + + // params + MergeParams mergeParams = getMergeParams(); + mergeParams.setDataMergeStrategy(DataMergeStrategy.LAST_UPDATED); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + List sourceItems = + dataApprovalStore.getByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List targetItems = + dataApprovalStore.getByCategoryOptionCombo(List.of(UID.of(cocTarget))); + + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertFalse(report.hasErrorMessages()); + assertEquals(0, sourceItems.size(), "Expect 0 entries with source COC refs"); + assertEquals(2, targetItems.size(), "Expect 2 entries with target COC refs"); + assertEquals( + Set.of("2024-12-08", "2024-12-09"), + targetItems.stream() + .map(da -> DateUtils.toMediumDate(da.getLastUpdated())) + .collect(Collectors.toSet()), + "target items should contain the original target Data Approvals lastUpdated dates"); + assertEquals(7, allCategoryOptionCombos.size(), "Expect 7 COCs present"); + assertTrue(allCategoryOptionCombos.contains(cocTarget), "Target COC should be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource1), "Source COC should not be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource2), "Source COC should not be present"); + } + + @Test + @DisplayName( + "Duplicate DataApprovals are replaced with target COC using LAST_UPDATED strategy, sources have latest lastUpdated value") + void duplicateDataApprovalSourceLastUpdatedTest() throws ConflictException { + // given + DataApprovalLevel level1 = new DataApprovalLevel(); + level1.setLevel(1); + level1.setName("DAL1"); + manager.save(level1); + + DataApprovalLevel level2 = new DataApprovalLevel(); + level2.setLevel(2); + level2.setName("DAL2"); + manager.save(level2); + + DataApprovalWorkflow daw1 = new DataApprovalWorkflow(); + daw1.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + daw1.setName("DAW1"); + daw1.setCategoryCombo(cc1); + manager.save(daw1); + + DataApprovalWorkflow daw2 = new DataApprovalWorkflow(); + daw2.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + daw2.setName("DAW2"); + daw2.setCategoryCombo(cc1); + manager.save(daw2); + + DataApproval da1a = createDataApproval(cocSource1, level1, daw1, p1, ou1); + da1a.setLastUpdated(DateUtils.parseDate("2024-12-03")); + DataApproval da1b = createDataApproval(cocSource1, level1, daw1, p2, ou1); + da1b.setLastUpdated(DateUtils.parseDate("2024-12-01")); + DataApproval da2a = createDataApproval(cocSource2, level1, daw1, p1, ou1); + da2a.setLastUpdated(DateUtils.parseDate("2024-11-01")); + DataApproval da2b = createDataApproval(cocSource2, level1, daw1, p2, ou1); + da2b.setLastUpdated(DateUtils.parseDate("2024-12-08")); + DataApproval da3a = createDataApproval(cocTarget, level1, daw1, p1, ou1); + da3a.setLastUpdated(DateUtils.parseDate("2024-06-08")); + DataApproval da3b = createDataApproval(cocTarget, level1, daw1, p2, ou1); + da3b.setLastUpdated(DateUtils.parseDate("2024-06-14")); + DataApproval da4a = createDataApproval(cocRandom, level1, daw1, p1, ou1); + DataApproval da4b = createDataApproval(cocRandom, level1, daw1, p2, ou1); + + dataApprovalStore.addDataApproval(da1a); + dataApprovalStore.addDataApproval(da1b); + dataApprovalStore.addDataApproval(da2a); + dataApprovalStore.addDataApproval(da2b); + dataApprovalStore.addDataApproval(da3a); + dataApprovalStore.addDataApproval(da3b); + dataApprovalStore.addDataApproval(da4a); + dataApprovalStore.addDataApproval(da4b); + + // pre-merge state + List sourcesPreMerge = + dataApprovalStore.getByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List targetPreMerge = + dataApprovalStore.getByCategoryOptionCombo(List.of(UID.of(cocTarget))); + assertEquals(4, sourcesPreMerge.size(), "Expect 4 entries with source COC refs"); + assertEquals(2, targetPreMerge.size(), "Expect 2 entries with target COC refs"); + + // params + MergeParams mergeParams = getMergeParams(); + mergeParams.setDataMergeStrategy(DataMergeStrategy.LAST_UPDATED); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + List sourceItems = + dataApprovalStore.getByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List targetItems = + dataApprovalStore.getByCategoryOptionCombo(List.of(UID.of(cocTarget))); + + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertFalse(report.hasErrorMessages()); + assertEquals(0, sourceItems.size(), "Expect 0 entries with source COC refs"); + assertEquals(2, targetItems.size(), "Expect 2 entries with target COC refs"); + assertEquals( + Set.of("2024-12-03", "2024-12-08"), + targetItems.stream() + .map(da -> DateUtils.toMediumDate(da.getLastUpdated())) + .collect(Collectors.toSet()), + "target items should contain the original source Data Approvals lastUpdated dates"); + assertEquals(7, allCategoryOptionCombos.size(), "Expect 7 COCs present"); + assertTrue(allCategoryOptionCombos.contains(cocTarget), "Target COC should be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource1), "Source COC should not be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource2), "Source COC should not be present"); + } + + @Test + @DisplayName( + "DataApprovals with references to source COCs are deleted when using DISCARD strategy") + void dataApprovalMergeCocDiscardTest() throws ConflictException { + // given + DataApprovalLevel level1 = new DataApprovalLevel(); + level1.setLevel(1); + level1.setName("DAL1"); + manager.save(level1); + + DataApprovalLevel level2 = new DataApprovalLevel(); + level2.setLevel(2); + level2.setName("DAL2"); + manager.save(level2); + + DataApprovalWorkflow daw1 = new DataApprovalWorkflow(); + daw1.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + daw1.setName("DAW1"); + daw1.setCategoryCombo(cc1); + manager.save(daw1); + + DataApprovalWorkflow daw2 = new DataApprovalWorkflow(); + daw2.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + daw2.setName("DAW2"); + daw2.setCategoryCombo(cc1); + manager.save(daw2); + + DataApproval da1a = createDataApproval(cocSource1, level1, daw1, p1, ou1); + da1a.setLastUpdated(DateUtils.parseDate("2024-12-03")); + DataApproval da1b = createDataApproval(cocSource1, level1, daw1, p2, ou1); + da1b.setLastUpdated(DateUtils.parseDate("2024-12-01")); + DataApproval da2a = createDataApproval(cocSource2, level1, daw1, p1, ou1); + da2a.setLastUpdated(DateUtils.parseDate("2024-11-01")); + DataApproval da2b = createDataApproval(cocSource2, level1, daw1, p2, ou1); + da2b.setLastUpdated(DateUtils.parseDate("2024-12-08")); + DataApproval da3a = createDataApproval(cocTarget, level1, daw1, p1, ou1); + da3a.setLastUpdated(DateUtils.parseDate("2024-06-08")); + DataApproval da3b = createDataApproval(cocTarget, level1, daw1, p2, ou1); + da3b.setLastUpdated(DateUtils.parseDate("2024-06-14")); + DataApproval da4a = createDataApproval(cocRandom, level1, daw1, p1, ou1); + DataApproval da4b = createDataApproval(cocRandom, level1, daw1, p2, ou1); + + dataApprovalStore.addDataApproval(da1a); + dataApprovalStore.addDataApproval(da1b); + dataApprovalStore.addDataApproval(da2a); + dataApprovalStore.addDataApproval(da2b); + dataApprovalStore.addDataApproval(da3a); + dataApprovalStore.addDataApproval(da3b); + dataApprovalStore.addDataApproval(da4a); + dataApprovalStore.addDataApproval(da4b); + + // pre merge state + List sourceItemsBefore = + dataApprovalStore.getByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List targetItemsBefore = + dataApprovalStore.getByCategoryOptionCombo(List.of(UID.of(cocTarget))); + + assertEquals(4, sourceItemsBefore.size(), "Expect 4 entries with source COC refs"); + assertEquals(2, targetItemsBefore.size(), "Expect 2 entry with target COC ref only"); + + // params + MergeParams mergeParams = getMergeParams(); + mergeParams.setDataMergeStrategy(DataMergeStrategy.DISCARD); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + List sourceItems = + dataApprovalStore.getByCategoryOptionCombo(UID.of(cocSource1, cocSource2)); + List targetItems = + dataApprovalStore.getByCategoryOptionCombo(List.of(UID.of(cocTarget))); + + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertFalse(report.hasErrorMessages()); + assertEquals(0, sourceItems.size(), "Expect 0 entries with source COC refs"); + assertEquals(2, targetItems.size(), "Expect 2 entry with target COC ref only"); + assertEquals(7, allCategoryOptionCombos.size(), "Expect 7 COCs present"); + assertTrue(allCategoryOptionCombos.contains(cocTarget), "Target COC should be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource1), "Source COC should not be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource2), "Source COC should not be present"); + } + + // ----------------------------- + // -- Event eventDataValues -- + // ----------------------------- + @Test + @DisplayName( + "Event attributeOptionCombo references to source COCs are replaced with target COC when using LAST_UPDATED, source COCs are not deleted") + void eventMergeTest() throws ConflictException { + // given + TrackedEntity trackedEntity = createTrackedEntity(ou1); + manager.save(trackedEntity); + Enrollment enrollment = createEnrollment(program, trackedEntity, ou1); + manager.save(enrollment); + ProgramStage stage = createProgramStage('s', 2); + manager.save(stage); + + Event e1 = createEvent(stage, enrollment, ou1); + e1.setAttributeOptionCombo(cocSource1); + Event e2 = createEvent(stage, enrollment, ou1); + e2.setAttributeOptionCombo(cocSource2); + Event e3 = createEvent(stage, enrollment, ou1); + e3.setAttributeOptionCombo(cocTarget); + Event e4 = createEvent(stage, enrollment, ou1); + e4.setAttributeOptionCombo(cocRandom); + + manager.save(List.of(e1, e2, e3, e4)); + + // params + MergeParams mergeParams = getMergeParams(); + mergeParams.setDeleteSources(false); + mergeParams.setDataMergeStrategy(DataMergeStrategy.LAST_UPDATED); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + List allEvents = eventStore.getAll(); + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertFalse(report.hasErrorMessages()); + assertEquals(4, allEvents.size(), "Expect 4 entries still"); + assertTrue( + allEvents.stream() + .map(e -> e.getAttributeOptionCombo().getUid()) + .collect(Collectors.toSet()) + .containsAll(Set.of(cocTarget.getUid(), cocRandom.getUid())), + "All events should only have references to the target coc and the random coc"); + assertEquals(9, allCategoryOptionCombos.size(), "Expect 9 COCs present"); + assertTrue( + allCategoryOptionCombos.stream() + .map(BaseIdentifiableObject::getUid) + .collect(Collectors.toSet()) + .containsAll(Set.of(cocSource1.getUid(), cocSource2.getUid(), cocTarget.getUid()))); + } + + @Test + @DisplayName( + "Event eventDataValues references to source COCs are deleted using DISCARD, source COCs are deleted") + void eventMergeSourcesDeletedTest() throws ConflictException { + // given + TrackedEntity trackedEntity = createTrackedEntity(ou1); + manager.save(trackedEntity); + Enrollment enrollment = createEnrollment(program, trackedEntity, ou1); + manager.save(enrollment); + ProgramStage stage = createProgramStage('s', 2); + manager.save(stage); + + Event e1 = createEvent(stage, enrollment, ou1); + e1.setAttributeOptionCombo(cocSource1); + Event e2 = createEvent(stage, enrollment, ou1); + e2.setAttributeOptionCombo(cocSource2); + Event e3 = createEvent(stage, enrollment, ou1); + e3.setAttributeOptionCombo(cocTarget); + Event e4 = createEvent(stage, enrollment, ou1); + e4.setAttributeOptionCombo(cocRandom); + + manager.save(List.of(e1, e2, e3, e4)); + + // params + MergeParams mergeParams = getMergeParams(); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + List allEvents = eventStore.getAll(); + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertFalse(report.hasErrorMessages()); + assertEquals(2, allEvents.size(), "Expect 2 entries still"); + assertEquals(7, allCategoryOptionCombos.size(), "Expect 7 COCs present"); + assertTrue(allCategoryOptionCombos.contains(cocTarget), "target COC should be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource1), "source COC should not be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource2), "source COC should not be present"); + } + + // -------------------------------- + // --CompleteDataSetRegistration-- + // -------------------------------- + @Test + @DisplayName( + "CompleteDataSetRegistration with references to source COCs are deleted when using DISCARD strategy") + void cdsrMergeCocDiscardTest() throws ConflictException { + // given + DataSet ds1 = createDataSet('1'); + ds1.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + manager.save(ds1); + + CompleteDataSetRegistration cdsr1 = createCdsr(ds1, ou1, p1, cocSource1); + CompleteDataSetRegistration cdsr2 = createCdsr(ds1, ou1, p1, cocSource2); + CompleteDataSetRegistration cdsr3 = createCdsr(ds1, ou1, p1, cocTarget); + CompleteDataSetRegistration cdsr4 = createCdsr(ds1, ou1, p1, cocRandom); + completeDataSetRegistrationStore.saveCompleteDataSetRegistration(cdsr1); + completeDataSetRegistrationStore.saveCompleteDataSetRegistration(cdsr2); + completeDataSetRegistrationStore.saveCompleteDataSetRegistration(cdsr3); + completeDataSetRegistrationStore.saveCompleteDataSetRegistration(cdsr4); + + // params + MergeParams mergeParams = getMergeParams(); + mergeParams.setDataMergeStrategy(DataMergeStrategy.DISCARD); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + List sourceItems = + completeDataSetRegistrationStore.getAllByCategoryOptionCombo( + UID.of(cocSource1, cocSource2)); + List targetItems = + completeDataSetRegistrationStore.getAllByCategoryOptionCombo(List.of(UID.of(cocTarget))); + + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertFalse(report.hasErrorMessages()); + assertEquals(0, sourceItems.size(), "Expect 0 entries with source COC refs"); + assertEquals(1, targetItems.size(), "Expect 1 entry with target COC ref only"); + assertEquals(7, allCategoryOptionCombos.size(), "Expect 7 COCs present"); + assertTrue(allCategoryOptionCombos.contains(cocTarget), "Target COC should be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource1), "Source COC should not be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource2), "Source COC should not be present"); + } + + @Test + @DisplayName( + "CompleteDataSetRegistration with references to source COCs are merged when using LAST_UPDATED strategy, no duplicates") + void cdsrMergeNoDuplicatesTest() throws ConflictException { + // given + DataSet ds1 = createDataSet('1'); + ds1.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + manager.save(ds1); + + DataSet ds2 = createDataSet('2'); + ds2.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + manager.save(ds2); + + CompleteDataSetRegistration cdsr1 = createCdsr(ds1, ou1, p1, cocSource1); + CompleteDataSetRegistration cdsr2 = createCdsr(ds2, ou1, p3, cocSource2); + CompleteDataSetRegistration cdsr3 = createCdsr(ds1, ou3, p2, cocTarget); + CompleteDataSetRegistration cdsr4 = createCdsr(ds2, ou2, p1, cocRandom); + completeDataSetRegistrationStore.saveWithoutUpdatingLastUpdated(cdsr1); + completeDataSetRegistrationStore.saveWithoutUpdatingLastUpdated(cdsr2); + completeDataSetRegistrationStore.saveWithoutUpdatingLastUpdated(cdsr3); + completeDataSetRegistrationStore.saveWithoutUpdatingLastUpdated(cdsr4); + + // params + MergeParams mergeParams = getMergeParams(); + mergeParams.setDataMergeStrategy(DataMergeStrategy.LAST_UPDATED); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + List sourceItems = + completeDataSetRegistrationStore.getAllByCategoryOptionCombo( + UID.of(cocSource1, cocSource2)); + List targetItems = + completeDataSetRegistrationStore.getAllByCategoryOptionCombo(List.of(UID.of(cocTarget))); + + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertFalse(report.hasErrorMessages()); + assertEquals(0, sourceItems.size(), "Expect 0 entries with source COC refs"); + assertEquals(3, targetItems.size(), "Expect 3 entries with target COC ref only"); + assertEquals(7, allCategoryOptionCombos.size(), "Expect 7 COCs present"); + assertTrue(allCategoryOptionCombos.contains(cocTarget), "Target COC should be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource1), "Source COC should not be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource2), "Source COC should not be present"); + } + + @Test + @DisplayName( + "Merge CompleteDataSetRegistration with references to source COCs, using LAST_UPDATED strategy, with duplicates, target has latest lastUpdated") + void cdsrMergeDuplicatesTargetLastUpdatedTest() throws ConflictException { + // given + DataSet ds1 = createDataSet('1'); + ds1.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + manager.save(ds1); + + DataSet ds2 = createDataSet('2'); + ds2.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + manager.save(ds2); + + CompleteDataSetRegistration cdsr1 = createCdsr(ds1, ou1, p1, cocSource1); + cdsr1.setLastUpdated(DateUtils.parseDate("2024-11-01")); + CompleteDataSetRegistration cdsr2 = createCdsr(ds1, ou1, p1, cocSource2); + cdsr2.setLastUpdated(DateUtils.parseDate("2024-10-01")); + CompleteDataSetRegistration cdsr3 = createCdsr(ds1, ou1, p1, cocTarget); + cdsr3.setLastUpdated(DateUtils.parseDate("2024-12-05")); + CompleteDataSetRegistration cdsr4 = createCdsr(ds2, ou2, p1, cocRandom); + completeDataSetRegistrationStore.saveWithoutUpdatingLastUpdated(cdsr1); + completeDataSetRegistrationStore.saveWithoutUpdatingLastUpdated(cdsr2); + completeDataSetRegistrationStore.saveWithoutUpdatingLastUpdated(cdsr3); + completeDataSetRegistrationStore.saveWithoutUpdatingLastUpdated(cdsr4); + + // params + MergeParams mergeParams = getMergeParams(); + mergeParams.setDataMergeStrategy(DataMergeStrategy.LAST_UPDATED); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + List sourceItems = + completeDataSetRegistrationStore.getAllByCategoryOptionCombo( + UID.of(cocSource1, cocSource2)); + List targetItems = + completeDataSetRegistrationStore.getAllByCategoryOptionCombo(List.of(UID.of(cocTarget))); + + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertFalse(report.hasErrorMessages()); + assertEquals(0, sourceItems.size(), "Expect 0 entries with source COC refs"); + assertEquals(1, targetItems.size(), "Expect 1 entries with target COC ref only"); + assertEquals(7, allCategoryOptionCombos.size(), "Expect 7 COCs present"); + assertEquals( + Set.of("2024-12-05"), + targetItems.stream() + .map(da -> DateUtils.toMediumDate(da.getLastUpdated())) + .collect(Collectors.toSet()), + "target items should contain target Data Approvals lastUpdated dates"); + assertTrue(allCategoryOptionCombos.contains(cocTarget), "Target COC should be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource1), "Source COC should not be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource2), "Source COC should not be present"); + } + + @Test + @DisplayName( + "Merge CompleteDataSetRegistration with references to source COCs, using LAST_UPDATED strategy, with duplicates, sources have latest lastUpdated") + void cdsrMergeDuplicatesSourcesLastUpdatedTest() throws ConflictException { + // given + DataSet ds1 = createDataSet('1'); + ds1.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + manager.save(ds1); + + DataSet ds2 = createDataSet('2'); + ds2.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + manager.save(ds2); + + CompleteDataSetRegistration cdsr1 = createCdsr(ds1, ou1, p1, cocSource1); + cdsr1.setLastUpdated(DateUtils.parseDate("2024-10-01")); + CompleteDataSetRegistration cdsr2 = createCdsr(ds1, ou1, p1, cocSource2); + cdsr2.setLastUpdated(DateUtils.parseDate("2024-11-01")); + CompleteDataSetRegistration cdsr3 = createCdsr(ds1, ou1, p1, cocTarget); + cdsr3.setLastUpdated(DateUtils.parseDate("2024-05-05")); + CompleteDataSetRegistration cdsr4 = createCdsr(ds2, ou2, p1, cocRandom); + completeDataSetRegistrationStore.saveWithoutUpdatingLastUpdated(cdsr1); + completeDataSetRegistrationStore.saveWithoutUpdatingLastUpdated(cdsr2); + completeDataSetRegistrationStore.saveWithoutUpdatingLastUpdated(cdsr3); + completeDataSetRegistrationStore.saveWithoutUpdatingLastUpdated(cdsr4); + + // params + MergeParams mergeParams = getMergeParams(); + mergeParams.setDataMergeStrategy(DataMergeStrategy.LAST_UPDATED); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + List sourceItems = + completeDataSetRegistrationStore.getAllByCategoryOptionCombo( + UID.of(cocSource1, cocSource2)); + List targetItems = + completeDataSetRegistrationStore.getAllByCategoryOptionCombo(List.of(UID.of(cocTarget))); + + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertFalse(report.hasErrorMessages()); + assertEquals(0, sourceItems.size(), "Expect 0 entries with source COC refs"); + assertEquals(1, targetItems.size(), "Expect 1 entries with target COC ref only"); + assertEquals(7, allCategoryOptionCombos.size(), "Expect 7 COCs present"); + assertEquals( + Set.of("2024-11-01"), + targetItems.stream() + .map(da -> DateUtils.toMediumDate(da.getLastUpdated())) + .collect(Collectors.toSet()), + "target items should contain source registration lastUpdated dates"); + assertTrue(allCategoryOptionCombos.contains(cocTarget), "Target COC should be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource1), "Source COC should not be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource2), "Source COC should not be present"); + } + + @Test + @DisplayName( + "Merge CompleteDataSetRegistration with references to source COCs, using LAST_UPDATED strategy, with duplicates & non-duplicates") + void cdsrMergeDuplicatesNonDuplicatesTest() throws ConflictException { + // given + DataSet ds1 = createDataSet('1'); + ds1.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + manager.save(ds1); + + DataSet ds2 = createDataSet('2'); + ds2.setPeriodType(PeriodType.getPeriodType(PeriodTypeEnum.MONTHLY)); + manager.save(ds2); + + CompleteDataSetRegistration cdsr1 = createCdsr(ds1, ou1, p1, cocSource1); + cdsr1.setLastUpdated(DateUtils.parseDate("2024-10-01")); + CompleteDataSetRegistration cdsr2 = createCdsr(ds2, ou2, p1, cocSource2); + cdsr2.setLastUpdated(DateUtils.parseDate("2024-11-11")); + CompleteDataSetRegistration cdsr3 = createCdsr(ds1, ou1, p1, cocTarget); + cdsr3.setLastUpdated(DateUtils.parseDate("2024-12-05")); + CompleteDataSetRegistration cdsr4 = createCdsr(ds2, ou2, p1, cocRandom); + completeDataSetRegistrationStore.saveWithoutUpdatingLastUpdated(cdsr1); + completeDataSetRegistrationStore.saveWithoutUpdatingLastUpdated(cdsr2); + completeDataSetRegistrationStore.saveWithoutUpdatingLastUpdated(cdsr3); + completeDataSetRegistrationStore.saveWithoutUpdatingLastUpdated(cdsr4); + + // params + MergeParams mergeParams = getMergeParams(); + mergeParams.setDataMergeStrategy(DataMergeStrategy.LAST_UPDATED); + + // when + MergeReport report = categoryOptionComboMergeService.processMerge(mergeParams); + + // then + List sourceItems = + completeDataSetRegistrationStore.getAllByCategoryOptionCombo( + UID.of(cocSource1, cocSource2)); + List targetItems = + completeDataSetRegistrationStore.getAllByCategoryOptionCombo(List.of(UID.of(cocTarget))); + + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + assertFalse(report.hasErrorMessages()); + assertEquals(0, sourceItems.size(), "Expect 0 entries with source COC refs"); + assertEquals(2, targetItems.size(), "Expect 2 entries with target COC ref only"); + assertEquals(7, allCategoryOptionCombos.size(), "Expect 7 COCs present"); + assertEquals( + Set.of("2024-12-05", "2024-11-11"), + targetItems.stream() + .map(da -> DateUtils.toMediumDate(da.getLastUpdated())) + .collect(Collectors.toSet()), + "target items should contain target & source registration lastUpdated dates"); + assertTrue(allCategoryOptionCombos.contains(cocTarget), "Target COC should be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource1), "Source COC should not be present"); + assertFalse(allCategoryOptionCombos.contains(cocSource2), "Source COC should not be present"); + } + + private CompleteDataSetRegistration createCdsr( + DataSet ds, OrganisationUnit ou, Period p, CategoryOptionCombo coc) { + CompleteDataSetRegistration cdsr = new CompleteDataSetRegistration(); + cdsr.setSource(ou); + cdsr.setAttributeOptionCombo(coc); + cdsr.setPeriod(p); + cdsr.setDataSet(ds); + cdsr.setCompleted(true); + return cdsr; + } + + private MergeParams getMergeParams() { + MergeParams mergeParams = new MergeParams(); + mergeParams.setSources(UID.of(List.of(cocSource1.getUid(), cocSource2.getUid()))); + mergeParams.setTarget(UID.of(cocTarget.getUid())); + mergeParams.setDataMergeStrategy(DataMergeStrategy.DISCARD); + mergeParams.setDeleteSources(true); + return mergeParams; + } + + private CategoryOptionCombo getCocWithOptions(String co1, String co2) { + List allCategoryOptionCombos = + categoryService.getAllCategoryOptionCombos(); + + return allCategoryOptionCombos.stream() + .filter( + coc -> { + List categoryOptions = + coc.getCategoryOptions().stream().map(BaseIdentifiableObject::getName).toList(); + return categoryOptions.containsAll(List.of(co1, co2)); + }) + .toList() + .get(0); + } + + private DataValueAudit createDataValueAudit(CategoryOptionCombo coc, String value, Period p) { + DataValueAudit dva = new DataValueAudit(); + dva.setDataElement(de1); + dva.setValue(value); + dva.setAuditType(AuditOperationType.CREATE); + dva.setCreated(new Date()); + dva.setCategoryOptionCombo(coc); + dva.setAttributeOptionCombo(coc); + dva.setPeriod(p); + dva.setOrganisationUnit(ou1); + return dva; + } + + private DataApprovalAudit createDataApprovalAudit( + CategoryOptionCombo coc, DataApprovalLevel level, DataApprovalWorkflow workflow, Period p) { + DataApprovalAudit daa = new DataApprovalAudit(); + daa.setAttributeOptionCombo(coc); + daa.setOrganisationUnit(ou1); + daa.setLevel(level); + daa.setWorkflow(workflow); + daa.setPeriod(p); + daa.setAction(APPROVE); + daa.setCreated(new Date()); + daa.setCreator(getCurrentUser()); + return daa; + } + + private DataApproval createDataApproval( + CategoryOptionCombo coc, + DataApprovalLevel level, + DataApprovalWorkflow workflow, + Period p, + OrganisationUnit org) { + DataApproval da = new DataApproval(level, workflow, p, org, coc); + da.setCreated(new Date()); + da.setCreator(getCurrentUser()); + return da; + } + + private DataValueAuditQueryParams getQueryParams(CategoryOptionCombo coc) { + return new DataValueAuditQueryParams().setCategoryOptionCombo(coc); + } +} diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/minmax/MinMaxDataElementStoreTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/minmax/MinMaxDataElementStoreTest.java index 826ae81ddc24..3e8f10389e19 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/minmax/MinMaxDataElementStoreTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/minmax/MinMaxDataElementStoreTest.java @@ -37,7 +37,9 @@ import java.util.ArrayList; import java.util.List; import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.category.CategoryOptionComboStore; import org.hisp.dhis.category.CategoryService; +import org.hisp.dhis.common.UID; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.dataelement.DataElementService; import org.hisp.dhis.organisationunit.OrganisationUnit; @@ -61,6 +63,7 @@ class MinMaxDataElementStoreTest extends PostgresIntegrationTestBase { @Autowired private OrganisationUnitService organisationUnitService; @Autowired private CategoryService categoryService; + @Autowired private CategoryOptionComboStore categoryOptionComboStore; @Autowired private MinMaxDataElementStore minMaxDataElementStore; @@ -207,6 +210,57 @@ void getMinMaxDataElementsByDataElement() { .containsAll(List.of(deW.getUid(), deX.getUid()))); } + @Test + @DisplayName("retrieving min max data elements by cat option combo returns expected entries") + void getMinMaxDataElementsByCoc() { + // given + DataElement deW = createDataElementAndSave('W'); + DataElement deX = createDataElementAndSave('X'); + DataElement deY = createDataElementAndSave('Y'); + DataElement deZ = createDataElementAndSave('Z'); + + MinMaxDataElement mmde1 = createMinMaxDataElementAndSave(deW); + CategoryOptionCombo coc1 = createCategoryOptionCombo('A'); + coc1.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryOptionComboStore.save(coc1); + mmde1.setOptionCombo(coc1); + + MinMaxDataElement mmde2 = createMinMaxDataElementAndSave(deX); + CategoryOptionCombo coc2 = createCategoryOptionCombo('B'); + coc2.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryOptionComboStore.save(coc2); + mmde2.setOptionCombo(coc2); + + MinMaxDataElement mmde3 = createMinMaxDataElementAndSave(deY); + CategoryOptionCombo coc3 = createCategoryOptionCombo('C'); + coc3.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryOptionComboStore.save(coc3); + mmde3.setOptionCombo(coc3); + + MinMaxDataElement mmde4 = createMinMaxDataElementAndSave(deZ); + CategoryOptionCombo coc4 = createCategoryOptionCombo('D'); + coc4.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryOptionComboStore.save(coc4); + mmde4.setOptionCombo(coc4); + + // when + List allByCoc = + minMaxDataElementStore.getByCategoryOptionCombo(UID.of(coc1, coc2)); + + // then + assertEquals(2, allByCoc.size()); + assertTrue( + allByCoc.stream() + .map(mmde -> mmde.getOptionCombo().getUid()) + .toList() + .containsAll(List.of(coc1.getUid(), coc2.getUid()))); + assertTrue( + allByCoc.stream() + .map(mmde -> mmde.getDataElement().getUid()) + .toList() + .containsAll(List.of(deW.getUid(), deX.getUid()))); + } + private DataElement createDataElementAndSave(char c) { DataElement de = createDataElement(c); dataElementService.addDataElement(de); diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/organisationunit/OrganisationUnitServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/organisationunit/OrganisationUnitServiceTest.java index 435a06608f51..341c32c8a7c9 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/organisationunit/OrganisationUnitServiceTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/organisationunit/OrganisationUnitServiceTest.java @@ -241,7 +241,6 @@ void testGetOrganisationUnitWithChildren() throws Exception { OrganisationUnit unit3 = createOrganisationUnit('C', unit2); OrganisationUnit unit4 = createOrganisationUnit('D'); long id1 = organisationUnitService.addOrganisationUnit(unit1); - unit1.getChildren().add(unit2); organisationUnitService.addOrganisationUnit(unit2); organisationUnitService.addOrganisationUnit(unit3); organisationUnitService.addOrganisationUnit(unit4); @@ -308,12 +307,6 @@ void testGetOrganisationUnitWithChildrenMaxLevel() { OrganisationUnit unit5 = createOrganisationUnit('E', unit2); OrganisationUnit unit6 = createOrganisationUnit('F', unit3); OrganisationUnit unit7 = createOrganisationUnit('G', unit3); - unit1.getChildren().add(unit2); - unit1.getChildren().add(unit3); - unit2.getChildren().add(unit4); - unit2.getChildren().add(unit5); - unit3.getChildren().add(unit6); - unit3.getChildren().add(unit7); long id1 = organisationUnitService.addOrganisationUnit(unit1); long id2 = organisationUnitService.addOrganisationUnit(unit2); organisationUnitService.addOrganisationUnit(unit3); @@ -415,23 +408,18 @@ void testGetOrganisationUnitsAtLevel() { organisationUnitService.addOrganisationUnitLevel(levelC); organisationUnitService.addOrganisationUnitLevel(levelD); OrganisationUnit unit1 = createOrganisationUnit('1'); - organisationUnitService.addOrganisationUnit(unit1); OrganisationUnit unit2 = createOrganisationUnit('2', unit1); - unit1.getChildren().add(unit2); - organisationUnitService.addOrganisationUnit(unit2); OrganisationUnit unit3 = createOrganisationUnit('3', unit2); - unit2.getChildren().add(unit3); - organisationUnitService.addOrganisationUnit(unit3); OrganisationUnit unit4 = createOrganisationUnit('4', unit2); - unit2.getChildren().add(unit4); - organisationUnitService.addOrganisationUnit(unit4); OrganisationUnit unit5 = createOrganisationUnit('5', unit2); - unit2.getChildren().add(unit5); - organisationUnitService.addOrganisationUnit(unit5); OrganisationUnit unit6 = createOrganisationUnit('6', unit3); - unit3.getChildren().add(unit6); - organisationUnitService.addOrganisationUnit(unit6); OrganisationUnit unit7 = createOrganisationUnit('7'); + organisationUnitService.addOrganisationUnit(unit1); + organisationUnitService.addOrganisationUnit(unit2); + organisationUnitService.addOrganisationUnit(unit3); + organisationUnitService.addOrganisationUnit(unit4); + organisationUnitService.addOrganisationUnit(unit5); + organisationUnitService.addOrganisationUnit(unit6); organisationUnitService.addOrganisationUnit(unit7); // unit1 // unit1 . unit2 @@ -508,16 +496,13 @@ void testGetOrganisationUnitsAtLevel() { void testGetNumberOfOrganisationalLevels() { assertEquals(0, organisationUnitService.getNumberOfOrganisationalLevels()); OrganisationUnit unit1 = createOrganisationUnit('1'); - organisationUnitService.addOrganisationUnit(unit1); OrganisationUnit unit2 = createOrganisationUnit('2', unit1); - unit1.getChildren().add(unit2); + OrganisationUnit unit3 = createOrganisationUnit('3', unit2); + OrganisationUnit unit4 = createOrganisationUnit('4', unit2); + organisationUnitService.addOrganisationUnit(unit1); organisationUnitService.addOrganisationUnit(unit2); assertEquals(2, organisationUnitService.getNumberOfOrganisationalLevels()); - OrganisationUnit unit3 = createOrganisationUnit('3', unit2); - unit2.getChildren().add(unit3); organisationUnitService.addOrganisationUnit(unit3); - OrganisationUnit unit4 = createOrganisationUnit('4', unit2); - unit2.getChildren().add(unit4); organisationUnitService.addOrganisationUnit(unit4); assertEquals(3, organisationUnitService.getNumberOfOrganisationalLevels()); } @@ -525,14 +510,12 @@ void testGetNumberOfOrganisationalLevels() { @Test void testIsDescendantSet() { OrganisationUnit unit1 = createOrganisationUnit('1'); - organisationUnitService.addOrganisationUnit(unit1); OrganisationUnit unit2 = createOrganisationUnit('2', unit1); - unit1.getChildren().add(unit2); - organisationUnitService.addOrganisationUnit(unit2); OrganisationUnit unit3 = createOrganisationUnit('3', unit2); - unit2.getChildren().add(unit3); - organisationUnitService.addOrganisationUnit(unit3); OrganisationUnit unit4 = createOrganisationUnit('4'); + organisationUnitService.addOrganisationUnit(unit1); + organisationUnitService.addOrganisationUnit(unit2); + organisationUnitService.addOrganisationUnit(unit3); organisationUnitService.addOrganisationUnit(unit4); assertTrue(unit1.isDescendant(Sets.newHashSet(unit1))); assertTrue(unit2.isDescendant(Sets.newHashSet(unit1))); @@ -545,14 +528,12 @@ void testIsDescendantSet() { @Test void testIsDescendantOrgUnit() { OrganisationUnit ouA = createOrganisationUnit('A'); - organisationUnitService.addOrganisationUnit(ouA); OrganisationUnit ouB = createOrganisationUnit('B', ouA); - ouA.getChildren().add(ouB); - organisationUnitService.addOrganisationUnit(ouB); OrganisationUnit ouC = createOrganisationUnit('C', ouB); - ouB.getChildren().add(ouC); - organisationUnitService.addOrganisationUnit(ouC); OrganisationUnit ouD = createOrganisationUnit('D'); + organisationUnitService.addOrganisationUnit(ouA); + organisationUnitService.addOrganisationUnit(ouB); + organisationUnitService.addOrganisationUnit(ouC); organisationUnitService.addOrganisationUnit(ouD); assertTrue(ouA.isDescendant(Set.of(ouA))); assertTrue(ouB.isDescendant(Set.of(ouA))); @@ -565,14 +546,12 @@ void testIsDescendantOrgUnit() { @Test void testIsDescendantObject() { OrganisationUnit unit1 = createOrganisationUnit('1'); - organisationUnitService.addOrganisationUnit(unit1); OrganisationUnit unit2 = createOrganisationUnit('2', unit1); - unit1.getChildren().add(unit2); - organisationUnitService.addOrganisationUnit(unit2); OrganisationUnit unit3 = createOrganisationUnit('3', unit2); - unit2.getChildren().add(unit3); - organisationUnitService.addOrganisationUnit(unit3); OrganisationUnit unit4 = createOrganisationUnit('4'); + organisationUnitService.addOrganisationUnit(unit1); + organisationUnitService.addOrganisationUnit(unit2); + organisationUnitService.addOrganisationUnit(unit3); organisationUnitService.addOrganisationUnit(unit4); assertTrue(unit1.isDescendant(unit1)); assertTrue(unit2.isDescendant(unit1)); @@ -652,20 +631,6 @@ void testGetOrganisationUnitAtLevelAndBranches() { OrganisationUnit unitM = createOrganisationUnit('M', unitF); OrganisationUnit unitN = createOrganisationUnit('N', unitG); OrganisationUnit unitO = createOrganisationUnit('O', unitG); - unitA.getChildren().add(unitB); - unitA.getChildren().add(unitC); - unitB.getChildren().add(unitD); - unitB.getChildren().add(unitE); - unitC.getChildren().add(unitF); - unitC.getChildren().add(unitG); - unitD.getChildren().add(unitH); - unitD.getChildren().add(unitI); - unitE.getChildren().add(unitJ); - unitE.getChildren().add(unitK); - unitF.getChildren().add(unitL); - unitF.getChildren().add(unitM); - unitG.getChildren().add(unitN); - unitG.getChildren().add(unitO); organisationUnitService.addOrganisationUnit(unitA); organisationUnitService.addOrganisationUnit(unitB); organisationUnitService.addOrganisationUnit(unitC); @@ -884,9 +849,6 @@ void testGetOrganisationUnitLevels() { OrganisationUnit unitB = createOrganisationUnit('B', unitA); OrganisationUnit unitC = createOrganisationUnit('C', unitB); OrganisationUnit unitD = createOrganisationUnit('D', unitC); - unitA.getChildren().add(unitB); - unitB.getChildren().add(unitC); - unitC.getChildren().add(unitD); organisationUnitService.addOrganisationUnit(unitA); organisationUnitService.addOrganisationUnit(unitB); organisationUnitService.addOrganisationUnit(unitC); @@ -944,12 +906,6 @@ void testIsInUserHierarchy() { OrganisationUnit ouE = createOrganisationUnit('E', ouB); OrganisationUnit ouF = createOrganisationUnit('F', ouC); OrganisationUnit ouG = createOrganisationUnit('G', ouC); - ouA.getChildren().add(ouB); - ouA.getChildren().add(ouC); - ouB.getChildren().add(ouD); - ouB.getChildren().add(ouE); - ouC.getChildren().add(ouF); - ouC.getChildren().add(ouG); organisationUnitService.addOrganisationUnit(ouA); organisationUnitService.addOrganisationUnit(ouB); organisationUnitService.addOrganisationUnit(ouC); @@ -975,9 +931,6 @@ void testGetAncestorUids() { OrganisationUnit ouB = createOrganisationUnit('B', ouA); OrganisationUnit ouC = createOrganisationUnit('C', ouB); OrganisationUnit ouD = createOrganisationUnit('D', ouC); - ouA.getChildren().add(ouB); - ouA.getChildren().add(ouC); - ouB.getChildren().add(ouD); organisationUnitService.addOrganisationUnit(ouA); organisationUnitService.addOrganisationUnit(ouB); organisationUnitService.addOrganisationUnit(ouC); @@ -995,9 +948,6 @@ void testGetParentGraph() { OrganisationUnit ouB = createOrganisationUnit('B', ouA); OrganisationUnit ouC = createOrganisationUnit('C', ouB); OrganisationUnit ouD = createOrganisationUnit('D', ouC); - ouA.getChildren().add(ouB); - ouA.getChildren().add(ouC); - ouB.getChildren().add(ouD); organisationUnitService.addOrganisationUnit(ouA); organisationUnitService.addOrganisationUnit(ouB); organisationUnitService.addOrganisationUnit(ouC); @@ -1009,6 +959,22 @@ void testGetParentGraph() { assertEquals(expected, ouD.getParentGraph(Sets.newHashSet(ouB))); } + @Test + void testGetStoredPath() { + OrganisationUnit ouA = createOrganisationUnit('A'); + OrganisationUnit ouB = createOrganisationUnit('B', ouA); + OrganisationUnit ouC = createOrganisationUnit('C', ouB); + organisationUnitService.addOrganisationUnit(ouA); + organisationUnitService.addOrganisationUnit(ouB); + organisationUnitService.addOrganisationUnit(ouC); + String expectedA = String.format("/%s", ouA.getUid()); + String expectedB = String.format("/%s/%s", ouA.getUid(), ouB.getUid()); + String expectedC = String.format("/%s/%s/%s", ouA.getUid(), ouB.getUid(), ouC.getUid()); + assertEquals(expectedA, ouA.getStoredPath()); + assertEquals(expectedB, ouB.getStoredPath()); + assertEquals(expectedC, ouC.getStoredPath()); + } + @Test void testSaveImage() { byte[] content = "<>".getBytes(); diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/predictor/PredictorStoreTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/predictor/PredictorStoreTest.java index 1dd9dcbde805..fd57b9383411 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/predictor/PredictorStoreTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/predictor/PredictorStoreTest.java @@ -37,6 +37,7 @@ import java.util.Set; import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.category.CategoryService; +import org.hisp.dhis.common.UID; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.dataelement.DataElementService; import org.hisp.dhis.expression.Expression; @@ -476,7 +477,7 @@ void generatorWithDataElementTest() { @Test @DisplayName( - "Retrieving Predictors whose sample skit test contains DataElements returns expected results") + "Retrieving Predictors whose sample skip test contains DataElements returns expected results") void sampleSkipTestWithDataElementTest() { // given DataElement de1 = createDataElement('1'); @@ -524,4 +525,74 @@ void sampleSkipTestWithDataElementTest() { allWithSampleSkipTestDEs.containsAll(List.of(p1, p2)), "Retrieved result set should contain both Predictors"); } + + @Test + @DisplayName("Retrieving Predictors by CategoryOptionCombo returns expected results") + void getByCocTest() { + // given + CategoryOptionCombo coc1 = createCategoryOptionCombo('1'); + coc1.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(coc1); + + CategoryOptionCombo coc2 = createCategoryOptionCombo('2'); + coc2.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(coc2); + + CategoryOptionCombo coc3 = createCategoryOptionCombo('3'); + coc3.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryService.addCategoryOptionCombo(coc3); + + Predictor p1 = + createPredictor( + dataElementA, + coc1, + "A", + expressionC, + createExpression2('a', "#{test123}"), + periodType, + orgUnitLevel1, + 1, + 1, + 1); + + Predictor p2 = + createPredictor( + dataElementX, + coc2, + "B", + expressionD, + createExpression2('a', "#{test123}"), + periodType, + orgUnitLevel1, + 1, + 1, + 1); + + Predictor p3 = + createPredictor( + dataElementB, + coc3, + "C", + expressionB, + createExpression2('a', "#{test123}"), + periodType, + orgUnitLevel1, + 1, + 1, + 1); + + predictorStore.save(p1); + predictorStore.save(p2); + predictorStore.save(p3); + + // when + List allByCoc = + predictorStore.getByCategoryOptionCombo(UID.of(coc1.getUid(), coc2.getUid())); + + // then + assertEquals(2, allByCoc.size()); + assertTrue( + allByCoc.containsAll(List.of(p1, p2)), + "Retrieved result set should contain both Predictors"); + } } diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/resourcetable/ResourceTableServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/resourcetable/ResourceTableServiceTest.java index f8becff09629..a45664bb2baf 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/resourcetable/ResourceTableServiceTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/resourcetable/ResourceTableServiceTest.java @@ -86,12 +86,8 @@ void setUp() { idObjectManager.save(degsA); idObjectManager.save(degsB); OrganisationUnit ouA = createOrganisationUnit('A'); - OrganisationUnit ouB = createOrganisationUnit('B'); - OrganisationUnit ouC = createOrganisationUnit('C'); - ouB.setParent(ouA); - ouC.setParent(ouA); - ouA.getChildren().add(ouB); - ouA.getChildren().add(ouC); + OrganisationUnit ouB = createOrganisationUnit('B', ouA); + OrganisationUnit ouC = createOrganisationUnit('C', ouA); idObjectManager.save(ouA); idObjectManager.save(ouB); idObjectManager.save(ouC); diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/sms/SMSCommandStoreTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/sms/SMSCommandStoreTest.java index ed7efd3ac9a1..4291222da416 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/sms/SMSCommandStoreTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/sms/SMSCommandStoreTest.java @@ -32,6 +32,9 @@ import java.util.List; import java.util.Set; +import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.category.CategoryOptionComboStore; +import org.hisp.dhis.common.UID; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.dataelement.DataElementService; import org.hisp.dhis.sms.command.SMSCommand; @@ -54,6 +57,7 @@ class SMSCommandStoreTest extends PostgresIntegrationTestBase { @Autowired private DataElementService dataElementService; @Autowired private SMSCommandStore smsCommandStore; + @Autowired private CategoryOptionComboStore categoryOptionComboStore; @Test @DisplayName("retrieving SMS Codes by data element returns expected entries") @@ -81,13 +85,59 @@ void getSMSCodesByDataElementTest() { .containsAll(List.of(deW.getUid(), deX.getUid(), deY.getUid()))); } + @Test + @DisplayName("retrieving SMS Codes by cat option combos returns expected entries") + void getByCocTest() { + // given + DataElement deW = createDataElementAndSave('W'); + DataElement deX = createDataElementAndSave('X'); + DataElement deY = createDataElementAndSave('Y'); + DataElement deZ = createDataElementAndSave('Z'); + + SMSCode code1 = createSMSCodeAndSave(deW, "code 1"); + CategoryOptionCombo coc1 = createCategoryOptionCombo('A'); + coc1.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryOptionComboStore.save(coc1); + code1.setOptionId(coc1); + + SMSCode code2 = createSMSCodeAndSave(deX, "code 2"); + CategoryOptionCombo coc2 = createCategoryOptionCombo('B'); + coc2.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryOptionComboStore.save(coc2); + code2.setOptionId(coc2); + + SMSCode code3 = createSMSCodeAndSave(deY, "code 3"); + CategoryOptionCombo coc3 = createCategoryOptionCombo('C'); + coc3.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryOptionComboStore.save(coc3); + code3.setOptionId(coc3); + + SMSCode code4 = createSMSCodeAndSave(deZ, "code 4"); + CategoryOptionCombo coc4 = createCategoryOptionCombo('D'); + coc4.setCategoryCombo(categoryService.getDefaultCategoryCombo()); + categoryOptionComboStore.save(coc4); + code4.setOptionId(coc4); + + // when + List allByCoc = smsCommandStore.getCodesByCategoryOptionCombo(UID.of(coc1, coc2)); + + // then + assertEquals(2, allByCoc.size()); + assertTrue( + allByCoc.stream() + .map(code -> code.getOptionId().getUid()) + .toList() + .containsAll(List.of(coc1.getUid(), coc2.getUid())), + "Codes should contain correct COC UIDs"); + } + private DataElement createDataElementAndSave(char c) { DataElement de = createDataElement(c); dataElementService.addDataElement(de); return de; } - private void createSMSCodeAndSave(DataElement de, String code) { + private SMSCode createSMSCodeAndSave(DataElement de, String code) { SMSCode smsCode = new SMSCode(); smsCode.setCode("Code " + code); smsCode.setDataElement(de); @@ -96,5 +146,6 @@ private void createSMSCodeAndSave(DataElement de, String code) { smsCommand.setCodes(Set.of(smsCode)); smsCommand.setName("CMD " + code); smsCommandStore.save(smsCommand); + return smsCode; } } diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/trackedentity/TrackedEntityServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/trackedentity/TrackedEntityServiceTest.java index d797b3ccf943..160bb6c6281c 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/trackedentity/TrackedEntityServiceTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/trackedentity/TrackedEntityServiceTest.java @@ -143,11 +143,11 @@ class TrackedEntityServiceTest extends PostgresIntegrationTestBase { private TrackedEntityAttribute teaC; - private TrackedEntityAttributeValue trackedEntityAttributeValueA; + private TrackedEntityAttributeValue tetavA; - private TrackedEntityAttributeValue trackedEntityAttributeValueB; + private TrackedEntityAttributeValue tetavB; - private TrackedEntityAttributeValue trackedEntityAttributeValueC; + private TrackedEntityAttributeValue pteavC; private TrackedEntityType trackedEntityTypeA; @@ -324,17 +324,13 @@ void setUp() { new ProgramTrackedEntityAttribute(programB, teaE))); manager.update(programB); - trackedEntityAttributeValueA = new TrackedEntityAttributeValue(teaA, trackedEntityA, "A"); - trackedEntityAttributeValueB = new TrackedEntityAttributeValue(teaB, trackedEntityA, "B"); - trackedEntityAttributeValueC = new TrackedEntityAttributeValue(teaC, trackedEntityA, "C"); + tetavA = new TrackedEntityAttributeValue(teaA, trackedEntityA, "A"); + tetavB = new TrackedEntityAttributeValue(teaB, trackedEntityA, "B"); + pteavC = new TrackedEntityAttributeValue(teaC, trackedEntityA, "C"); trackedEntityA = createTrackedEntity(orgUnitA); trackedEntityA.setTrackedEntityType(trackedEntityTypeA); - trackedEntityA.setTrackedEntityAttributeValues( - Set.of( - trackedEntityAttributeValueA, - trackedEntityAttributeValueB, - trackedEntityAttributeValueC)); + trackedEntityA.setTrackedEntityAttributeValues(Set.of(tetavA, tetavB, pteavC)); manager.save(trackedEntityA, false); trackedEntityChildA = createTrackedEntity(orgUnitChildA); @@ -2015,23 +2011,22 @@ void shouldReturnProgramAttributesWhenSingleTERequestedAndProgramSpecified() UID.of(trackedEntityA), UID.of(programA), TrackedEntityParams.TRUE); assertContainsOnly( - Set.of( - trackedEntityAttributeValueA, - trackedEntityAttributeValueB, - trackedEntityAttributeValueC), - trackedEntity.getTrackedEntityAttributeValues()); + Set.of(tetavA, tetavB, pteavC), + trackedEntity.getTrackedEntityAttributeValues(), + TrackedEntityAttributeValue::getValue); } @Test - void shouldReturnTrackedEntityTypeAttributesWhenSingleTERequestedAndNoProgramSpecified() + void shouldReturnTrackedEntityTypeAttributesOnlyWhenSingleTERequestedAndNoProgramSpecified() throws ForbiddenException, NotFoundException, BadRequestException { TrackedEntity trackedEntity = trackedEntityService.getTrackedEntity( UID.of(trackedEntityA), null, TrackedEntityParams.TRUE); assertContainsOnly( - Set.of(trackedEntityAttributeValueA, trackedEntityAttributeValueB), - trackedEntity.getTrackedEntityAttributeValues()); + Set.of(tetavA, tetavB), + trackedEntity.getTrackedEntityAttributeValues(), + TrackedEntityAttributeValue::getValue); } @Test diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AbstractCrudControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AbstractCrudControllerTest.java index 97a596c5e3ed..7b94aae3a774 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AbstractCrudControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AbstractCrudControllerTest.java @@ -42,6 +42,7 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.List; import java.util.Map; import java.util.Set; import org.hisp.dhis.attribute.Attribute; @@ -1246,6 +1247,53 @@ void testFilterSharingLt() { assertEquals(1, programs.get("programs").as(JsonArray.class).size()); } + @Test + void testPostObject_MandatoryAttributeNoValue() { + String attr = + "{'name':'USER', 'valueType':'TRUE_ONLY', 'userAttribute':true, 'mandatory':true}"; + String attrId = assertStatus(HttpStatus.CREATED, POST("/attributes", attr)); + // language=JSON5 + String user = + """ + { + "username": "testMandatoryAttribute", + "password": "-hu@_ka9$P", + "firstName": "testMandatoryAttribute", + "surname": "tester", + "userRoles":[{ "id": "yrB6vc5Ip3r" }], + "attributeValues": [{ "attribute": { "id": "%s" }, "value": "" } ] + } + """; + assertErrorMandatoryAttributeRequired(attrId, POST("/users", user.formatted(attrId))); + } + + @Test + void testPostObject_MandatoryAttributeNoAttribute() { + String attr = + "{'name':'USER', 'valueType':'TRUE_ONLY', 'userAttribute':true, 'mandatory':true}"; + String attrId = assertStatus(HttpStatus.CREATED, POST("/attributes", attr)); + String user = + """ + { + "username": "testMandatoryAttribute", + "password": "-hu@_ka9$P", + "firstName": "testMandatoryAttribute", + "surname": "tester", + "userRoles":[{ "id": "yrB6vc5Ip3r" }] + } + """; + assertErrorMandatoryAttributeRequired(attrId, POST("/users", user)); + } + + private void assertErrorMandatoryAttributeRequired(String attrId, HttpResponse response) { + JsonError msg = response.content(HttpStatus.CONFLICT).as(JsonError.class); + JsonList errorReports = msg.getTypeReport().getErrorReports(); + assertEquals(1, errorReports.size()); + JsonErrorReport error = errorReports.get(0); + assertEquals(ErrorCode.E4011, error.getErrorCode()); + assertEquals(List.of(attrId), error.getErrorProperties()); + } + private void assertUserGroupHasOnlyUser(String groupId, String userId) { manager.flush(); manager.clear(); diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyControllerTest.java index 0621b0724844..9d0087dc59bf 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyControllerTest.java @@ -38,7 +38,10 @@ import org.hisp.dhis.common.CodeGenerator; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.dataelement.DataElementService; +import org.hisp.dhis.jsontree.JsonList; import org.hisp.dhis.test.webapi.H2ControllerIntegrationTestBase; +import org.hisp.dhis.test.webapi.json.domain.JsonUser; +import org.hisp.dhis.user.User; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; @@ -54,6 +57,25 @@ class AbstractFullReadOnlyControllerTest extends H2ControllerIntegrationTestBase @Autowired private DataElementService dataElementService; + @Test + void testGetObjectList_QueryUsers() { + // this just simulates the normal setup with a system super-user + User user = switchToNewUser("system", "ALL"); + // make sure "system" does not occur in any other property that might be searched by query= + user.setName("x"); + user.setFirstName("y"); + user.setSurname("z"); + user.setCode("xyz"); + userService.updateUser(user); + + JsonList users = + GET("/users?fields=id,name,username&query=system") + .content() + .getList("users", JsonUser.class); + assertEquals(1, users.size()); + assertEquals("system", users.get(0).getUsername()); + } + @Test void testGetObjectListCsv() { createDataElements(36); diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/CategoryOptionComboControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/CategoryOptionComboControllerTest.java index dcd627964017..11fa36cd1762 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/CategoryOptionComboControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/CategoryOptionComboControllerTest.java @@ -28,6 +28,7 @@ package org.hisp.dhis.webapi.controller; import static org.hisp.dhis.http.HttpAssertions.assertStatus; +import static org.hisp.dhis.test.webapi.Assertions.assertWebMessage; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -43,11 +44,13 @@ import org.hisp.dhis.http.HttpStatus; import org.hisp.dhis.jsontree.JsonArray; import org.hisp.dhis.jsontree.JsonList; +import org.hisp.dhis.jsontree.JsonMixed; import org.hisp.dhis.jsontree.JsonObject; import org.hisp.dhis.test.webapi.H2ControllerIntegrationTestBase; import org.hisp.dhis.test.webapi.json.domain.JsonCategoryOptionCombo; import org.hisp.dhis.test.webapi.json.domain.JsonErrorReport; import org.hisp.dhis.test.webapi.json.domain.JsonIdentifiableObject; +import org.hisp.dhis.test.webapi.json.domain.JsonWebMessage; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -74,11 +77,11 @@ void setUp() { categoryService.addCategoryOption(catOptC); CategoryOptionCombo cocA = - createCategoryOptionCombo("CatOptCombo A", "CocUid0001", catComboA, catOptA); + createCategoryOptionCombo("CatOptCombo A", "CocUid00001", catComboA, catOptA); CategoryOptionCombo cocB = - createCategoryOptionCombo("CatOptCombo B", "CocUid0002", catComboB, catOptB); + createCategoryOptionCombo("CatOptCombo B", "CocUid00002", catComboB, catOptB); CategoryOptionCombo cocC = - createCategoryOptionCombo("CatOptCombo C", "CocUid0003", catComboC, catOptC); + createCategoryOptionCombo("CatOptCombo C", "CocUid00003", catComboC, catOptC); categoryService.addCategoryOptionCombo(cocA); categoryService.addCategoryOptionCombo(cocB); categoryService.addCategoryOptionCombo(cocC); @@ -123,6 +126,116 @@ void catOptionCombosExcludingDefaultTest() { catOptionComboNames.contains("default"), "default catOptionCombo is not in payload"); } + @Test + @DisplayName("Invalid merge with source and target missing") + void testInvalidMerge() { + JsonMixed mergeResponse = + POST( + "/categoryOptionCombos/merge", + """ + { + "sources": ["Uid00000010"], + "target": "Uid00000012", + "deleteSources": true, + "dataMergeStrategy": "DISCARD" + }""") + .content(HttpStatus.CONFLICT); + assertEquals("Conflict", mergeResponse.getString("httpStatus").string()); + assertEquals("WARNING", mergeResponse.getString("status").string()); + assertEquals( + "One or more errors occurred, please see full details in merge report.", + mergeResponse.getString("message").string()); + + JsonArray errors = + mergeResponse.getObject("response").getObject("mergeReport").getArray("mergeErrors"); + JsonObject error1 = errors.getObject(0); + JsonObject error2 = errors.getObject(1); + assertEquals( + "SOURCE CategoryOptionCombo does not exist: `Uid00000010`", + error1.getString("message").string()); + assertEquals( + "TARGET CategoryOptionCombo does not exist: `Uid00000012`", + error2.getString("message").string()); + } + + @Test + @DisplayName("invalid merge, missing required auth") + void testMergeNoAuth() { + switchToNewUser("noAuth", "NoAuth"); + JsonMixed mergeResponse = + POST( + "/categoryOptionCombos/merge", + """ + { + "sources": ["Uid00000010"], + "target": "Uid00000012", + "deleteSources": true, + "dataMergeStrategy": "DISCARD" + }""") + .content(HttpStatus.FORBIDDEN); + assertEquals("Forbidden", mergeResponse.getString("httpStatus").string()); + assertEquals("ERROR", mergeResponse.getString("status").string()); + assertEquals( + "Access is denied, requires one Authority from [F_CATEGORY_OPTION_COMBO_MERGE]", + mergeResponse.getString("message").string()); + } + + @Test + @DisplayName("invalid merge, missing dataMergeStrategy") + void mergeMissingDataMergeStrategyTest() { + JsonWebMessage validationErrorMsg = + assertWebMessage( + "Conflict", + 409, + "WARNING", + "One or more errors occurred, please see full details in merge report.", + POST( + "/categoryOptionCombos/merge", + """ + { + "sources": ["CocUid00001"], + "target": "CocUid00002", + "deleteSources": true + }""") + .content(HttpStatus.CONFLICT)); + + JsonErrorReport errorReport = + validationErrorMsg.find( + JsonErrorReport.class, error -> error.getErrorCode() == ErrorCode.E1534); + assertNotNull(errorReport); + assertEquals( + "dataMergeStrategy field must be specified. With value `DISCARD` or `LAST_UPDATED`", + errorReport.getMessage()); + } + + @Test + @DisplayName("invalid merge, UID is for type other than CategoryOptionCombo") + void mergeIncorrectTypeTest() { + JsonWebMessage validationErrorMsg = + assertWebMessage( + "Conflict", + 409, + "WARNING", + "One or more errors occurred, please see full details in merge report.", + POST( + "/categoryOptionCombos/merge", + """ + { + "sources": ["bjDvmb4bfuf"], + "target": "CocUid00002", + "deleteSources": true, + "dataMergeStrategy": "DISCARD" + }""") + .content(HttpStatus.CONFLICT)); + + JsonErrorReport errorReport = + validationErrorMsg.find( + JsonErrorReport.class, error -> error.getErrorCode() == ErrorCode.E1533); + assertNotNull(errorReport); + assertEquals( + "SOURCE CategoryOptionCombo does not exist: `bjDvmb4bfuf`", errorReport.getMessage()); + } + @Test @DisplayName("Duplicate default category option combos should not be allowed") void catOptionCombosDuplicatedDefaultTest() { @@ -138,10 +251,10 @@ void catOptionCombosDuplicatedDefaultTest() { POST( "/categoryOptionCombos/", """ - { "name": "Not default", - "categoryOptions" : [{"id" : "%s"}], - "categoryCombo" : {"id" : "%s"} } - """ + { "name": "Not default", + "categoryOptions" : [{"id" : "%s"}], + "categoryCombo" : {"id" : "%s"} } + """ .formatted(defaultCatOptionComboOptions, defaultCatOptionComboCatComboId)) .content(HttpStatus.CONFLICT); diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/JobSchedulingControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/JobSchedulingControllerTest.java index 6ac3f48adf20..d6a2d2537f7a 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/JobSchedulingControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/JobSchedulingControllerTest.java @@ -35,14 +35,12 @@ import org.hisp.dhis.jsontree.JsonObject; import org.hisp.dhis.test.webapi.PostgresControllerIntegrationTestBase; import org.junit.jupiter.api.Test; -import org.springframework.transaction.annotation.Transactional; /** * Tests the {@link org.hisp.dhis.webapi.controller.scheduling.SchedulingController}. * * @author Jan Bernitt */ -@Transactional class JobSchedulingControllerTest extends PostgresControllerIntegrationTestBase { @Test @@ -65,14 +63,14 @@ void testGetCompletedProgressTypes() { @Test void testGetRunningProgress() { - JsonObject progress = GET("/scheduling/running/DATA_INTEGRITY").content(); + JsonObject progress = GET("/scheduling/running/PUSH_ANALYSIS").content(); assertTrue(progress.isObject()); assertTrue(progress.isEmpty()); } @Test void testGetCompletedProgress() { - JsonObject progress = GET("/scheduling/completed/DATA_INTEGRITY").content(); + JsonObject progress = GET("/scheduling/completed/PUSH_ANALYSIS").content(); assertTrue(progress.isObject()); assertTrue(progress.isEmpty()); } diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/SchemaControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/SchemaControllerTest.java index 0bf231cd1258..1b13c7777b05 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/SchemaControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/SchemaControllerTest.java @@ -27,14 +27,18 @@ */ package org.hisp.dhis.webapi.controller; +import static java.util.stream.Collectors.toSet; import static org.hisp.dhis.test.webapi.Assertions.assertWebMessage; +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.Optional; +import java.util.Set; import org.hisp.dhis.http.HttpStatus; +import org.hisp.dhis.jsontree.JsonList; import org.hisp.dhis.jsontree.JsonObject; import org.hisp.dhis.schema.PropertyType; import org.hisp.dhis.test.webapi.H2ControllerIntegrationTestBase; @@ -145,4 +149,35 @@ void testUserNameIsPersistedButReadOnly() { assertFalse(name.isWritable()); assertFalse(name.isRequired()); } + + @Test + void testSortableProperties() { + JsonSchema de = GET("/schemas/dataElement").content().as(JsonSchema.class); + JsonList properties = de.getProperties(); + Set expected = + Set.of( + "fieldMask", + "aggregationType", + "code", + "domainType", + "displayName", + "created", + "description", + "zeroIsSignificant", + "displayFormName", + "displayShortName", + "url", + "lastUpdated", + "valueType", + "formName", + "name", + "id", + "shortName"); + Set actual = + properties.stream() + .filter(JsonProperty::isSortable) + .map(JsonProperty::getName) + .collect(toSet()); + assertEquals(expected, actual); + } } diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUserRolesNoAuthorities.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUserRolesNoAuthorities.java index 4861ab9ba514..c37972fe7a66 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUserRolesNoAuthorities.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUserRolesNoAuthorities.java @@ -28,8 +28,17 @@ package org.hisp.dhis.webapi.controller.dataintegrity; import static org.hisp.dhis.http.HttpAssertions.assertStatus; +import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import org.hisp.dhis.http.HttpStatus; +import org.hisp.dhis.jsontree.JsonList; +import org.hisp.dhis.jsontree.JsonObject; +import org.hisp.dhis.jsontree.JsonString; +import org.hisp.dhis.test.webapi.json.domain.JsonUserRole; import org.junit.jupiter.api.Test; class DataIntegrityUserRolesNoAuthorities extends AbstractDataIntegrityIntegrationTest { @@ -48,10 +57,22 @@ void testUserRolesNoAuthorities() { assertStatus( HttpStatus.CREATED, POST("/userRoles", "{ 'name': 'Good role', 'authorities': ['F_DATAVALUE_ADD'] }")); - // Note that two user roles already exist due to the setup in the - // AbstractDataIntegrityIntegrationTest class - // Thus there should be 4 roles total. Only the Empty role should be flagged. + + JsonObject content = GET("/userRoles?fields=id,authorities").content(); + JsonList userRolesInSystem = content.getList("userRoles", JsonUserRole.class); + assertEquals(3, userRolesInSystem.size()); + + List authorityCount = + userRolesInSystem.stream() + .map(userRole -> userRole.getList("authorities", JsonString.class).size()) + .toList(); + + // Two of the roles have no authorities, one has one authority. + assertEquals(Set.of(0, 1), new HashSet<>(authorityCount)); + assertEquals(1, Collections.frequency(authorityCount, 0)); + assertEquals(2, Collections.frequency(authorityCount, 1)); + assertHasDataIntegrityIssues( - DETAILS_ID_TYPE, CHECK_NAME, 25, userRoleUid, "Empty role", null, true); + DETAILS_ID_TYPE, CHECK_NAME, 33, userRoleUid, "Empty role", null, true); } } diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUserRolesNoUsers.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUserRolesNoUsers.java index 3cf64296ddde..c4a9a6995e5e 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUserRolesNoUsers.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUserRolesNoUsers.java @@ -56,6 +56,6 @@ void testUserRolesNoUsers() { JsonDataIntegritySummary summary = getSummary(CHECK_NAME); assertEquals(1, summary.getCount()); assertHasDataIntegrityIssues( - DETAILS_ID_TYPE, CHECK_NAME, 33, userRoleUid, "Test role", null, true); + DETAILS_ID_TYPE, CHECK_NAME, 50, userRoleUid, "Test role", null, true); } } diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUsersWithAllAuthorityControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUsersWithAllAuthorityControllerTest.java new file mode 100644 index 000000000000..0c33693299a7 --- /dev/null +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUsersWithAllAuthorityControllerTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.webapi.controller.dataintegrity; + +import static org.hisp.dhis.http.HttpAssertions.assertStatus; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import org.hisp.dhis.http.HttpStatus; +import org.hisp.dhis.user.User; +import org.junit.jupiter.api.Test; + +/** + * Integrity check to identify users who have the ALL authority. {@see + * dhis-2/dhis-services/dhis-service-administration/src/main/resources/data-integrity-checks/users/users_with_all_authority.yaml} + * + * @author Jason P. Pickering + */ +class DataIntegrityUsersWithAllAuthorityControllerTest + extends AbstractDataIntegrityIntegrationTest { + + private static final String CHECK_NAME = "users_with_all_authority"; + + private static final String DETAILS_ID_TYPE = "users"; + + @Test + void testCanIdentifyUsersWithAllAuthority() { + + String orgunitAUid = + assertStatus( + HttpStatus.CREATED, + POST( + "/organisationUnits", + "{ 'name': 'Fish District', 'shortName': 'Fish District', 'openingDate' : '2022-01-01'}")); + + String userRoleUidA = + assertStatus( + HttpStatus.CREATED, + POST("/userRoles", "{ 'name': 'ALL role', 'authorities': ['ALL'] }")); + + assertStatus( + HttpStatus.CREATED, + POST( + "/users", + "{ 'username': 'bobbytables' , 'password': 'District123+', 'firstName': 'Bobby', 'surname': 'Tables', 'organisationUnits' : [{'id' : '" + + orgunitAUid + + "'}], 'dataViewOrganisationUnits' : [{'id' : '" + + orgunitAUid + + "'}], 'userRoles' : [{'id' : '" + + userRoleUidA + + "'}]}")); + + // Note that there is already one user which exists due to the overall test setup, thus, two + // users in total. + List allUsers = userService.getAllUsers(); + assertEquals(2, allUsers.size()); + Set userNames = new HashSet<>(); + for (User user : allUsers) { + userNames.add(user.getUsername()); + } + Set userUids = new HashSet<>(); + for (User user : allUsers) { + userUids.add(user.getUid()); + } + Set userComments = new HashSet<>(); + // Each user should be active, so just create a set of "Active" for each user + allUsers.forEach(user -> userComments.add("Active")); + + // Add a non-ALL authority user + + String userRoleUidB = + assertStatus( + HttpStatus.CREATED, + POST("/userRoles", "{ 'name': 'Not all role', 'authorities': ['F_DATAVALUE_ADD'] }")); + + assertStatus( + HttpStatus.CREATED, + POST( + "/users", + "{ 'username': 'bobbytables2' , 'password': 'District123+', 'firstName': 'Bobby', 'surname': 'Tables', 'organisationUnits' : [{'id' : '" + + orgunitAUid + + "'}], 'dataViewOrganisationUnits' : [{'id' : '" + + orgunitAUid + + "'}], 'userRoles' : [{'id' : '" + + userRoleUidB + + "'}]}")); + + // Note the expected percentage is 2/3 = 66% + assertHasDataIntegrityIssues( + DETAILS_ID_TYPE, CHECK_NAME, 66, userUids, userNames, userComments, true); + } +} diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUsersWithNoRoleControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUsersWithNoRoleControllerTest.java index b2ce3e5ebed1..d218e792b956 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUsersWithNoRoleControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/dataintegrity/DataIntegrityUsersWithNoRoleControllerTest.java @@ -84,7 +84,6 @@ void testCanFlagUserWithNoRoles() { @Test void testDoNotFlagUsersWithRoles() { - assertHasNoDataIntegrityIssues(DETAILS_ID_TYPE, CHECK_NAME, true); } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyController.java index 9d88e596e414..02edb0947e54 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyController.java @@ -81,6 +81,7 @@ import org.hisp.dhis.security.acl.AclService; import org.hisp.dhis.system.util.ReflectionUtils; import org.hisp.dhis.user.CurrentUser; +import org.hisp.dhis.user.User; import org.hisp.dhis.user.UserDetails; import org.hisp.dhis.user.UserSettingsService; import org.hisp.dhis.webapi.mvc.annotation.ApiVersion; @@ -242,7 +243,7 @@ protected List getPreQueryMatches(P params) throws ConflictException { @Nonnull protected List getAdditionalFilters(P params) throws ConflictException { List filters = new ArrayList<>(); - if (params.getQuery() != null && !params.getQuery().isEmpty()) + if (params.getQuery() != null && !params.getQuery().isEmpty() && getEntityClass() != User.class) filters.add(Restrictions.query(getSchema(), params.getQuery())); List matches = getPreQueryMatches(params); // Note: null = no special filters, empty = no matches for special filters diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/CompleteDataSetRegistrationController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/CompleteDataSetRegistrationController.java index 7f4e13de57d0..a099aa28926f 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/CompleteDataSetRegistrationController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/CompleteDataSetRegistrationController.java @@ -63,17 +63,14 @@ import org.hisp.dhis.dxf2.webmessage.WebMessage; import org.hisp.dhis.dxf2.webmessage.WebMessageException; import org.hisp.dhis.feedback.ConflictException; -import org.hisp.dhis.feedback.NotFoundException; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.organisationunit.OrganisationUnitService; import org.hisp.dhis.period.Period; import org.hisp.dhis.period.PeriodType; import org.hisp.dhis.scheduling.JobConfiguration; -import org.hisp.dhis.scheduling.JobConfigurationService; -import org.hisp.dhis.scheduling.JobSchedulerService; +import org.hisp.dhis.scheduling.JobExecutionService; import org.hisp.dhis.user.CurrentUserUtil; import org.hisp.dhis.user.UserDetails; -import org.hisp.dhis.user.UserService; import org.hisp.dhis.webapi.mvc.annotation.ApiVersion; import org.hisp.dhis.webapi.webdomain.CompleteDataSetRegQueryParams; import org.springframework.http.HttpStatus; @@ -112,10 +109,7 @@ public class CompleteDataSetRegistrationController { private final CompleteDataSetRegistrationExchangeService registrationExchangeService; - private final JobConfigurationService jobConfigurationService; - private final JobSchedulerService jobSchedulerService; - - private final UserService userService; + private final JobExecutionService jobExecutionService; // ------------------------------------------------------------------------- // GET @@ -149,7 +143,7 @@ public void getCompleteRegistrationsXml( @ResponseBody public WebMessage postCompleteRegistrationsXml( ImportOptions importOptions, HttpServletRequest request) - throws IOException, ConflictException, NotFoundException { + throws IOException, ConflictException { if (importOptions.isAsync()) { return asyncImport(importOptions, APPLICATION_XML, request); } @@ -164,7 +158,7 @@ public WebMessage postCompleteRegistrationsXml( @ResponseBody public WebMessage postCompleteRegistrationsJson( ImportOptions importOptions, HttpServletRequest request) - throws IOException, ConflictException, NotFoundException { + throws IOException, ConflictException { if (importOptions.isAsync()) { return asyncImport(importOptions, APPLICATION_JSON, request); } @@ -260,13 +254,13 @@ public void deleteCompleteDataSetRegistration( private WebMessage asyncImport( ImportOptions importOptions, MimeType mimeType, HttpServletRequest request) - throws IOException, ConflictException, NotFoundException { + throws IOException, ConflictException { JobConfiguration jobConfig = new JobConfiguration(COMPLETE_DATA_SET_REGISTRATION_IMPORT); jobConfig.setJobParameters(importOptions); jobConfig.setExecutedBy(CurrentUserUtil.getCurrentUserDetails().getUid()); - jobSchedulerService.createThenExecute(jobConfig, mimeType, request.getInputStream()); + jobExecutionService.executeOnceNow(jobConfig, mimeType, request.getInputStream()); return jobConfigurationReport(jobConfig); } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataAnalysisController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataAnalysisController.java index c2a674756fc2..efa85a4d88a6 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataAnalysisController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataAnalysisController.java @@ -672,7 +672,7 @@ private List validationResultsListToResponse( if (organisationUnit != null) { validationResultView.setOrganisationUnitId(organisationUnit.getUid()); validationResultView.setOrganisationUnitDisplayName(organisationUnit.getDisplayName()); - validationResultView.setOrganisationUnitPath(organisationUnit.getPath()); + validationResultView.setOrganisationUnitPath(organisationUnit.getStoredPath()); validationResultView.setOrganisationUnitAncestorNames(organisationUnit.getAncestorNames()); } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataIntegrityController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataIntegrityController.java index 606da5e7e712..e6fe860c71a0 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataIntegrityController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataIntegrityController.java @@ -47,11 +47,9 @@ import org.hisp.dhis.dataintegrity.DataIntegritySummary; import org.hisp.dhis.dxf2.webmessage.WebMessage; import org.hisp.dhis.feedback.ConflictException; -import org.hisp.dhis.feedback.NotFoundException; import org.hisp.dhis.scheduling.JobConfiguration; -import org.hisp.dhis.scheduling.JobConfigurationService; +import org.hisp.dhis.scheduling.JobExecutionService; import org.hisp.dhis.scheduling.JobParameters; -import org.hisp.dhis.scheduling.JobSchedulerService; import org.hisp.dhis.scheduling.JobType; import org.hisp.dhis.scheduling.parameters.DataIntegrityDetailsJobParameters; import org.hisp.dhis.scheduling.parameters.DataIntegrityJobParameters; @@ -82,8 +80,7 @@ public class DataIntegrityController { private final DataIntegrityService dataIntegrityService; - private final JobConfigurationService jobConfigurationService; - private final JobSchedulerService jobSchedulerService; + private final JobExecutionService jobExecutionService; @RequiresAuthority(anyOf = F_PERFORM_MAINTENANCE) @PostMapping @@ -92,7 +89,7 @@ public WebMessage runDataIntegrity( @CheckForNull @RequestParam(required = false) Set checks, @CheckForNull @RequestBody(required = false) Set checksBody, @CurrentUser UserDetails currentUser) - throws ConflictException, @OpenApi.Ignore NotFoundException { + throws ConflictException { Set names = getCheckNames(checksBody, checks); return runDataIntegrityAsync(names, currentUser, DataIntegrityReportType.SUMMARY) .setLocation("/dataIntegrity/details?checks=" + toChecksList(names)); @@ -100,7 +97,7 @@ public WebMessage runDataIntegrity( private WebMessage runDataIntegrityAsync( @Nonnull Set checks, UserDetails currentUser, DataIntegrityReportType type) - throws ConflictException, NotFoundException { + throws ConflictException { JobType jobType = type == DataIntegrityReportType.DETAILS ? JobType.DATA_INTEGRITY_DETAILS @@ -113,7 +110,7 @@ private WebMessage runDataIntegrityAsync( : new DataIntegrityJobParameters(type, checks); config.setJobParameters(parameters); - jobSchedulerService.createThenExecute(config); + jobExecutionService.executeOnceNow(config); return jobConfigurationReport(config); } @@ -162,7 +159,7 @@ public WebMessage runSummariesCheck( @CheckForNull @RequestParam(required = false) Set checks, @CheckForNull @RequestBody(required = false) Set checksBody, @CurrentUser UserDetails currentUser) - throws ConflictException, @OpenApi.Ignore NotFoundException { + throws ConflictException { Set names = getCheckNames(checksBody, checks); return runDataIntegrityAsync(names, currentUser, DataIntegrityReportType.SUMMARY) .setLocation("/dataIntegrity/summary?checks=" + toChecksList(names)); @@ -196,7 +193,7 @@ public WebMessage runDetailsCheck( @CheckForNull @RequestParam(required = false) Set checks, @RequestBody(required = false) Set checksBody, @CurrentUser UserDetails currentUser) - throws ConflictException, @OpenApi.Ignore NotFoundException { + throws ConflictException { Set names = getCheckNames(checksBody, checks); return runDataIntegrityAsync(names, currentUser, DataIntegrityReportType.DETAILS) .setLocation("/dataIntegrity/details?checks=" + toChecksList(names)); diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataValueSetController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataValueSetController.java index b4a58a2d7181..4c2281fd5acb 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataValueSetController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataValueSetController.java @@ -67,11 +67,9 @@ import org.hisp.dhis.dxf2.importsummary.ImportSummary; import org.hisp.dhis.dxf2.webmessage.WebMessage; import org.hisp.dhis.feedback.ConflictException; -import org.hisp.dhis.feedback.NotFoundException; import org.hisp.dhis.node.Provider; import org.hisp.dhis.scheduling.JobConfiguration; -import org.hisp.dhis.scheduling.JobConfigurationService; -import org.hisp.dhis.scheduling.JobSchedulerService; +import org.hisp.dhis.scheduling.JobExecutionService; import org.hisp.dhis.security.RequiresAuthority; import org.hisp.dhis.user.CurrentUserUtil; import org.hisp.dhis.user.User; @@ -104,8 +102,7 @@ public class DataValueSetController { private final DataValueSetService dataValueSetService; private final AdxDataService adxDataService; private final UserService userService; - private final JobConfigurationService jobConfigurationService; - private final JobSchedulerService jobSchedulerService; + private final JobExecutionService jobExecutionService; // ------------------------------------------------------------------------- // Get @@ -234,7 +231,7 @@ private void getDataValueSet( @RequiresAuthority(anyOf = F_DATAVALUE_ADD) @ResponseBody public WebMessage postDxf2DataValueSet(ImportOptions importOptions, HttpServletRequest request) - throws IOException, ConflictException, @OpenApi.Ignore NotFoundException { + throws IOException, ConflictException { if (importOptions.isAsync()) { return startAsyncImport(importOptions, MediaType.APPLICATION_XML, request); } @@ -249,7 +246,7 @@ public WebMessage postDxf2DataValueSet(ImportOptions importOptions, HttpServletR @RequiresAuthority(anyOf = F_DATAVALUE_ADD) @ResponseBody public WebMessage postAdxDataValueSet(ImportOptions importOptions, HttpServletRequest request) - throws IOException, ConflictException, @OpenApi.Ignore NotFoundException { + throws IOException, ConflictException { if (importOptions.isAsync()) { return startAsyncImport(importOptions, MimeType.valueOf("application/adx+xml"), request); } @@ -264,7 +261,7 @@ public WebMessage postAdxDataValueSet(ImportOptions importOptions, HttpServletRe @RequiresAuthority(anyOf = F_DATAVALUE_ADD) @ResponseBody public WebMessage postJsonDataValueSet(ImportOptions importOptions, HttpServletRequest request) - throws IOException, ConflictException, @OpenApi.Ignore NotFoundException { + throws IOException, ConflictException { if (importOptions.isAsync()) { return startAsyncImport(importOptions, MediaType.APPLICATION_JSON, request); } @@ -279,7 +276,7 @@ public WebMessage postJsonDataValueSet(ImportOptions importOptions, HttpServletR @RequiresAuthority(anyOf = F_DATAVALUE_ADD) @ResponseBody public WebMessage postCsvDataValueSet(ImportOptions importOptions, HttpServletRequest request) - throws IOException, ConflictException, @OpenApi.Ignore NotFoundException { + throws IOException, ConflictException { if (importOptions.isAsync()) { return startAsyncImport(importOptions, MimeType.valueOf("application/csv"), request); } @@ -294,7 +291,7 @@ public WebMessage postCsvDataValueSet(ImportOptions importOptions, HttpServletRe @RequiresAuthority(anyOf = F_DATAVALUE_ADD) @ResponseBody public WebMessage postPdfDataValueSet(ImportOptions importOptions, HttpServletRequest request) - throws IOException, ConflictException, @OpenApi.Ignore NotFoundException { + throws IOException, ConflictException { if (importOptions.isAsync()) { return startAsyncImport(importOptions, MediaType.APPLICATION_PDF, request); } @@ -312,13 +309,13 @@ public WebMessage postPdfDataValueSet(ImportOptions importOptions, HttpServletRe /** Starts an asynchronous import task. */ private WebMessage startAsyncImport( ImportOptions importOptions, MimeType mimeType, HttpServletRequest request) - throws ConflictException, IOException, NotFoundException { + throws ConflictException, IOException { JobConfiguration config = new JobConfiguration(DATAVALUE_IMPORT); User currentUser = userService.getUserByUsername(CurrentUserUtil.getCurrentUsername()); config.setExecutedBy(currentUser.getUid()); config.setJobParameters(importOptions); - jobSchedulerService.createThenExecute(config, mimeType, request.getInputStream()); + jobExecutionService.executeOnceNow(config, mimeType, request.getInputStream()); return jobConfigurationReport(config); } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EnrollmentAggregateAnalyticsController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EnrollmentAggregateAnalyticsController.java index 844bb11460a3..33c0188cc65b 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EnrollmentAggregateAnalyticsController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EnrollmentAggregateAnalyticsController.java @@ -39,7 +39,7 @@ import static org.hisp.dhis.system.grid.GridUtils.toXls; import static org.hisp.dhis.system.grid.GridUtils.toXlsx; import static org.hisp.dhis.system.grid.GridUtils.toXml; -import static org.hisp.dhis.util.PeriodCriteriaUtils.defineDefaultPeriodForCriteria; +import static org.hisp.dhis.util.PeriodCriteriaUtils.addDefaultPeriodIfAbsent; import static org.hisp.dhis.webapi.dimension.EnrollmentAnalyticsPrefixStrategy.INSTANCE; import static org.hisp.dhis.webapi.utils.ContextUtils.CONTENT_TYPE_CSV; import static org.hisp.dhis.webapi.utils.ContextUtils.CONTENT_TYPE_EXCEL; @@ -256,7 +256,7 @@ private EventQueryParams getEventQueryParams( SystemSettings settings = settingsProvider.getCurrentSettings(); criteria.definePageSize(settings.getAnalyticsMaxLimit()); - defineDefaultPeriodForCriteria(criteria, settings.getAnalysisRelativePeriod()); + addDefaultPeriodIfAbsent(criteria, settings.getAnalysisRelativePeriod()); EventDataQueryRequest request = EventDataQueryRequest.builder() diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EventAggregateAnalyticsController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EventAggregateAnalyticsController.java index 43e3ab37e4e1..a93af40289e5 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EventAggregateAnalyticsController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EventAggregateAnalyticsController.java @@ -275,8 +275,7 @@ private EventQueryParams getEventQueryParams( SystemSettings settings = settingsProvider.getCurrentSettings(); criteria.definePageSize(settings.getAnalyticsMaxLimit()); - PeriodCriteriaUtils.defineDefaultPeriodForCriteria( - criteria, settings.getAnalysisRelativePeriod()); + PeriodCriteriaUtils.addDefaultPeriodIfAbsent(criteria, settings.getAnalysisRelativePeriod()); EventDataQueryRequest request = EventDataQueryRequest.builder() diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EventQueryAnalyticsController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EventQueryAnalyticsController.java index b4c6ac675d00..946ee6989b0d 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EventQueryAnalyticsController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EventQueryAnalyticsController.java @@ -324,7 +324,7 @@ private EventQueryParams getEventQueryParams( EndpointAction endpointAction) { criteria.definePageSize(settingsProvider.getCurrentSettings().getAnalyticsMaxLimit()); - PeriodCriteriaUtils.defineDefaultPeriodForCriteria( + PeriodCriteriaUtils.addDefaultPeriodIfAbsent( criteria, settingsProvider.getCurrentSettings().getAnalysisRelativePeriod()); EventDataQueryRequest request = diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/GeoJsonImportController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/GeoJsonImportController.java index 22537a7cacda..14895ef6a78d 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/GeoJsonImportController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/GeoJsonImportController.java @@ -46,12 +46,10 @@ import org.hisp.dhis.dxf2.importsummary.ImportStatus; import org.hisp.dhis.dxf2.webmessage.WebMessage; import org.hisp.dhis.feedback.ConflictException; -import org.hisp.dhis.feedback.NotFoundException; import org.hisp.dhis.feedback.Status; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.scheduling.JobConfiguration; -import org.hisp.dhis.scheduling.JobConfigurationService; -import org.hisp.dhis.scheduling.JobSchedulerService; +import org.hisp.dhis.scheduling.JobExecutionService; import org.hisp.dhis.scheduling.JobType; import org.hisp.dhis.scheduling.parameters.GeoJsonImportJobParams; import org.hisp.dhis.security.RequiresAuthority; @@ -77,12 +75,9 @@ @RestController @RequiredArgsConstructor public class GeoJsonImportController { - private final GeoJsonService geoJsonService; - - private final JobSchedulerService jobSchedulerService; - - private final JobConfigurationService jobConfigurationService; + private final GeoJsonService geoJsonService; + private final JobExecutionService jobExecutionService; private final UserService userService; @PostMapping( @@ -96,7 +91,7 @@ public WebMessage postImport( @RequestParam(required = false) boolean dryRun, @RequestParam(required = false, defaultValue = "false") boolean async, HttpServletRequest request) - throws IOException, ConflictException, NotFoundException { + throws IOException, ConflictException { GeoJsonImportJobParams params = GeoJsonImportJobParams.builder() .attributeId(attributeId) @@ -113,14 +108,14 @@ public WebMessage postImport( private WebMessage runImport( boolean async, GeoJsonImportJobParams params, HttpServletRequest request) - throws ConflictException, NotFoundException, IOException { + throws ConflictException, IOException { User currentUser = userService.getUserByUsername(CurrentUserUtil.getCurrentUsername()); if (async) { JobConfiguration jobConfig = new JobConfiguration(JobType.GEOJSON_IMPORT); jobConfig.setJobParameters(params); jobConfig.setExecutedBy(currentUser.getUid()); - jobSchedulerService.createThenExecute(jobConfig, APPLICATION_JSON, request.getInputStream()); + jobExecutionService.executeOnceNow(jobConfig, APPLICATION_JSON, request.getInputStream()); return jobConfigurationReport(jobConfig); } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/LockExceptionController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/LockExceptionController.java index da710c5fb24a..267c87f86c68 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/LockExceptionController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/LockExceptionController.java @@ -301,6 +301,6 @@ private boolean canCapture(OrganisationUnit captureTarget) { User currentUser = userService.getUserByUsername(CurrentUserUtil.getCurrentUsername()); return currentUser.isSuper() || currentUser.getOrganisationUnits().stream() - .anyMatch(ou -> captureTarget.getPath().startsWith(ou.getPath())); + .anyMatch(ou -> captureTarget.getStoredPath().startsWith(ou.getStoredPath())); } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PredictionController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PredictionController.java index 31a2cd084a31..f2e2594ad628 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PredictionController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PredictionController.java @@ -38,14 +38,13 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.dxf2.webmessage.WebMessage; import org.hisp.dhis.feedback.ConflictException; -import org.hisp.dhis.feedback.NotFoundException; import org.hisp.dhis.feedback.Status; import org.hisp.dhis.predictor.PredictionService; import org.hisp.dhis.predictor.PredictionSummary; import org.hisp.dhis.predictor.Predictor; import org.hisp.dhis.scheduling.JobConfiguration; +import org.hisp.dhis.scheduling.JobExecutionService; import org.hisp.dhis.scheduling.JobProgress; -import org.hisp.dhis.scheduling.JobSchedulerService; import org.hisp.dhis.scheduling.parameters.PredictorJobParameters; import org.hisp.dhis.security.RequiresAuthority; import org.hisp.dhis.user.CurrentUser; @@ -71,7 +70,7 @@ public class PredictionController { private final PredictionService predictionService; - private final JobSchedulerService jobSchedulerService; + private final JobExecutionService jobExecutionService; @RequestMapping(method = {RequestMethod.POST, RequestMethod.PUT}) @RequiresAuthority(anyOf = F_PREDICTOR_RUN) @@ -83,7 +82,7 @@ public WebMessage runPredictors( @RequestParam(value = "predictorGroup", required = false) List predictorGroups, @RequestParam(defaultValue = "false", required = false) boolean async, @CurrentUser UserDetails currentUser) - throws ConflictException, @OpenApi.Ignore NotFoundException { + throws ConflictException { if (async) { JobConfiguration config = new JobConfiguration(PREDICTOR); @@ -97,7 +96,7 @@ public WebMessage runPredictors( config.setJobParameters(params); config.setExecutedBy(currentUser.getUid()); - jobSchedulerService.createThenExecute(config); + jobExecutionService.executeOnceNow(config); return jobConfigurationReport(config); } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PushAnalysisController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PushAnalysisController.java index 725f4a462f4b..b285f2991d31 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PushAnalysisController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PushAnalysisController.java @@ -43,8 +43,7 @@ import org.hisp.dhis.pushanalysis.PushAnalysisService; import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.scheduling.JobConfiguration; -import org.hisp.dhis.scheduling.JobConfigurationService; -import org.hisp.dhis.scheduling.JobSchedulerService; +import org.hisp.dhis.scheduling.JobExecutionService; import org.hisp.dhis.scheduling.JobType; import org.hisp.dhis.scheduling.parameters.PushAnalysisJobParameters; import org.hisp.dhis.user.CurrentUserUtil; @@ -73,8 +72,7 @@ public class PushAnalysisController private final PushAnalysisService pushAnalysisService; private final ContextUtils contextUtils; - private final JobConfigurationService jobConfigurationService; - private final JobSchedulerService jobSchedulerService; + private final JobExecutionService jobExecutionService; @GetMapping("/{uid}/render") public void renderPushAnalytics(@PathVariable() String uid, HttpServletResponse response) @@ -112,6 +110,6 @@ public void sendPushAnalysis(@PathVariable() String uid) config.setJobParameters(new PushAnalysisJobParameters(uid)); config.setExecutedBy(CurrentUserUtil.getCurrentUserDetails().getUid()); - jobSchedulerService.createThenExecute(config); + jobExecutionService.executeOnceNow(config); } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ResourceTableController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ResourceTableController.java index 8b1f4e9088e9..084696f7dfaa 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ResourceTableController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ResourceTableController.java @@ -56,10 +56,8 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.dxf2.webmessage.WebMessage; import org.hisp.dhis.feedback.ConflictException; -import org.hisp.dhis.feedback.NotFoundException; import org.hisp.dhis.scheduling.JobConfiguration; -import org.hisp.dhis.scheduling.JobConfigurationService; -import org.hisp.dhis.scheduling.JobSchedulerService; +import org.hisp.dhis.scheduling.JobExecutionService; import org.hisp.dhis.scheduling.parameters.AnalyticsJobParameters; import org.hisp.dhis.scheduling.parameters.MonitoringJobParameters; import org.hisp.dhis.security.RequiresAuthority; @@ -85,8 +83,7 @@ @RequiredArgsConstructor public class ResourceTableController { - private final JobConfigurationService jobConfigurationService; - private final JobSchedulerService jobSchedulerService; + private final JobExecutionService jobExecutionService; @RequestMapping( value = "/analytics", @@ -103,7 +100,7 @@ public WebMessage analytics( @RequestParam(defaultValue = "false") Boolean skipOrgUnitOwnership, @RequestParam(required = false) Integer lastYears, @RequestParam(defaultValue = "false") Boolean skipOutliers) - throws ConflictException, @OpenApi.Ignore NotFoundException { + throws ConflictException { Set skipTableTypes = new HashSet<>(); Set skipPrograms = new HashSet<>(); @@ -147,8 +144,7 @@ public WebMessage analytics( @RequestMapping(method = {PUT, POST}) @RequiresAuthority(anyOf = F_PERFORM_MAINTENANCE) @ResponseBody - public WebMessage resourceTables(@CurrentUser UserDetails currentUser) - throws ConflictException, @OpenApi.Ignore NotFoundException { + public WebMessage resourceTables(@CurrentUser UserDetails currentUser) throws ConflictException { JobConfiguration config = new JobConfiguration(RESOURCE_TABLE); config.setExecutedBy(currentUser.getUid()); return execute(config); @@ -159,17 +155,16 @@ public WebMessage resourceTables(@CurrentUser UserDetails currentUser) method = {PUT, POST}) @RequiresAuthority(anyOf = F_PERFORM_MAINTENANCE) @ResponseBody - public WebMessage monitoring() throws ConflictException, @OpenApi.Ignore NotFoundException { + public WebMessage monitoring() throws ConflictException { JobConfiguration config = new JobConfiguration(MONITORING); config.setJobParameters(new MonitoringJobParameters()); return execute(config); } - private WebMessage execute(JobConfiguration configuration) - throws ConflictException, NotFoundException { + private WebMessage execute(JobConfiguration configuration) throws ConflictException { log.debug("Executing requested job of type: '{}'", configuration.getJobType()); - jobSchedulerService.createThenExecute(configuration); + jobExecutionService.executeOnceNow(configuration); return jobConfigurationReport(configuration); } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionComboController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionComboController.java index e243d3a836a0..a20d43baaf3e 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionComboController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionComboController.java @@ -27,18 +27,64 @@ */ package org.hisp.dhis.webapi.controller.category; +import static org.hisp.dhis.security.Authorities.F_CATEGORY_OPTION_COMBO_MERGE; +import static org.hisp.dhis.webapi.controller.CrudControllerAdvice.getHelpfulMessage; +import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; + +import jakarta.persistence.PersistenceException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.common.Maturity.Beta; 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.merge.MergeParams; +import org.hisp.dhis.merge.MergeService; import org.hisp.dhis.query.GetObjectListParams; +import org.hisp.dhis.security.RequiresAuthority; import org.hisp.dhis.webapi.controller.AbstractCrudController; +import org.springframework.http.HttpStatus; 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 */ +@Slf4j @Controller +@RequiredArgsConstructor @RequestMapping("/api/categoryOptionCombos") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) public class CategoryOptionComboController - extends AbstractCrudController {} + extends AbstractCrudController { + + private final MergeService categoryOptionComboMergeService; + + @Beta + @ResponseStatus(HttpStatus.OK) + @RequiresAuthority(anyOf = F_CATEGORY_OPTION_COMBO_MERGE) + @PostMapping(value = "/merge", produces = APPLICATION_JSON_VALUE) + public @ResponseBody WebMessage mergeCategoryOptionCombos(@RequestBody MergeParams params) + throws ConflictException { + log.info("CategoryOptionCombo merge received"); + + MergeReport report; + try { + report = categoryOptionComboMergeService.processMerge(params); + } catch (PersistenceException ex) { + String helpfulMessage = getHelpfulMessage(ex); + log.error("Error while processing CategoryOptionCombo merge: {}", helpfulMessage); + throw ex; + } + + log.info("CategoryOptionCombo 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/metadata/MetadataImportExportController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/metadata/MetadataImportExportController.java index b2428f6fb5fe..b5f34ff2442d 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/metadata/MetadataImportExportController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/metadata/MetadataImportExportController.java @@ -60,7 +60,6 @@ import org.hisp.dhis.dxf2.metadata.feedback.ImportReport; import org.hisp.dhis.dxf2.webmessage.WebMessage; import org.hisp.dhis.feedback.ConflictException; -import org.hisp.dhis.feedback.NotFoundException; import org.hisp.dhis.feedback.Status; import org.hisp.dhis.importexport.ImportStrategy; import org.hisp.dhis.jsonpatch.BulkJsonPatches; @@ -70,15 +69,13 @@ import org.hisp.dhis.render.RenderFormat; import org.hisp.dhis.render.RenderService; import org.hisp.dhis.scheduling.JobConfiguration; -import org.hisp.dhis.scheduling.JobConfigurationService; +import org.hisp.dhis.scheduling.JobExecutionService; import org.hisp.dhis.scheduling.JobProgress; -import org.hisp.dhis.scheduling.JobSchedulerService; import org.hisp.dhis.scheduling.JobType; import org.hisp.dhis.schema.SchemaService; import org.hisp.dhis.user.CurrentUserUtil; import org.hisp.dhis.user.User; import org.hisp.dhis.user.UserService; -import org.hisp.dhis.user.UserSettingsService; import org.hisp.dhis.webapi.mvc.annotation.ApiVersion; import org.hisp.dhis.webapi.service.ContextService; import org.springframework.http.ResponseEntity; @@ -111,16 +108,14 @@ public class MetadataImportExportController { private final GmlImportService gmlImportService; private final MetadataExportService metadataExportService; private final UserService userService; - private final UserSettingsService userSettingsService; - private final JobConfigurationService jobConfigurationService; - private final JobSchedulerService jobSchedulerService; + private final JobExecutionService jobExecutionService; private final ObjectMapper jsonMapper; private final BulkPatchManager bulkPatchManager; @PostMapping(value = "", consumes = APPLICATION_JSON_VALUE, produces = APPLICATION_JSON_VALUE) @ResponseBody public WebMessage postJsonMetadata(HttpServletRequest request) - throws IOException, ConflictException, @OpenApi.Ignore NotFoundException { + throws IOException, ConflictException { MetadataImportParams params = getMetadataImportParams(); if (params.isAsync()) { @@ -140,7 +135,7 @@ public WebMessage postJsonMetadata(HttpServletRequest request) @PostMapping(value = "", consumes = "application/csv") @ResponseBody public WebMessage postCsvMetadata(HttpServletRequest request) - throws IOException, ConflictException, @OpenApi.Ignore NotFoundException { + throws IOException, ConflictException { MetadataImportParams params = getMetadataImportParams(); String classKey = request.getParameter("classKey"); @@ -173,7 +168,7 @@ public WebMessage postCsvMetadata(HttpServletRequest request) @PostMapping(value = "/gml", consumes = APPLICATION_XML_VALUE) @ResponseBody public WebMessage postGmlMetadata(HttpServletRequest request) - throws IOException, ConflictException, @OpenApi.Ignore NotFoundException { + throws IOException, ConflictException { MetadataImportParams params = getMetadataImportParams(); if (params.isAsync()) { @@ -255,12 +250,12 @@ private MetadataImportParams getMetadataImportParams() { private WebMessage startAsyncMetadata( MetadataImportParams params, MimeType contentType, HttpServletRequest request) - throws IOException, ConflictException, NotFoundException { + throws IOException, ConflictException { JobConfiguration config = new JobConfiguration(JobType.METADATA_IMPORT); config.setExecutedBy(CurrentUserUtil.getCurrentUserDetails().getUid()); config.setJobParameters(params); - jobSchedulerService.createThenExecute(config, contentType, request.getInputStream()); + jobExecutionService.executeOnceNow(config, contentType, request.getInputStream()); return jobConfigurationReport(config); } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/organisationunit/OrganisationUnitController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/organisationunit/OrganisationUnitController.java index 6d6ca6fdffae..2265cbae802b 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/organisationunit/OrganisationUnitController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/organisationunit/OrganisationUnitController.java @@ -252,7 +252,8 @@ Limits results to organisation units on the given level or above (absolute start @CurrentUser UserDetails currentUser) throws ForbiddenException, BadRequestException, NotFoundException, ConflictException { OrganisationUnit parent = getEntity(uid); - List childrenWithLevel = List.of(like("path", parent.getPath(), MatchMode.START)); + List childrenWithLevel = + List.of(like("path", parent.getStoredPath(), MatchMode.START)); params.setParentLevel(parent.getLevel()); return getObjectListWith(params, response, currentUser, childrenWithLevel); } @@ -300,7 +301,7 @@ Limits results to organisation units on the given level or above (absolute start @CurrentUser UserDetails currentUser) throws ForbiddenException, BadRequestException, NotFoundException, ConflictException { OrganisationUnit root = getEntity(uid); - List ancestorsIds = List.of(root.getPath().split("/")); + List ancestorsIds = List.of(root.getStoredPath().split("/")); List ancestorPaths = new ArrayList<>(); for (int i = 0; i < ancestorsIds.size(); i++) ancestorPaths.add(Restrictions.eq("path", String.join("/", ancestorsIds.subList(0, i + 1)))); @@ -321,7 +322,7 @@ Limits results to organisation units on the given level or above (absolute start // when parent is root => no matches by adding an impossible in filter if (parent.getLevel() == 1) return getObjectListWith(params, response, currentUser, List.of(in("id", List.of()))); - List ancestorsIds = List.of(parent.getPath().split("/")); + List ancestorsIds = List.of(parent.getStoredPath().split("/")); List parentPaths = new ArrayList<>(); for (int i = 0; i < ancestorsIds.size() - 1; i++) parentPaths.add(Restrictions.eq("path", String.join("/", ancestorsIds.subList(0, i + 1)))); diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/scheduling/JobConfigurationController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/scheduling/JobConfigurationController.java index c2fe9c4b5cc1..c80fc39538c0 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/scheduling/JobConfigurationController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/scheduling/JobConfigurationController.java @@ -127,7 +127,7 @@ public JobTypes getJobTypeInfo() { public ObjectReport executeNow(@PathVariable("uid") String uid) throws NotFoundException, ConflictException { - jobSchedulerService.runInTransaction(uid); + jobSchedulerService.executeNow(uid); // OBS! This response is kept for better backwards compatibility return new ObjectReport(JobConfiguration.class, 0); @@ -236,11 +236,10 @@ private void checkExecutingUserOrAdmin(UID uid, boolean read) JobConfiguration obj = jobConfigurationService.getJobConfigurationByUid(uid.getValue()); if (obj == null) throw new NotFoundException(JobConfiguration.class, uid.getValue()); boolean isAuthorized = - currentUser != null - && (currentUser.isSuper() - || (!read && currentUser.isAuthorized("F_PERFORM_MAINTENANCE")) - || (read && currentUser.isAuthorized(F_JOB_LOG_READ.toString())) - || currentUser.getUid().equals(obj.getExecutedBy())); + currentUser.isSuper() + || !read && currentUser.isAuthorized("F_PERFORM_MAINTENANCE") + || read && currentUser.isAuthorized(F_JOB_LOG_READ.toString()) + || currentUser.getUid().equals(obj.getExecutedBy()); if (!isAuthorized) throw new ForbiddenException(JobConfiguration.class, obj.getUid()); } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/sms/SmsInboundController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/sms/SmsInboundController.java index fcc2c2e12b96..fdf0872dc4de 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/sms/SmsInboundController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/sms/SmsInboundController.java @@ -50,12 +50,10 @@ import org.hisp.dhis.feedback.BadRequestException; import org.hisp.dhis.feedback.ConflictException; import org.hisp.dhis.feedback.ForbiddenException; -import org.hisp.dhis.feedback.NotFoundException; import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.render.RenderService; import org.hisp.dhis.scheduling.JobConfiguration; -import org.hisp.dhis.scheduling.JobConfigurationService; -import org.hisp.dhis.scheduling.JobSchedulerService; +import org.hisp.dhis.scheduling.JobExecutionService; import org.hisp.dhis.scheduling.parameters.SmsInboundProcessingJobParameters; import org.hisp.dhis.security.RequiresAuthority; import org.hisp.dhis.sms.command.SMSCommand; @@ -96,8 +94,7 @@ public class SmsInboundController extends AbstractCrudController[,...]` diff --git a/dhis-2/dhis-web-api/src/main/resources/openapi/TrackedEntitiesExportController.md b/dhis-2/dhis-web-api/src/main/resources/openapi/TrackedEntitiesExportController.md index 2d089c687bff..4098e086a485 100644 --- a/dhis-2/dhis-web-api/src/main/resources/openapi/TrackedEntitiesExportController.md +++ b/dhis-2/dhis-web-api/src/main/resources/openapi/TrackedEntitiesExportController.md @@ -155,6 +155,9 @@ Get tracked entities with given UID(s). ### `*.parameter.TrackedEntityRequestParams.assignedUserMode` +Get tracked entities with events assigned to users according to the specified user mode. By default, +all events will be retrieved, regardless of whether a user is assigned. + ### `*.parameter.TrackedEntityRequestParams.assignedUsers` `[,...]` diff --git a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportControllerTest.java b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportControllerTest.java index 6d77650330d4..e0f654124bdf 100644 --- a/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportControllerTest.java +++ b/dhis-2/dhis-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/imports/TrackerImportControllerTest.java @@ -53,8 +53,7 @@ import org.hisp.dhis.feedback.NotFoundException; import org.hisp.dhis.render.DefaultRenderService; import org.hisp.dhis.render.RenderService; -import org.hisp.dhis.scheduling.JobConfigurationService; -import org.hisp.dhis.scheduling.JobSchedulerService; +import org.hisp.dhis.scheduling.JobExecutionService; import org.hisp.dhis.schema.SchemaService; import org.hisp.dhis.system.notification.Notification; import org.hisp.dhis.system.notification.Notifier; @@ -95,9 +94,7 @@ class TrackerImportControllerTest { @Mock private Notifier notifier; - @Mock private JobSchedulerService jobSchedulerService; - - @Mock private JobConfigurationService jobConfigurationService; + @Mock private JobExecutionService jobExecutionService; @Mock private UserService userService; @@ -121,8 +118,7 @@ public void setUp() { trackerImportService, csvEventService, notifier, - jobSchedulerService, - jobConfigurationService, + jobExecutionService, new ObjectMapper(), noteService); diff --git a/dhis-2/dhis-web-server/pom.xml b/dhis-2/dhis-web-server/pom.xml index 5577fc51763e..2d7993a319c3 100644 --- a/dhis-2/dhis-web-server/pom.xml +++ b/dhis-2/dhis-web-server/pom.xml @@ -15,7 +15,7 @@ ../ ${maven.build.timestamp} - 10.1.31 + 10.1.34 3.4.4 tomcat:${tomcat.version}-jre17 dhis2/core-dev:local diff --git a/dhis-2/pom.xml b/dhis-2/pom.xml index 18e2d7f865cf..01ae47c69525 100644 --- a/dhis-2/pom.xml +++ b/dhis-2/pom.xml @@ -85,7 +85,7 @@ - 3.2.1 + 3.2.2 0.6.1 @@ -98,7 +98,7 @@ 1.1.1.RELEASE 1.70 1.9.3 - 10.0 + 10.0.1 3.5.3 3.5.3 @@ -112,10 +112,10 @@ 2.0.8.RELEASE 2.4.4 1.6.0.Final - 6.5.1.RELEASE + 6.5.2.RELEASE - 11.1.0 + 11.1.1 5.6.15.Final 3.10.8 4.0.5 @@ -190,7 +190,7 @@ 2.13.0 9.2.1 4.4 - 1.9.4 + 1.10.0 3.17.0 1.13.0 1.5 @@ -199,13 +199,13 @@ 2.18.0 2.1.1 1.6.0 - 5.3.1 + 5.3.2 5.4.1 3.9.0 1.3.20 - 1.14.2 + 1.14.3 1.3.1 @@ -238,9 +238,9 @@ 3.5.0 2.6.0 2.18.0 - 11.1.1 + 12.0.0 4.8.6.6 - 2.43.0 + 2.44.1 0.8.12 1.2.0 1.0.36