createProgramTeAttributesCache();
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/cache/SoftCache.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/cache/SoftCache.java
new file mode 100644
index 000000000000..30ca2d876f6e
--- /dev/null
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/cache/SoftCache.java
@@ -0,0 +1,95 @@
+/*
+ * 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.cache;
+
+import java.lang.ref.SoftReference;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Supplier;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nonnull;
+
+/**
+ * A soft cache uses a concurrent map with {@link java.lang.ref.SoftReference} boxed values.
+ *
+ * This means the values might get GC-ed if the JVM needs memory. Therefore, when accessing a
+ * value by key, a {@link java.util.function.Supplier} needs to be passed in case the value needs
+ * re-computing.
+ *
+ * @author Jan Bernitt
+ * @since 2.42
+ */
+public final class SoftCache {
+
+ private final ConcurrentMap> cache = new ConcurrentHashMap<>();
+
+ /**
+ * Access a cached value by key.
+ *
+ * @param key the key to get from the cache, when null the value is computed and not cached
+ * @param onMiss a function to compute the value on a cache miss; the function must not return
+ * null
+ * @return the cached or computed value
+ */
+ @Nonnull
+ public T get(@CheckForNull String key, @Nonnull Supplier onMiss) {
+ if (key == null) return onMiss.get();
+ Supplier memOnMiss = memorize(onMiss);
+ T res =
+ cache
+ .compute(
+ key,
+ (k, v) -> {
+ if (v == null) return new SoftReference<>(memOnMiss.get());
+ if (v.get() != null) return v;
+ return new SoftReference<>(memOnMiss.get());
+ })
+ .get();
+ // in theory the get() of the SoftReference can become null
+ // just when it is returned from the map
+ // in which case the res is taken directly from the supplier
+ // since this should mean the JVM is low on memory
+ return res == null ? memOnMiss.get() : res;
+ }
+
+ /**
+ * @param onMiss assumed expensive to compute
+ * @return a supplier that will memorize the result once it has been computed
+ */
+ private Supplier memorize(Supplier onMiss) {
+ Object[] memorize = new Object[1];
+ return () -> {
+ @SuppressWarnings("unchecked")
+ T v = (T) memorize[0];
+ if (v != null) return v;
+ v = onMiss.get();
+ memorize[0] = v;
+ return v;
+ };
+ }
+}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/Category.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/Category.java
index 12eb663a33e5..b3297a804b5d 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/Category.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/Category.java
@@ -34,6 +34,7 @@
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
import com.google.common.collect.Lists;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -92,6 +93,10 @@ public void removeCategoryOption(CategoryOption categoryOption) {
categoryOption.getCategories().remove(this);
}
+ public void removeCategoryOptions(Collection categoryOptions) {
+ categoryOptions.forEach(this::removeCategoryOption);
+ }
+
public void removeAllCategoryOptions() {
for (CategoryOption categoryOption : categoryOptions) {
categoryOption.getCategories().remove(this);
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryDimension.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryDimension.java
index b70f0474eded..e5529076ebf7 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryDimension.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryDimension.java
@@ -33,7 +33,9 @@
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.List;
+import javax.annotation.Nonnull;
import org.hisp.dhis.common.BaseIdentifiableObject;
import org.hisp.dhis.common.DimensionalEmbeddedObject;
import org.hisp.dhis.common.DxfNamespaces;
@@ -83,6 +85,18 @@ public void setItems(List items) {
this.items = items;
}
+ public void addItem(CategoryOption item) {
+ this.getItems().add(item);
+ }
+
+ public void removeItem(CategoryOption item) {
+ this.getItems().remove(item);
+ }
+
+ public void removeItems(@Nonnull Collection items) {
+ items.forEach(this::removeItem);
+ }
+
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("CategoryDimension{");
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOption.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOption.java
index 465a63b2acec..6b1cca0a2b1e 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOption.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOption.java
@@ -32,6 +32,7 @@
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
+import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
@@ -116,11 +117,19 @@ public void addCategoryOptionCombo(CategoryOptionCombo dataElementCategoryOption
dataElementCategoryOptionCombo.getCategoryOptions().add(this);
}
+ public void addCategoryOptionCombos(Collection categoryOptionCombos) {
+ categoryOptionCombos.forEach(this::addCategoryOptionCombo);
+ }
+
public void removeCategoryOptionCombo(CategoryOptionCombo dataElementCategoryOptionCombo) {
categoryOptionCombos.remove(dataElementCategoryOptionCombo);
dataElementCategoryOptionCombo.getCategoryOptions().remove(this);
}
+ public void removeCategoryOptionCombos(Collection categoryOptionCombos) {
+ categoryOptionCombos.forEach(this::removeCategoryOptionCombo);
+ }
+
public void addOrganisationUnit(OrganisationUnit organisationUnit) {
organisationUnits.add(organisationUnit);
organisationUnit.getCategoryOptions().add(this);
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionCombo.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionCombo.java
index 532afe95cc99..d728fd6b594e 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionCombo.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionCombo.java
@@ -33,6 +33,7 @@
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
+import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
@@ -151,6 +152,10 @@ public void removeCategoryOption(CategoryOption dataElementCategoryOption) {
dataElementCategoryOption.getCategoryOptionCombos().remove(this);
}
+ public void removeCategoryOptions(Collection categoryOptions) {
+ categoryOptions.forEach(this::removeCategoryOption);
+ }
+
public void removeAllCategoryOptions() {
categoryOptions.clear();
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionComboStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionComboStore.java
index a2b494997579..3aa5be77c8c4 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionComboStore.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionComboStore.java
@@ -27,6 +27,7 @@
*/
package org.hisp.dhis.category;
+import java.util.Collection;
import java.util.List;
import java.util.Set;
import org.hisp.dhis.common.IdentifiableObjectStore;
@@ -56,4 +57,13 @@ CategoryOptionCombo getCategoryOptionCombo(
* @return a List of {@link CategoryOptionCombo} or empty List
*/
List getCategoryOptionCombosByGroupUid(String groupId, String dataElementId);
+
+ /**
+ * Retrieves all CategoryOptionCombos with a ref to any of the CategoryOptions passed in.
+ *
+ * @param categoryOptions refs to search for
+ * @return categoryOptionCombos with refs to categoryOptions
+ */
+ List getCategoryOptionCombosByCategoryOption(
+ Collection categoryOptions);
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionGroup.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionGroup.java
index f15116a66b59..84c68c08e4d5 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionGroup.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionGroup.java
@@ -32,6 +32,7 @@
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
+import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import org.hisp.dhis.common.BaseDimensionalItemObject;
@@ -128,4 +129,8 @@ public void removeCategoryOption(CategoryOption categoryOption) {
members.remove(categoryOption);
categoryOption.getGroups().remove(this);
}
+
+ public void removeCategoryOptions(Collection categoryOptions) {
+ categoryOptions.forEach(this::removeCategoryOption);
+ }
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionGroupStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionGroupStore.java
index 2979e635c789..78f14b3750fc 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionGroupStore.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryOptionGroupStore.java
@@ -27,9 +27,12 @@
*/
package org.hisp.dhis.category;
+import java.util.Collection;
import java.util.List;
import org.hisp.dhis.common.IdentifiableObjectStore;
public interface CategoryOptionGroupStore extends IdentifiableObjectStore {
List getCategoryOptionGroups(CategoryOptionGroupSet groupSet);
+
+ List getByCategoryOption(Collection categoryOptions);
}
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 92999fe0417a..f16c298f3fee 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
@@ -32,6 +32,7 @@
import java.util.Set;
import org.apache.commons.collections4.SetValuedMap;
import org.hisp.dhis.common.IdScheme;
+import org.hisp.dhis.common.UID;
import org.hisp.dhis.dataelement.DataElement;
import org.hisp.dhis.dataelement.DataElementOperand;
import org.hisp.dhis.dataset.DataSet;
@@ -145,6 +146,14 @@ public interface CategoryService {
*/
List getAttributeDataDimensionCategoriesNoAcl();
+ /**
+ * Retrieves all Categories with a ref to any of the CategoryOptions passed in.
+ *
+ * @param categoryOptions refs to search for
+ * @return categories with refs to categoryOptions
+ */
+ List getCategoriesByCategoryOption(Collection categoryOptions);
+
// -------------------------------------------------------------------------
// CategoryOption
// -------------------------------------------------------------------------
@@ -461,6 +470,15 @@ CategoryOptionCombo getCategoryOptionCombo(
/** Updates the name property of all category option combinations. */
void updateCategoryOptionComboNames();
+ /**
+ * Retrieves all CategoryOptionCombos with a ref to any of the CategoryOptions passed in.
+ *
+ * @param categoryOptions refs to search for
+ * @return categoryOptionCombos with refs to categoryOptions
+ */
+ List getCategoryOptionCombosByCategoryOption(
+ Collection categoryOptions);
+
// -------------------------------------------------------------------------
// DataElementOperand
// -------------------------------------------------------------------------
@@ -509,6 +527,8 @@ CategoryOptionCombo getCategoryOptionCombo(
List getCategoryOptionGroups(CategoryOptionGroupSet groupSet);
+ List getCategoryOptionGroupByCategoryOption(Collection categoryOptions);
+
/**
* Returns a set of CategoryOptionGroups that may be seen by the current user, if the current user
* has any CategoryOptionGroupSet constraint(s).
@@ -541,4 +561,6 @@ CategoryOptionCombo getCategoryOptionCombo(
CategoryOption getDefaultCategoryOption();
Category getDefaultCategory();
+
+ List getCategoryOptionsByUid(List catOptionUids);
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryStore.java
index 8c83c541a69c..71d6587f2a83 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryStore.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryStore.java
@@ -27,6 +27,7 @@
*/
package org.hisp.dhis.category;
+import java.util.Collection;
import java.util.List;
import org.hisp.dhis.common.DataDimensionType;
import org.hisp.dhis.common.GenericDimensionalObjectStore;
@@ -40,4 +41,6 @@ public interface CategoryStore extends GenericDimensionalObjectStore {
List getCategories(DataDimensionType dataDimensionType, boolean dataDimension);
List getCategoriesNoAcl(DataDimensionType dataDimensionType, boolean dataDimension);
+
+ List getCategoriesByCategoryOption(Collection categoryOptions);
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/HashUtils.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/HashUtils.java
index d77a6cd72cef..d21c6b83967f 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/HashUtils.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/HashUtils.java
@@ -43,6 +43,26 @@ private HashUtils() {
throw new IllegalStateException("Utility class");
}
+ /**
+ * Calculates a MD5 hash for the given input string.
+ *
+ * @param bytes the input string.
+ * @return the hash.
+ */
+ public static String hashMD5(@Nonnull byte[] bytes) {
+ return Hashing.md5().hashBytes(bytes).toString();
+ }
+
+ /**
+ * Calculates a SHA1 hash for the given input string.
+ *
+ * @param bytes the input string.
+ * @return the hash.
+ */
+ public static String hashSHA1(@Nonnull byte[] bytes) {
+ return Hashing.sha1().hashBytes(bytes).toString();
+ }
+
/**
* Calculates a SHA256 hash for the given input string.
*
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/IdentifiableObjectManager.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/IdentifiableObjectManager.java
index 02a0685f8657..12a493210513 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/IdentifiableObjectManager.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/IdentifiableObjectManager.java
@@ -101,6 +101,16 @@ public interface IdentifiableObjectManager {
@CheckForNull
T get(@Nonnull Class type, @Nonnull String uid);
+ /**
+ * Retrieves the object of the given type and UID, or null if no object exists.
+ *
+ * @param type the object class type.
+ * @param uid the UID.
+ * @return the object with the given UID.
+ */
+ @CheckForNull
+ T get(@Nonnull Class type, @Nonnull UID uid);
+
/**
* Retrieves the object of the given type and UID, throws exception if no object exists.
*
@@ -172,9 +182,6 @@ List getByCode(
@CheckForNull
T search(@Nonnull Class type, @Nonnull String query);
- @Nonnull
- List filter(@Nonnull Class type, @Nonnull String query);
-
@Nonnull
List getAll(@Nonnull Class type);
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/OpenApi.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/OpenApi.java
index 54c3e793959a..538384551a39 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/OpenApi.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/OpenApi.java
@@ -217,6 +217,8 @@ enum Access {
* @return the class that represents the domain for the annotated controller {@link Class} or
* endpoint {@link java.lang.reflect.Method}.
*/
+ // TODO make this metadataEntity() ? => always point out the most related metadata object to
+ // categorise a controller by?
Class> entity() default EntityType.class;
/**
@@ -467,6 +469,7 @@ enum Status {
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
+ @Inherited
@interface Shared {
/**
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/UID.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/UID.java
index 35d779963a46..ee5b85334e69 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/UID.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/UID.java
@@ -105,4 +105,9 @@ public static Set toValueSet(Collection uids) {
public static List toValueList(Collection uids) {
return uids.stream().map(UID::getValue).toList();
}
+
+ public static Set toUidValueSet(
+ @Nonnull Collection elements) {
+ return elements.stream().map(BaseIdentifiableObject::getUid).collect(toUnmodifiableSet());
+ }
}
diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/EventStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/cache/Region.java
similarity index 58%
rename from dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/EventStore.java
rename to dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/cache/Region.java
index 1bccaaaeea3c..05eb0773b410 100644
--- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/EventStore.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/cache/Region.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2004-2022, University of Oslo
+ * Copyright (c) 2004-2024, University of Oslo
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -25,28 +25,46 @@
* (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.tracker.export.event;
-
-import java.util.List;
-import java.util.Set;
-import org.hisp.dhis.program.Event;
-import org.hisp.dhis.tracker.export.Page;
-import org.hisp.dhis.tracker.export.PageParams;
+package org.hisp.dhis.common.cache;
/**
- * @author Morten Olav Hansen
+ * Enum is used to make sure we do not use same region twice. Each method should have its own
+ * constant.
*/
-public interface EventStore {
- /** Get all events matching given params. */
- List getEvents(EventQueryParams params);
-
- /** Get a page of events matching given params. */
- Page getEvents(EventQueryParams params, PageParams pageParams);
-
- /**
- * Fields the {@link #getEvents(EventQueryParams)} can order events by. Ordering by fields other
- * than these is considered a programmer error. Validation of user provided field names should
- * occur before calling {@link #getEvents(EventQueryParams)}.
- */
- Set getOrderableFields();
+@SuppressWarnings("squid:S115") // allow non enum-ish names
+public enum Region {
+ analyticsResponse,
+ defaultObjectCache,
+ isDataApproved,
+ allConstantsCache,
+ inUserOrgUnitHierarchy,
+ inUserSearchOrgUnitHierarchy,
+ periodIdCache,
+ userAccountRecoverAttempt,
+ userFailedLoginAttempt,
+ twoFaDisableFailedAttempt,
+ programOwner,
+ programTempOwner,
+ currentUserGroupInfoCache,
+ attrOptionComboIdCache,
+ googleAccessToken,
+ dataItemsPagination,
+ metadataAttributes,
+ canDataWriteCocCache,
+ analyticsSql,
+ propertyTransformerCache,
+ programHasRulesCache,
+ userGroupNameCache,
+ userDisplayNameCache,
+ pgmOrgUnitAssocCache,
+ catOptOrgUnitAssocCache,
+ dataSetOrgUnitAssocCache,
+ apiTokensCache,
+ teAttributesCache,
+ programTeAttributesCache,
+ userGroupUIDCache,
+ securityCache,
+ dataIntegritySummaryCache,
+ dataIntegrityDetailsCache,
+ queryAliasCache
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/collection/CollectionUtils.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/collection/CollectionUtils.java
index eba00bea3f3c..4dce993d1163 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/collection/CollectionUtils.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/collection/CollectionUtils.java
@@ -214,6 +214,7 @@ private static Set union(@Nonnull Set... more) {
* @return a string, or null if no matches.
*/
public static String popStartsWith(Collection collection, String prefix) {
+ if (collection == null || collection.isEmpty()) return null;
Iterator iterator = collection.iterator();
while (iterator.hasNext()) {
diff --git a/dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/filter/UserRoleCanIssueFilter.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/event/CacheInvalidationEvent.java
similarity index 70%
rename from dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/filter/UserRoleCanIssueFilter.java
rename to dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/event/CacheInvalidationEvent.java
index d9cab6377453..b7ddf01259c9 100644
--- a/dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/filter/UserRoleCanIssueFilter.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/event/CacheInvalidationEvent.java
@@ -25,31 +25,31 @@
* (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.filter;
+package org.hisp.dhis.common.event;
-import org.hisp.dhis.commons.filter.Filter;
-import org.hisp.dhis.user.User;
-import org.hisp.dhis.user.UserRole;
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import lombok.Getter;
+import org.hisp.dhis.common.cache.Region;
+import org.springframework.context.ApplicationEvent;
/**
- * @author Lars Helge Overland
+ * @author Ameen Mohamed
*/
-public class UserRoleCanIssueFilter implements Filter {
- private User user;
+@Getter
+public class CacheInvalidationEvent extends ApplicationEvent {
+ private final Region region;
+ private final String key;
- private boolean canGrantOwnUserRoles = false;
-
- protected UserRoleCanIssueFilter() {}
-
- public UserRoleCanIssueFilter(User user, boolean canGrantOwnUserRoles) {
- if (user != null) {
- this.user = user;
- this.canGrantOwnUserRoles = canGrantOwnUserRoles;
- }
+ public CacheInvalidationEvent(Object source, @Nonnull Region region, @Nullable String key) {
+ super(source);
+ this.region = region;
+ this.key = key;
}
- @Override
- public boolean retain(UserRole group) {
- return user != null && user.canIssueUserRole(group, canGrantOwnUserRoles);
+ public CacheInvalidationEvent(Object source, @Nonnull Region region) {
+ super(source);
+ this.region = region;
+ this.key = null;
}
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/Dashboard.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/Dashboard.java
index b7dbab8bff09..96d0d2e8b7c5 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/Dashboard.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/Dashboard.java
@@ -43,8 +43,12 @@
import org.hisp.dhis.common.MetadataObject;
import org.hisp.dhis.dashboard.design.ItemConfig;
import org.hisp.dhis.dashboard.design.Layout;
+import org.hisp.dhis.dashboard.embedded.EmbeddedDashboard;
/**
+ * Encapsulates information about an embedded dashboard. An embedded dashboard is typically loaded
+ * from an external provider.
+ *
* @author Lars Helge Overland
*/
@NoArgsConstructor
@@ -67,6 +71,9 @@ public class Dashboard extends BaseNameableObject implements MetadataObject {
/** Allowed filter dimensions (if any) which may be used for the dashboard. */
private List allowedFilters = new ArrayList<>();
+ /** Optional, only set if this dashboard is embedded and loaded from an external provider. */
+ private EmbeddedDashboard embedded;
+
// -------------------------------------------------------------------------
// Constructors
// -------------------------------------------------------------------------
@@ -146,4 +153,14 @@ public List getAllowedFilters() {
public void setAllowedFilters(List allowedFilters) {
this.allowedFilters = allowedFilters;
}
+
+ @JsonProperty
+ @JacksonXmlProperty(namespace = DXF_2_0)
+ public EmbeddedDashboard getEmbedded() {
+ return embedded;
+ }
+
+ public void setEmbedded(EmbeddedDashboard embedded) {
+ this.embedded = embedded;
+ }
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/DashboardItemType.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/DashboardItemType.java
index ea804396b2a7..4f1122f41b8d 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/DashboardItemType.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/DashboardItemType.java
@@ -28,6 +28,8 @@
package org.hisp.dhis.dashboard;
/**
+ * Encapsulates type of dashboard item.
+ *
* @author Lars Helge Overland
*/
public enum DashboardItemType {
diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/dataelement/DataElementMergeProcessor.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/embedded/EmbeddedDashboard.java
similarity index 66%
rename from dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/dataelement/DataElementMergeProcessor.java
rename to dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/embedded/EmbeddedDashboard.java
index 57ca57b21ccf..c6f08a84c83a 100644
--- a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/dataelement/DataElementMergeProcessor.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/embedded/EmbeddedDashboard.java
@@ -25,30 +25,31 @@
* (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.dataelement;
+package org.hisp.dhis.dashboard.embedded;
-import lombok.RequiredArgsConstructor;
-import org.hisp.dhis.merge.MergeProcessor;
-import org.hisp.dhis.merge.MergeService;
-import org.springframework.stereotype.Component;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
/**
- * Implementation of {@link MergeProcessor} that currently only uses its default method.
+ * Encapsulates metadata for an embedded and externally provided dashboard.
*
- * @author david mackessy
+ * @author Lars Helge Overland
*/
-@Component
-@RequiredArgsConstructor
-public class DataElementMergeProcessor implements MergeProcessor {
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class EmbeddedDashboard implements Serializable {
+ /** Provider of embedded dashboards. */
+ @JsonProperty private EmbeddedProvider provider;
- /**
- * Spring injects the correct service based on the variable name (when there are multiple
- * implementations to choose from). So The {@link DataElementMergeService} gets injected here.
- */
- private final MergeService dataElementMergeService;
+ /** Identifier for embedded dashboard. */
+ @JsonProperty private String id;
- @Override
- public MergeService getMergeService() {
- return dataElementMergeService;
- }
+ /** Customization options for embedded dashboard. */
+ @JsonProperty private EmbeddedOptions options;
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/embedded/EmbeddedOptions.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/embedded/EmbeddedOptions.java
new file mode 100644
index 000000000000..f856b5ba457f
--- /dev/null
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/embedded/EmbeddedOptions.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.dashboard.embedded;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/**
+ * Encapsulates customization options for embedded dashboards.
+ *
+ * @author Lars Helge Overland
+ */
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class EmbeddedOptions implements Serializable {
+ /** Hide tab. Applies to Superset. */
+ @JsonProperty private boolean hideTab;
+
+ /** Hide the chart controls. Applies to Superset. */
+ @JsonProperty private boolean hideChartControls;
+
+ /** Filter options. Applies to Superset. */
+ @JsonProperty private FilterOptions filters;
+}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/embedded/EmbeddedProvider.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/embedded/EmbeddedProvider.java
new file mode 100644
index 000000000000..3b7ee0aca75c
--- /dev/null
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/embedded/EmbeddedProvider.java
@@ -0,0 +1,37 @@
+/*
+ * 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.dashboard.embedded;
+
+/**
+ * Enumeration of providers for embedded dashboards.
+ *
+ * @author Lars Helge Overland
+ */
+public enum EmbeddedProvider {
+ SUPERSET;
+}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/embedded/FilterOptions.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/embedded/FilterOptions.java
new file mode 100644
index 000000000000..90d5edb6da18
--- /dev/null
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/embedded/FilterOptions.java
@@ -0,0 +1,52 @@
+/*
+ * 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.dashboard.embedded;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/**
+ * Encapsulates embedded dashboard filter options.
+ *
+ * @author Lars Helge Overland
+ */
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class FilterOptions implements Serializable {
+ /** Whether filter sidebar should be accessible when opening the dashboard. */
+ @JsonProperty private boolean visible;
+
+ /** Whether filter sidebar should be expanded when opening the dashboard. */
+ @JsonProperty private boolean expanded;
+}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/SectionService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/SectionService.java
index d63b12c2393c..4f32edf657ec 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/SectionService.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/SectionService.java
@@ -27,73 +27,63 @@
*/
package org.hisp.dhis.dataset;
+import java.util.Collection;
import java.util.List;
+import org.hisp.dhis.dataelement.DataElement;
import org.hisp.dhis.indicator.Indicator;
public interface SectionService {
- String ID = SectionService.class.getName();
-
/**
- * Adds a Section.
+ * Adds a {@link Section}.
*
- * @param section the Section to add.
+ * @param section the {@link Section} to add.
* @return the generated identifier.
*/
long addSection(Section section);
/**
- * Updates a Section.
+ * Updates a {@link Section}.
*
- * @param section the Section to update.
+ * @param section the {@link Section} to update.
*/
void updateSection(Section section);
/**
- * Deletes a Section.
+ * Deletes a {@link Section}.
*
- * @param section the Section to delete.
+ * @param section the {@link Section} to delete.
*/
void deleteSection(Section section);
/**
- * Retrieves the Section with the given identifier.
+ * Retrieves the {@link Section} with the given UID.
*
- * @param id the identifier of the Section to retrieve.
- * @return the Section.
- */
- Section getSection(long id);
-
- /**
- * Retrieves the Section with the given identifier (uid).
- *
- * @param uid the identifier of the Section to retrieve.
- * @return the Section.
+ * @param uid the identifier of the {@link Section} to retrieve.
+ * @return the {@link Section}.
*/
Section getSection(String uid);
/**
- * Retrieves the Section with the given name.
+ * Retrieves sections associated with the data element with the given UID.
*
- * @param name the name of the Section to retrieve.
- * @return the Section.
+ * @param uid the data element UID.
+ * @return a list of {@link Section}.
*/
- Section getSectionByName(String name, Integer dataSetId);
+ List getSectionsByDataElement(String uid);
/**
- * Retrieves all Sections.
+ * Retrieves sections associated with the given data elements.
*
- * @return a Collection of Sections.
+ * @param dataElements the list of {@link DataElement}.
+ * @return a list of {@link Section}.
*/
- List getAllSections();
+ List getSectionsByDataElement(Collection dataElements);
/**
- * Retrieves all {@link Section}s referencing the provided {@link Indicator}s.
+ * Retrieves sections associated with the given indicators.
*
- * @return a Collection of {@link Section}s.
+ * @param indicators the list of {@link Indicator}.
+ * @return a list of {@link Section}.
*/
- List getSectionsByIndicators(List indicators);
-
- void removeIndicator(Section s, Indicator i);
-
- void addIndicator(Section s, Indicator i);
+ List getSectionsByIndicators(Collection indicators);
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/SectionStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/SectionStore.java
index 7e2d5990e506..c95419ddb794 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/SectionStore.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dataset/SectionStore.java
@@ -34,8 +34,6 @@
import org.hisp.dhis.indicator.Indicator;
public interface SectionStore extends IdentifiableObjectStore {
- String ID = SectionStore.class.getName();
-
/**
* Retrieves the Section with the given name and the given DataSet.
*
@@ -44,14 +42,27 @@ public interface SectionStore extends IdentifiableObjectStore {
*/
Section getSectionByName(String name, DataSet dataSet);
- List getSectionsByDataElement(String dataElementUid);
+ /**
+ * Retrieves sections associated with the data element with the given UID.
+ *
+ * @param uid the data element UID.
+ * @return a list of {@link Section}.
+ */
+ List getSectionsByDataElement(String uid);
/**
- * Retrieves all {@link Section}s referencing the provided {@link Indicator}s.
+ * Retrieves sections associated with the given indicators.
*
- * @return a Collection of {@link Section}s.
+ * @param indicators the list of {@link Indicator}.
+ * @return a list of {@link Section}.
*/
- List getSectionsByIndicators(List indicators);
+ List getSectionsByIndicators(Collection indicators);
- List getByDataElement(Collection dataElements);
+ /**
+ * Retrieves sections associated with the given data elements.
+ *
+ * @param dataElements the list of {@link DataElement}.
+ * @return a list of {@link Section}.
+ */
+ List getSectionsByDataElement(Collection dataElements);
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueService.java
index f4aff88d4a1a..7e06490ebdaf 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueService.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueService.java
@@ -32,6 +32,7 @@
import org.hisp.dhis.category.CategoryCombo;
import org.hisp.dhis.category.CategoryOptionCombo;
import org.hisp.dhis.common.IllegalQueryException;
+import org.hisp.dhis.common.UID;
import org.hisp.dhis.dataelement.DataElement;
import org.hisp.dhis.organisationunit.OrganisationUnit;
import org.hisp.dhis.period.Period;
@@ -212,4 +213,12 @@ DataValue getDataValue(
* @return true, if any value exist, otherwise false
*/
boolean dataValueExists(CategoryCombo combo);
+
+ /**
+ * Checks if any data values exist for the provided {@link DataElement} {@link UID}.
+ *
+ * @param uid the {@link DataElement} {@link UID} to check
+ * @return true, if any values exist, otherwise false
+ */
+ boolean dataValueExistsForDataElement(UID uid);
}
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 7991b7420853..61bd3550f9a2 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
@@ -32,6 +32,7 @@
import java.util.concurrent.TimeUnit;
import org.hisp.dhis.category.CategoryCombo;
import org.hisp.dhis.category.CategoryOptionCombo;
+import org.hisp.dhis.common.UID;
import org.hisp.dhis.dataelement.DataElement;
import org.hisp.dhis.organisationunit.OrganisationUnit;
import org.hisp.dhis.period.Period;
@@ -184,4 +185,12 @@ DataValue getDataValue(
* @return true, if any value exist, otherwise false
*/
boolean dataValueExists(CategoryCombo combo);
+
+ /**
+ * Checks if any data values exist for the provided {@link DataElement} {@link UID}.
+ *
+ * @param uid the {@link DataElement} {@link UID} to check
+ * @return true, if any values exist, otherwise false
+ */
+ boolean dataValueExistsForDataElement(String uid);
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dxf2/webmessage/responses/MergeWebResponse.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dxf2/webmessage/responses/MergeWebResponse.java
index 4e00bfb25e8f..17188062cb77 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dxf2/webmessage/responses/MergeWebResponse.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dxf2/webmessage/responses/MergeWebResponse.java
@@ -46,8 +46,8 @@ public MergeWebResponse(@Nonnull MergeReport mergeReport) {
MergeType mergeType = mergeReport.getMergeType();
this.mergeReport.setMessage(
mergeReport.hasErrorMessages()
- ? "%s merge has errors".formatted(mergeType)
- : "%s merge complete".formatted(mergeType));
+ ? "%s merge has errors".formatted(mergeType.getName())
+ : "%s merge complete".formatted(mergeType.getName()));
}
@Nonnull
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ErrorCode.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ErrorCode.java
index 7a68761c5c47..dea0cc65ed49 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ErrorCode.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/ErrorCode.java
@@ -65,6 +65,7 @@ public enum ErrorCode {
"Option set `{0}` of value type multi-text cannot have option codes with the separator character: `{1}`"),
E1119("{0} already exists: `{1}`"),
E1120("Update cannot be applied as it would make existing data values inaccessible"),
+ E1121("Data element `{0}` value type cannot be changed as it has associated data values"),
/* Org unit merge */
E1500("At least two source orgs unit must be specified"),
@@ -88,27 +89,17 @@ public enum ErrorCode {
E1522("User `{0}` is not allowed to move organisation `{1}` unit from parent `{2}`"),
E1523("User `{0}` is not allowed to move organisation `{1}` unit to parent `{2}`"),
- /* Indicator Type merge */
- E1530("At least one source indicator type must be specified"),
- E1531("Target indicator type must be specified"),
- E1532("Target indicator type cannot be a source indicator type"),
- E1533("{0} indicator type does not exist: `{1}`"),
-
- /* Indicator merge */
- E1540("At least one source indicator must be specified"),
- E1541("Target indicator must be specified"),
- E1542("Target indicator cannot be a source indicator"),
- E1543("{0} indicator does not exist: `{1}`"),
+ /* Generic merge errors */
+ E1530("At least one source {0} must be specified"),
+ E1531("Target {0} must be specified"),
+ E1532("Target {0} cannot be a source {1}"),
+ E1533("{0} {1} does not exist: `{2}`"),
+ E1534("dataMergeStrategy field must be specified. With value `DISCARD` or `LAST_UPDATED`"),
/* DataElement merge */
- E1550("At least one source data element must be specified"),
- E1551("Target data element must be specified"),
- E1552("Target data element cannot be a source data element"),
- E1553("{0} data element does not exist: `{1}`"),
- E1554("All source ValueTypes must match target ValueType: `{0}`. Other ValueTypes found: `{1}`"),
- E1555(
+ E1550("All source ValueTypes must match target ValueType: `{0}`. Other ValueTypes found: `{1}`"),
+ E1551(
"All source DataElementDomains must match target DataElementDomain: `{0}`. Other DataElementDomains found: `{1}`"),
- E1556("dataMergeStrategy field must be specified. With value `DISCARD` or `LAST_UPDATED`"),
/* Data */
E2000("Query parameters cannot be null"),
@@ -460,6 +451,7 @@ public enum ErrorCode {
"Query failed because a referenced table does not exist. Please ensure analytics job was run"),
E7145("Query failed because of a syntax error"),
E7146("A {0} date was not specified in periods, dimensions, filters"),
+ E7147("Query failed because of a missing column: `{0}`"),
/* Analytics outliers */
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/MergeReport.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/MergeReport.java
index 9d7c6dab1244..3a771fe1f91d 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/MergeReport.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/feedback/MergeReport.java
@@ -54,10 +54,6 @@ public class MergeReport implements ErrorMessageContainer {
@JsonProperty private Set sourcesDeleted = new HashSet<>();
@JsonProperty private String message;
- public MergeReport(MergeType mergeType) {
- this.mergeType = mergeType;
- }
-
@Override
public boolean hasErrorMessages() {
return !mergeErrors.isEmpty();
diff --git a/dhis-2/dhis-services/dhis-service-node/src/main/java/org/hisp/dhis/fieldfilter/Defaults.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/fieldfilter/Defaults.java
similarity index 100%
rename from dhis-2/dhis-services/dhis-service-node/src/main/java/org/hisp/dhis/fieldfilter/Defaults.java
rename to dhis-2/dhis-api/src/main/java/org/hisp/dhis/fieldfilter/Defaults.java
diff --git a/dhis-2/dhis-services/dhis-service-field-filtering/src/main/java/org/hisp/dhis/fieldfiltering/FieldPreset.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/fieldfiltering/FieldPreset.java
similarity index 98%
rename from dhis-2/dhis-services/dhis-service-field-filtering/src/main/java/org/hisp/dhis/fieldfiltering/FieldPreset.java
rename to dhis-2/dhis-api/src/main/java/org/hisp/dhis/fieldfiltering/FieldPreset.java
index 8e2dcb7b9099..f257c28c8a0e 100644
--- a/dhis-2/dhis-services/dhis-service-field-filtering/src/main/java/org/hisp/dhis/fieldfiltering/FieldPreset.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/fieldfiltering/FieldPreset.java
@@ -52,7 +52,7 @@ public enum FieldPreset {
FieldPreset(String name, List fields) {
this.name = name;
- this.fields = fields;
+ this.fields = List.copyOf(fields);
}
public String getName() {
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/interpretation/MentionUtils.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/interpretation/MentionUtils.java
index 42110e111df4..c17f9d41be8e 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/interpretation/MentionUtils.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/interpretation/MentionUtils.java
@@ -31,7 +31,6 @@
import java.util.Date;
import java.util.HashSet;
import java.util.List;
-import java.util.ListIterator;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -62,17 +61,4 @@ public static Set getMentionedUsers(String text, UserService userService)
}
return users;
}
-
- public static List removeCustomFilters(List filters) {
- List mentions = new ArrayList();
- ListIterator filterIterator = filters.listIterator();
- while (filterIterator.hasNext()) {
- String[] filterSplit = filterIterator.next().split(":");
- if (filterSplit[1].equals("in") && filterSplit[0].equals("mentions")) {
- mentions.add(filterSplit[2]);
- filterIterator.remove();
- }
- }
- return mentions;
- }
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeParams.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeParams.java
index d0f9ef9c7734..d3b81b230d3c 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeParams.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeParams.java
@@ -50,6 +50,4 @@ public class MergeParams {
@JsonProperty private boolean deleteSources;
@JsonProperty private DataMergeStrategy dataMergeStrategy;
-
- private MergeType mergeType;
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeProcessor.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeProcessor.java
deleted file mode 100644
index c26c3bcfb7ff..000000000000
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeProcessor.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (c) 2004-2023, University of Oslo
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- * Redistributions of source code must retain the above copyright notice, this
- * list of conditions and the following disclaimer.
- *
- * Redistributions in binary form must reproduce the above copyright notice,
- * this list of conditions and the following disclaimer in the documentation
- * and/or other materials provided with the distribution.
- * Neither the name of the HISP project nor the names of its contributors may
- * be used to endorse or promote products derived from this software without
- * specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package org.hisp.dhis.merge;
-
-import javax.annotation.Nonnull;
-import org.hisp.dhis.common.IdentifiableObject;
-import org.hisp.dhis.feedback.ConflictException;
-import org.hisp.dhis.feedback.MergeReport;
-
-/**
- * Interface with default method used to process merging of {@link IdentifiableObject}s uniformly,
- * in a standardised fashion. It requires an implementation of {@link MergeService}, which will
- * process the merging for its required use case.
- * It essentially calls each method of the {@link MergeService} in the following order:
- *
- *
- * - validate
- *
- merge
- *
- */
-public interface MergeProcessor {
-
- /**
- * @return implemented {@link MergeService} to process merge
- */
- MergeService getMergeService();
-
- /**
- * Processes a merge in full, using the implemented {@link MergeService} retrieved.
- *
- * @param mergeParams {@link MergeParams} to process
- * @return updated {@link MergeReport} with any errors
- */
- default MergeReport processMerge(@Nonnull MergeParams mergeParams) throws ConflictException {
- MergeReport mergeReport = new MergeReport(mergeParams.getMergeType());
-
- MergeRequest mergeRequest = getMergeService().validate(mergeParams, mergeReport);
- if (mergeReport.hasErrorMessages())
- throw new ConflictException("Merge validation error").setMergeReport(mergeReport);
-
- MergeReport report = getMergeService().merge(mergeRequest, mergeReport);
- if (report.hasErrorMessages())
- throw new ConflictException("Merge error").setMergeReport(mergeReport);
-
- return report;
- }
-}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeService.java
index 913503f4dea1..f99bb188783e 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeService.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeService.java
@@ -29,6 +29,7 @@
import javax.annotation.Nonnull;
import org.hisp.dhis.common.IdentifiableObject;
+import org.hisp.dhis.feedback.ConflictException;
import org.hisp.dhis.feedback.MergeReport;
import org.springframework.transaction.annotation.Transactional;
@@ -39,6 +40,27 @@
*/
public interface MergeService {
+ /**
+ * Processes a merge in full.
+ *
+ * @param mergeParams {@link MergeParams} to process
+ * @return updated {@link MergeReport} with any errors
+ */
+ @Transactional
+ default MergeReport processMerge(@Nonnull MergeParams mergeParams) throws ConflictException {
+ MergeReport mergeReport = new MergeReport();
+
+ MergeRequest mergeRequest = validate(mergeParams, mergeReport);
+ if (mergeReport.hasErrorMessages())
+ throw new ConflictException("Merge validation error").setMergeReport(mergeReport);
+
+ merge(mergeRequest, mergeReport);
+ if (mergeReport.hasErrorMessages())
+ throw new ConflictException("Merge error").setMergeReport(mergeReport);
+
+ return mergeReport;
+ }
+
/**
* This method transforms a {@link MergeParams} to a {@link MergeRequest}. If there are any
* errors/issues with the params then the {@link MergeReport} should be updated.
@@ -57,6 +79,5 @@ public interface MergeService {
* @param mergeReport report to be updated if any issues/errors with the {@link MergeRequest}
* @return {@link MergeReport}
*/
- @Transactional
MergeReport merge(@Nonnull MergeRequest request, @Nonnull MergeReport mergeReport);
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeType.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeType.java
index 56f73e09c965..a19697ace91b 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
@@ -27,15 +27,38 @@
*/
package org.hisp.dhis.merge;
+import com.fasterxml.jackson.annotation.JsonValue;
+import org.hisp.dhis.category.CategoryOption;
+import org.hisp.dhis.common.IdentifiableObject;
+import org.hisp.dhis.dataelement.DataElement;
+import org.hisp.dhis.indicator.Indicator;
+import org.hisp.dhis.indicator.IndicatorType;
+
/**
* Enum for merge type.
*
* @author david mackessy
*/
public enum MergeType {
- ORG_UNIT,
+ INDICATOR_TYPE(IndicatorType.class),
+ INDICATOR(Indicator.class),
+ DATA_ELEMENT(DataElement.class),
+ CATEGORY_OPTION(CategoryOption.class);
+
+ private final Class extends IdentifiableObject> clazz;
+ private final String name;
+
+ MergeType(Class extends IdentifiableObject> clazz) {
+ this.clazz = clazz;
+ this.name = clazz.getSimpleName();
+ }
+
+ public Class extends IdentifiableObject> getClazz() {
+ return this.clazz;
+ }
- INDICATOR_TYPE,
- INDICATOR,
- DATA_ELEMENT
+ @JsonValue
+ public String getName() {
+ return this.name;
+ }
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeValidator.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeValidator.java
index 471ecd7e6143..cc1e3fb5534c 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeValidator.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/merge/MergeValidator.java
@@ -28,6 +28,7 @@
package org.hisp.dhis.merge;
import java.util.Set;
+import javax.annotation.Nonnull;
import org.hisp.dhis.common.IdentifiableObject;
import org.hisp.dhis.common.UID;
import org.hisp.dhis.feedback.MergeReport;
@@ -37,18 +38,28 @@
*/
public interface MergeValidator {
+ /**
+ * Validates source & target {@link UID}s passed in the params. Validation used by all merges.
+ *
+ * @param params {@link MergeParams} that contain {@link UID}s to validate
+ * @param mergeReport {@link MergeReport} to update
+ * @param mergeType {@link MergeType}
+ * @return {@link MergeRequest} to process
+ */
+ MergeRequest validateUIDs(
+ @Nonnull MergeParams params, @Nonnull MergeReport mergeReport, @Nonnull MergeType mergeType);
+
/**
* Verifies whether the source {@link UID}s map to valid {@link IdentifiableObject}s.
* - If they are valid then they are added to verifiedSources param.
* - If any are not valid then the {@link MergeReport} is updated with an error.
*
* @param paramSources {@link UID}s
- * @param verifiedSources set to add verified source {@link UID}s
* @param mergeReport to update if any error
- * @param clazz {@link IdentifiableObject} type
+ * @param mergeType {@link MergeType}
+ * @return verified source {@link UID}s
*/
- void verifySources(
- Set paramSources, Set verifiedSources, MergeReport mergeReport, Class clazz);
+ Set verifySources(Set paramSources, MergeReport mergeReport, MergeType mergeType);
/**
* Checks whether the target is referenced in the sources collection
@@ -57,10 +68,10 @@ void verifySources(
* @param sources to check
* @param target to check if in sources
* @param mergeReport to update if any error
- * @param clazz {@link IdentifiableObject} type
+ * @param mergeType {@link MergeType}
*/
- void checkIsTargetInSources(
- Set sources, UID target, MergeReport mergeReport, Class clazz);
+ void checkIsTargetInSources(
+ Set sources, UID target, MergeReport mergeReport, MergeType mergeType);
/**
* Verifies whether the target {@link UID} maps to a valid {@link IdentifiableObject}.
@@ -71,9 +82,9 @@ void checkIsTargetInSources(
* @param mergeReport to update if any error
* @param sources to return in merge request
* @param params merge params with target to verify
- * @param clazz {@link IdentifiableObject} type
+ * @param mergeType {@link MergeType}
* @return merge request
*/
- MergeRequest verifyTarget(
- MergeReport mergeReport, Set sources, MergeParams params, Class clazz);
+ MergeRequest verifyTarget(
+ MergeReport mergeReport, Set sources, MergeParams params, MergeType mergeType);
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/message/MessageConversation.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/message/MessageConversation.java
index 316bbc99e996..c528e5332e9a 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/message/MessageConversation.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/message/MessageConversation.java
@@ -41,6 +41,7 @@
import org.apache.commons.lang3.StringUtils;
import org.hisp.dhis.common.BaseIdentifiableObject;
import org.hisp.dhis.common.DxfNamespaces;
+import org.hisp.dhis.common.UID;
import org.hisp.dhis.schema.annotation.PropertyRange;
import org.hisp.dhis.user.User;
@@ -192,12 +193,12 @@ public boolean isRead(User user) {
return false;
}
- public boolean markRead(User user) {
- for (UserMessage userMessage : userMessages) {
- if (userMessage.getUser() != null && userMessage.getUser().equals(user)) {
- boolean read = userMessage.isRead();
+ public boolean markRead(UID user) {
+ for (UserMessage msg : userMessages) {
+ if (msg.getUser() != null && msg.getUser().getUid().equals(user.getValue())) {
+ boolean read = msg.isRead();
- userMessage.setRead(true);
+ msg.setRead(true);
return !read;
}
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 144efee252e2..b64446845041 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
@@ -292,6 +292,10 @@ public void removeCategoryOption(CategoryOption categoryOption) {
categoryOption.getOrganisationUnits().remove(this);
}
+ public void removeCategoryOptions(Collection categoryOptions) {
+ categoryOptions.forEach(this::removeCategoryOption);
+ }
+
public void removeAllUsers() {
for (User user : users) {
user.getOrganisationUnits().remove(this);
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 33fd9263ddb0..f76dcab600e9 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
@@ -33,6 +33,7 @@
import java.util.Map;
import java.util.Set;
import javax.annotation.Nonnull;
+import org.hisp.dhis.common.UID;
import org.hisp.dhis.dataset.DataSet;
import org.hisp.dhis.hierarchy.HierarchyViolationException;
import org.hisp.dhis.program.Program;
@@ -132,14 +133,6 @@ public interface OrganisationUnitService extends OrganisationUnitDataIntegrityPr
*/
List getOrganisationUnitsByUid(@Nonnull Collection uids);
- /**
- * Returns a list of OrganisationUnits based on the given params.
- *
- * @param params the params.
- * @return a list of OrganisationUnits.
- */
- List getOrganisationUnitsByQuery(OrganisationUnitQueryParams params);
-
/**
* Returns an OrganisationUnit with a given name.
*
@@ -486,4 +479,12 @@ List getOrganisationUnitByCoordinate(
* @return
*/
List getSearchOrganisationUnitsUidsByUser(String username);
+
+ /**
+ * Returns all OrganisationUnits with refs to any of the CategoryOptions passed in.
+ *
+ * @param categoryOptions refs to search for.
+ * @return OrganisationUnits with refs to any of the CategoryOptions passed in
+ */
+ List getByCategoryOption(Collection categoryOptions);
}
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 8fcce43e67b1..f35af4770f09 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
@@ -186,4 +186,12 @@ Map> getOrganisationUnitDataSetAssocationMap(
List getOrganisationUnitUids(OrganisationUnitQueryParams params);
int updateAllOrganisationUnitsGeometryToNull();
+
+ /**
+ * Get OrganisationUnits with refs to any of the CategoryOptions passed in
+ *
+ * @param categoryOptions categoryOptions refs
+ * @return OrganisationUnits with refs to any of the CategoryOptions passed in
+ */
+ List getByCategoryOption(Collection categoryOptions);
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/period/PeriodService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/period/PeriodService.java
index 4836010871ae..1bae5b803803 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/period/PeriodService.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/period/PeriodService.java
@@ -258,7 +258,7 @@ public interface PeriodService {
PeriodType getPeriodType(int id);
/**
- * Returns a PeriodType with a given name.
+ * Returns a PeriodType with the given name.
*
* @param name the name of the PeriodType to return.
* @return the PeriodType with the given name, or null if no match.
@@ -282,6 +282,16 @@ public interface PeriodService {
*/
PeriodType reloadPeriodType(PeriodType periodType);
+ /**
+ * Returns a PeriodType with the given {@link PeriodTypeEnum} value.
+ *
+ * @param periodType the {@link PeriodTypeEnum}.
+ * @return the PeriodType with the given name, or null if no match.
+ */
+ default PeriodType getPeriodType(PeriodTypeEnum periodType) {
+ return getPeriodTypeByName(periodType.getName());
+ }
+
// -------------------------------------------------------------------------
// RelativePeriods
// -------------------------------------------------------------------------
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/program/ProgramTrackedEntityAttribute.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/program/ProgramTrackedEntityAttribute.java
index f08fd4afad12..2ea9d726b625 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/program/ProgramTrackedEntityAttribute.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/program/ProgramTrackedEntityAttribute.java
@@ -68,6 +68,8 @@ public class ProgramTrackedEntityAttribute extends BaseIdentifiableObject
private Boolean searchable = false;
+ private boolean skipIndividualAnalytics;
+
// -------------------------------------------------------------------------
// Constructors
// -------------------------------------------------------------------------
@@ -265,6 +267,16 @@ public DeviceRenderTypeMap getRenderType() {
return renderType;
}
+ @JsonProperty
+ @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
+ public boolean isSkipIndividualAnalytics() {
+ return skipIndividualAnalytics;
+ }
+
+ public void setSkipIndividualAnalytics(boolean skipIndividualAnalytics) {
+ this.skipIndividualAnalytics = skipIndividualAnalytics;
+ }
+
public void setRenderType(DeviceRenderTypeMap renderType) {
this.renderType = renderType;
}
@@ -295,5 +307,6 @@ private static void setShallowCopyValues(
copy.setSharing(original.getSharing());
copy.setSortOrder(original.getSortOrder());
copy.setTranslations(original.getTranslations());
+ copy.setSkipIndividualAnalytics(original.isSkipIndividualAnalytics());
}
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/relationship/RelationshipKey.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/relationship/RelationshipKey.java
index 98b1259f8108..e258ce7b7bae 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/relationship/RelationshipKey.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/relationship/RelationshipKey.java
@@ -30,7 +30,7 @@
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
-import org.apache.commons.lang3.StringUtils;
+import org.hisp.dhis.common.UID;
@Data
@Builder(toBuilder = true)
@@ -56,34 +56,34 @@ public RelationshipKey inverseKey() {
@Data
@Builder
public static class RelationshipItemKey {
- private final String trackedEntity;
+ private final UID trackedEntity;
- private final String enrollment;
+ private final UID enrollment;
- private final String event;
+ private final UID event;
public String asString() {
if (isTrackedEntity()) {
- return trackedEntity;
+ return trackedEntity.getValue();
} else if (isEnrollment()) {
- return enrollment;
+ return enrollment.getValue();
} else if (isEvent()) {
- return event;
+ return event.getValue();
}
return "ERROR";
}
public boolean isTrackedEntity() {
- return StringUtils.isNoneBlank(trackedEntity);
+ return trackedEntity != null;
}
public boolean isEnrollment() {
- return StringUtils.isNoneBlank(enrollment);
+ return enrollment != null;
}
public boolean isEvent() {
- return StringUtils.isNoneBlank(event);
+ return event != null;
}
}
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/resourcetable/ResourceTableService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/resourcetable/ResourceTableService.java
index 800c3fcae97c..186ada653ada 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/resourcetable/ResourceTableService.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/resourcetable/ResourceTableService.java
@@ -36,6 +36,9 @@ public interface ResourceTableService {
/** Generates resource tables. */
void generateResourceTables();
+ /** Replicates resource tables in the analytics database. */
+ void replicateAnalyticsResourceTables();
+
/** Generates data approval resource tables. */
void generateDataApprovalResourceTables();
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/resourcetable/ResourceTableType.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/resourcetable/ResourceTableType.java
index 30176f043b4a..1adb7e8fa0ef 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/resourcetable/ResourceTableType.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/resourcetable/ResourceTableType.java
@@ -44,6 +44,7 @@ public enum ResourceTableType {
ORG_UNIT_GROUP_SET_STRUCTURE,
CATEGORY_STRUCTURE,
DATA_ELEMENT_STRUCTURE,
+ DATA_SET,
PERIOD_STRUCTURE,
DATE_PERIOD_STRUCTURE,
DATA_ELEMENT_CATEGORY_OPTION_COMBO,
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobRunErrors.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobRunErrors.java
index 84bcc3e6f8b0..656f1dcdf2f7 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobRunErrors.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/scheduling/JobRunErrors.java
@@ -74,7 +74,8 @@ default JsonDate finished() {
return get("finished", JsonDate.class);
}
- @OpenApi.Description("""
+ @OpenApi.Description(
+ """
The file resource used to store the job's input
""")
@OpenApi.Property({UID.class, FileResource.class})
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 855d60d28143..27f121e3ae59 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
@@ -86,6 +86,7 @@ public enum Authorities {
F_INDICATOR_TYPE_MERGE,
F_INDICATOR_MERGE,
F_DATA_ELEMENT_MERGE,
+ F_CATEGORY_OPTION_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/setting/SystemSettings.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/setting/SystemSettings.java
index e211f4f51aea..26b2a534cd47 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/setting/SystemSettings.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/setting/SystemSettings.java
@@ -115,10 +115,7 @@ static boolean isTranslatable(@Nonnull String key) {
return LazySettings.isTranslatable(key);
}
- /*
- settings used in core
- */
-
+ /** Settings used in core */
default Locale getUiLocale() {
return asLocale("keyUiLocale", LocaleManager.DEFAULT_LOCALE);
}
@@ -308,6 +305,10 @@ default boolean getIncludeZeroValuesInAnalytics() {
return asBoolean("keyIncludeZeroValuesInAnalytics", false);
}
+ default boolean getEmbeddedDashboardsEnabled() {
+ return asBoolean("keyEmbeddedDashboardsEnabled", false);
+ }
+
default int getSqlViewMaxLimit() {
return asInt("keySqlViewMaxLimit", -1);
}
@@ -720,12 +721,14 @@ default String getGlobalShellAppName() {
return asString("globalShellAppName", "global-app-shell");
}
- /*
-
- Combinators based on several settings
-
- */
+ /**
+ * @return true if email verification is enforced for all users.
+ */
+ default boolean getEnforceVerifiedEmail() {
+ return asBoolean("enforceVerifiedEmail", false);
+ }
+ /** Combinators based on several settings. */
default boolean isEmailConfigured() {
return !getEmailHostName().isBlank() && !getEmailUsername().isBlank();
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/SystemUser.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/SystemUser.java
index 8aa2f6d2b048..b238adac245f 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/SystemUser.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/SystemUser.java
@@ -184,6 +184,11 @@ public boolean isTwoFactorEnabled() {
return false;
}
+ @Override
+ public boolean isEmailVerified() {
+ return true;
+ }
+
@Override
public boolean hasAnyRestrictions(Collection restrictions) {
return false;
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/User.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/User.java
index 12e672f36f8c..1bb2880b6670 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/User.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/User.java
@@ -41,6 +41,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
@@ -1192,6 +1193,12 @@ public String getVerifiedEmail() {
return this.verifiedEmail;
}
+ @JsonProperty
+ @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0)
+ public boolean isEmailVerified() {
+ return this.getEmail() != null && Objects.equals(this.getEmail(), this.getVerifiedEmail());
+ }
+
public void setVerifiedEmail(String verifiedEmail) {
this.verifiedEmail = verifiedEmail;
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserDetails.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserDetails.java
index 9c9db96a73b8..cca0ccb8ede9 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserDetails.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserDetails.java
@@ -28,6 +28,7 @@
package org.hisp.dhis.user;
import java.util.Collection;
+import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@@ -118,6 +119,7 @@ static UserDetails createUserDetails(
.password(user.getPassword())
.externalAuth(user.isExternalAuth())
.isTwoFactorEnabled(user.isTwoFactorEnabled())
+ .isEmailVerified(user.isEmailVerified())
.code(user.getCode())
.firstName(user.getFirstName())
.surname(user.getSurname())
@@ -243,10 +245,20 @@ static UserDetails createUserDetails(
boolean isTwoFactorEnabled();
+ boolean isEmailVerified();
+
boolean hasAnyRestrictions(Collection restrictions);
void setId(Long id);
+ default boolean canIssueUserRole(UserRole role, boolean canGrantOwnUserRole) {
+ if (role == null) return false;
+ if (isSuper()) return true;
+ if (hasAnyAuthorities(List.of(Authorities.ALL))) return true;
+ if (!canGrantOwnUserRole && getUserRoleIds().contains(role.getUid())) return false;
+ return getAllAuthorities().containsAll(role.getAuthorities());
+ }
+
default boolean isInUserHierarchy(String orgUnitPath) {
return isInUserHierarchy(orgUnitPath, getUserOrgUnitIds());
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserDetailsImpl.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserDetailsImpl.java
index e29438f00843..522bc1586093 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserDetailsImpl.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserDetailsImpl.java
@@ -53,6 +53,7 @@ public class UserDetailsImpl implements UserDetails {
private final String password;
private final boolean externalAuth;
private final boolean isTwoFactorEnabled;
+ private final boolean isEmailVerified;
private final boolean enabled;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
@@ -82,6 +83,11 @@ public boolean canModifyUser(User other) {
return auths.containsAll(other.getAllAuthorities());
}
+ @Override
+ public boolean isEmailVerified() {
+ return this.isEmailVerified;
+ }
+
@Override
public boolean hasAnyRestrictions(Collection restrictions) {
return getAllRestrictions().stream().anyMatch(restrictions::contains);
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserOrgUnitProperty.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserOrgUnitProperty.java
new file mode 100644
index 000000000000..702adebb2d02
--- /dev/null
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserOrgUnitProperty.java
@@ -0,0 +1,44 @@
+/*
+ * 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.user;
+
+public enum UserOrgUnitProperty {
+ ORG_UNITS("organisationUnits"),
+ DATA_VIEW_ORG_UNITS("dataViewOrganisationUnits"),
+ TEI_SEARCH_ORG_UNITS("teiSearchOrganisationUnits");
+
+ private final String value;
+
+ UserOrgUnitProperty(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserQueryParams.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserQueryParams.java
index 49752b1f2d03..a4e67fc429fb 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserQueryParams.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserQueryParams.java
@@ -75,6 +75,7 @@ public class UserQueryParams {
@ToString.Include private UserInvitationStatus invitationStatus;
+ /** If empty, no matching condition will be generated */
private Set organisationUnits = new HashSet<>();
private Set userGroups = new HashSet<>();
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserService.java
index 52e24f14c011..a64c41765021 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserService.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserService.java
@@ -41,11 +41,14 @@
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.hisp.dhis.common.IdentifiableObject;
+import org.hisp.dhis.common.UID;
import org.hisp.dhis.dataset.DataSet;
+import org.hisp.dhis.feedback.ConflictException;
import org.hisp.dhis.feedback.ErrorCode;
import org.hisp.dhis.feedback.ErrorReport;
import org.hisp.dhis.feedback.ForbiddenException;
import org.hisp.dhis.feedback.NotFoundException;
+import org.hisp.dhis.organisationunit.OrganisationUnit;
/**
* @author Chau Thu Tran
@@ -233,6 +236,17 @@ static boolean hasTwoFactorSecretForApproval(User user) {
*/
List getUsers(UserQueryParams params, @Nullable List orders);
+ /**
+ * Returns a list of users based on the given query parameters. If the specified list of orders
+ * are empty, default order of last name and first name will be applied.
+ *
+ * @param params the user query parameters.
+ * @param orders the already validated order strings (e.g. email:asc).
+ * @return a List of users.
+ */
+ List getUserIds(UserQueryParams params, @Nullable List orders)
+ throws ConflictException;
+
/**
* Returns the number of users based on the given query parameters.
*
@@ -406,12 +420,9 @@ static boolean hasTwoFactorSecretForApproval(User user) {
int countDataSetUserRoles(DataSet dataSet);
/**
- * Filters the given collection of user roles based on whether the current user is allowed to
- * issue it.
- *
- * @param userRoles the collection of user roles.
+ * @return IDs of the roles the current user can issue
*/
- void canIssueFilter(Collection userRoles);
+ List getRolesCurrentUserCanIssue();
/**
* Validate that the current user are allowed to create or modify properties of the given user.
@@ -891,4 +902,14 @@ void validateTwoFactorUpdate(boolean before, boolean after, User userToModify)
boolean isEmailVerified(User currentUser);
User getUserByVerificationToken(String token);
+
+ /**
+ * Method that retrieves all {@link User}s that have an entry for the {@link OrganisationUnit} in
+ * the given table
+ *
+ * @param orgUnitProperty {@link UserOrgUnitProperty} used to search
+ * @param uid {@link OrganisationUnit} {@link UID} to match on
+ * @return matching {@link User}s
+ */
+ List getUsersWithOrgUnit(@Nonnull UserOrgUnitProperty orgUnitProperty, @Nonnull UID uid);
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserStore.java
index 4348f74a7c03..f286f26e5850 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserStore.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserStore.java
@@ -38,6 +38,8 @@
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import org.hisp.dhis.common.IdentifiableObjectStore;
+import org.hisp.dhis.common.UID;
+import org.hisp.dhis.organisationunit.OrganisationUnit;
/**
* @author Nguyen Hong Duc
@@ -63,6 +65,8 @@ public interface UserStore extends IdentifiableObjectStore {
*/
List getUsers(UserQueryParams params, @Nullable List orders);
+ List getUserIds(UserQueryParams params, @Nullable List orders);
+
/**
* Returns the number of users based on the given query parameters.
*
@@ -224,4 +228,14 @@ Map> findNotifiableUsersWithPasswordLastUpdatedBetween(
User getUserByVerificationToken(String token);
User getUserByVerifiedEmail(String email);
+
+ /**
+ * Retrieves all {@link User}s that have an entry for the {@link OrganisationUnit} in the given
+ * table
+ *
+ * @param orgUnitProperty {@link UserOrgUnitProperty} used to search
+ * @param uid {@link OrganisationUnit} {@link UID} to match on
+ * @return matching {@link User}s
+ */
+ List getUsersWithOrgUnit(@Nonnull UserOrgUnitProperty orgUnitProperty, @Nonnull UID uid);
}
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/util/DateUtils.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/util/DateUtils.java
index 57c2e928c1a2..d51120225b22 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/util/DateUtils.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/util/DateUtils.java
@@ -283,6 +283,18 @@ public static Date getDate(int year, int month, int dayOfMonth, int hourOfDay, i
return new DateTime(year, month, dayOfMonth, hourOfDay, minuteOfHour).toDate();
}
+ /**
+ * Creates a {@link Date} representing the given year, month and day.
+ *
+ * @param year the year.
+ * @param month the month, from 1.
+ * @param dayOfMonth the day of the month, from 1.
+ * @return a {@link Date}.
+ */
+ public static Date getDate(int year, int month, int dayOfMonth) {
+ return new DateTime(year, month, dayOfMonth, 0, 0).toDate();
+ }
+
/**
* Formats a Date according to the HTTP specification standard date format.
*
@@ -798,19 +810,29 @@ private static R convertOrNull(T from, Function converter) {
* UTC time zone.
*
* @param time the LocalDateTime.
- * @return a Date.
+ * @return a {@link Date}.
*/
public static Date getDate(LocalDateTime time) {
Instant instant = time.toInstant(ZoneOffset.UTC);
-
return Date.from(instant);
}
+ /**
+ * Truncates the given date to the first day of the month.
+ *
+ * @param date the date to truncate.
+ * @return a {@link Date}.
+ */
+ public static Date dateTruncMonth(Date date) {
+ LocalDate localDate = new LocalDate(date);
+ return localDate.withDayOfMonth(1).toDate();
+ }
+
/**
* Return the current date minus the duration specified by the given string.
*
* @param duration the duration string.
- * @return a Date.
+ * @return a {@link Date}.
*/
public static Date nowMinusDuration(String duration) {
Duration dr = DateUtils.getDuration(duration);
diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/util/Timer.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/util/Timer.java
index 906dc4ce32dc..1d1039943357 100644
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/util/Timer.java
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/util/Timer.java
@@ -28,6 +28,7 @@
package org.hisp.dhis.util;
import lombok.extern.slf4j.Slf4j;
+import org.slf4j.helpers.MessageFormatter;
@Slf4j
public class Timer {
@@ -49,10 +50,10 @@ public long getSplitTime() {
return getSplitTime("Split");
}
- public long getSplitTime(String msg) {
- long endTime = System.nanoTime();
-
- long time = (endTime - startTime) / 1000;
+ public long getSplitTime(String pattern, Object... args) {
+ final long endTime = System.nanoTime();
+ final long time = (endTime - startTime) / 1000;
+ final String msg = MessageFormatter.arrayFormat(pattern, args).getMessage();
if (!printDisabled) {
log.info("Time: " + time + " micros: " + msg);
@@ -68,16 +69,13 @@ public long getTimeInMs() {
}
public long getTimeInS() {
- long endTime = System.nanoTime();
- long time = (endTime - startTime) / 1000000000;
- return time;
+ final long endTime = System.nanoTime();
+ return (endTime - startTime) / 1000000000;
}
public long getTime(String msg) {
- long time = getSplitTime(msg);
-
+ final long time = getSplitTime(msg);
start();
-
return time;
}
diff --git a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/common/HashUtilsTest.java b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/common/HashUtilsTest.java
index f9e8778914da..b1be79ba9ceb 100644
--- a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/common/HashUtilsTest.java
+++ b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/common/HashUtilsTest.java
@@ -37,6 +37,21 @@
* @author Morten Svanæs
*/
class HashUtilsTest {
+
+ @Test
+ void testMd5Hex() {
+ String value = "10-05-2022T12:55:45";
+ assertEquals(32, HashUtils.hashMD5(value.getBytes()).length());
+ assertEquals("c149820871470e3ab15eb24d42b3561a", HashUtils.hashMD5(value.getBytes()));
+ }
+
+ @Test
+ void testSha1Hex() {
+ String value = "/api/me";
+ assertEquals(40, HashUtils.hashSHA1(value.getBytes()).length());
+ assertEquals("4f8cc3f306852ecb642ba4375453be1a4b860e71", HashUtils.hashSHA1(value.getBytes()));
+ }
+
@Test
void testIsValidSHA256HexFormat() {
String validSHA256Hex = "635e253fddd8466788d9580983bda99c258e9dd8c5a60a032623fde6c3a2789d";
diff --git a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/common/IdSchemesTest.java b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/common/IdSchemesTest.java
index b26c5064a6d1..f2061ffa3c2e 100644
--- a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/common/IdSchemesTest.java
+++ b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/common/IdSchemesTest.java
@@ -58,7 +58,8 @@ void testSerializeIdSchemes() throws JsonProcessingException {
IdSchemes original = new IdSchemes();
original.setProgramIdScheme("CODE");
// language=JSON
- String expected = """
+ String expected =
+ """
{"programIdScheme":{"type":"CODE"}}""";
assertEquals(expected, new ObjectMapper().writeValueAsString(original));
}
diff --git a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/program/ProgramTrackedEntityAttributeTest.java b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/program/ProgramTrackedEntityAttributeTest.java
index 7ad34ae9e498..52409c166a17 100644
--- a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/program/ProgramTrackedEntityAttributeTest.java
+++ b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/program/ProgramTrackedEntityAttributeTest.java
@@ -29,6 +29,7 @@
import static org.hisp.dhis.program.ProgramTest.getNewProgram;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
@@ -62,6 +63,27 @@ void testCopyOf() {
assertEquals("Copy of Program Name tracked entity attr 1", copy.getName());
}
+ @Test
+ void testDefaultSkipAnalyticsValue() {
+ ProgramTrackedEntityAttribute ptea = new ProgramTrackedEntityAttribute();
+ assertFalse(
+ ptea.isSkipIndividualAnalytics(),
+ "Default value of skipIndividualAnalytics should be false");
+ }
+
+ @Test
+ void testCopyOfWithSkipAnalytics() {
+ ProgramTrackedEntityAttribute original = getNewProgramAttribute(getNewProgram());
+ original.setSkipIndividualAnalytics(true); // Set to true to check copy behavior
+ ProgramTrackedEntityAttribute copy =
+ ProgramTrackedEntityAttribute.copyOf.apply(original, getNewProgram());
+
+ assertEquals(
+ original.isSkipIndividualAnalytics(),
+ copy.isSkipIndividualAnalytics(),
+ "skipIndividualAnalytics should be copied correctly");
+ }
+
private ProgramTrackedEntityAttribute getNewProgramAttribute(Program program) {
ProgramTrackedEntityAttribute ptea = new ProgramTrackedEntityAttribute();
TrackedEntityAttribute tea = new TrackedEntityAttribute();
diff --git a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/relationship/RelationshipKeyTest.java b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/relationship/RelationshipKeyTest.java
index 69384538d1bf..89186847499f 100644
--- a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/relationship/RelationshipKeyTest.java
+++ b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/relationship/RelationshipKeyTest.java
@@ -29,6 +29,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.hisp.dhis.common.UID;
import org.junit.jupiter.api.Test;
class RelationshipKeyTest {
@@ -39,8 +40,12 @@ void asStringForRelationshipTeiToTei() {
RelationshipKey key =
RelationshipKey.of(
"dDrh5UyCyvQ",
- RelationshipKey.RelationshipItemKey.builder().trackedEntity("Ea0rRdBPAIp").build(),
- RelationshipKey.RelationshipItemKey.builder().trackedEntity("G1afLIEKt8A").build());
+ RelationshipKey.RelationshipItemKey.builder()
+ .trackedEntity(UID.of("Ea0rRdBPAIp"))
+ .build(),
+ RelationshipKey.RelationshipItemKey.builder()
+ .trackedEntity(UID.of("G1afLIEKt8A"))
+ .build());
assertEquals("dDrh5UyCyvQ_Ea0rRdBPAIp_G1afLIEKt8A", key.asString());
}
@@ -51,8 +56,12 @@ void asStringForRelationshipTeiToEnrollment() {
RelationshipKey key =
RelationshipKey.of(
"dDrh5UyCyvQ",
- RelationshipKey.RelationshipItemKey.builder().trackedEntity("Ea0rRdBPAIp").build(),
- RelationshipKey.RelationshipItemKey.builder().enrollment("G1afLIEKt8A").build());
+ RelationshipKey.RelationshipItemKey.builder()
+ .trackedEntity(UID.of("Ea0rRdBPAIp"))
+ .build(),
+ RelationshipKey.RelationshipItemKey.builder()
+ .enrollment(UID.of("G1afLIEKt8A"))
+ .build());
assertEquals("dDrh5UyCyvQ_Ea0rRdBPAIp_G1afLIEKt8A", key.asString());
}
@@ -63,8 +72,10 @@ void asStringForRelationshipTeiToEvent() {
RelationshipKey key =
RelationshipKey.of(
"dDrh5UyCyvQ",
- RelationshipKey.RelationshipItemKey.builder().trackedEntity("Ea0rRdBPAIp").build(),
- RelationshipKey.RelationshipItemKey.builder().event("G1afLIEKt8A").build());
+ RelationshipKey.RelationshipItemKey.builder()
+ .trackedEntity(UID.of("Ea0rRdBPAIp"))
+ .build(),
+ RelationshipKey.RelationshipItemKey.builder().event(UID.of("G1afLIEKt8A")).build());
assertEquals("dDrh5UyCyvQ_Ea0rRdBPAIp_G1afLIEKt8A", key.asString());
}
diff --git a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/util/DateUtilsTest.java b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/util/DateUtilsTest.java
index 8c441747ba0f..0c7b97ba9709 100644
--- a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/util/DateUtilsTest.java
+++ b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/util/DateUtilsTest.java
@@ -35,6 +35,7 @@
import static org.hamcrest.Matchers.lessThan;
import static org.hisp.dhis.util.DateUtils.dateIsValid;
import static org.hisp.dhis.util.DateUtils.dateTimeIsValid;
+import static org.hisp.dhis.util.DateUtils.getDate;
import static org.hisp.dhis.util.DateUtils.minusOneDay;
import static org.hisp.dhis.util.DateUtils.parseDate;
import static org.hisp.dhis.util.DateUtils.plusOneDay;
@@ -456,6 +457,15 @@ void testSafeParseDateInvalidDateFormats() {
assertNull(date);
}
+ @Test
+ void testDateTruncMonth() {
+ Date dateA = getDate(2024, 3, 9);
+ Date dateB = getDate(2024, 10, 20);
+
+ assertEquals(getDate(2024, 3, 1), DateUtils.dateTruncMonth(dateA));
+ assertEquals(getDate(2024, 10, 1), DateUtils.dateTruncMonth(dateB));
+ }
+
@Test
void testPlusOneDay() {
Date aDay = toMediumDate("2021-01-01");
diff --git a/dhis-2/dhis-services/dhis-service-administration/pom.xml b/dhis-2/dhis-services/dhis-service-administration/pom.xml
index d9ba0eeca877..b956e76a0f20 100644
--- a/dhis-2/dhis-services/dhis-service-administration/pom.xml
+++ b/dhis-2/dhis-services/dhis-service-administration/pom.xml
@@ -131,7 +131,7 @@
com.networknt
json-schema-validator
- 1.5.2
+ 1.5.3
org.apache.commons
diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/maintenance/DefaultMaintenanceService.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/maintenance/DefaultMaintenanceService.java
index 6f7df34de1d6..656854538283 100644
--- a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/maintenance/DefaultMaintenanceService.java
+++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/maintenance/DefaultMaintenanceService.java
@@ -78,7 +78,7 @@ public class DefaultMaintenanceService implements MaintenanceService {
private final ApplicationEventPublisher eventPublisher;
- private final EventChangeLogService trackedEntityDataValueChangelogService;
+ private final EventChangeLogService eventChangeLogService;
// -------------------------------------------------------------------------
// MaintenanceService implementation
@@ -169,7 +169,8 @@ public boolean pruneData(DataElement dataElement) {
return false;
}
- trackedEntityDataValueChangelogService.deleteTrackedEntityDataValueChangeLog(dataElement);
+ eventChangeLogService.deleteTrackedEntityDataValueChangeLog(dataElement);
+ eventChangeLogService.deleteEventChangeLog(dataElement);
dataValueAuditService.deleteDataValueAudits(dataElement);
dataValueService.deleteDataValues(dataElement);
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 c778900de40a..7809f5bd4c64 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
@@ -122,6 +122,7 @@ public int deleteSoftDeletedEvents() {
// delete other objects related to events
"delete from relationshipitem where eventid in " + eventSelect,
"delete from trackedentitydatavalueaudit where eventid in " + eventSelect,
+ "delete from eventchangelog where eventid in " + eventSelect,
"delete from programmessage where eventid in " + eventSelect,
// finally delete the events
"delete from event where deleted is true"
@@ -202,6 +203,7 @@ public int deleteSoftDeletedEnrollments() {
// delete other entries linked to events
"delete from relationshipitem where eventid in " + eventSelect,
"delete from trackedentitydatavalueaudit where eventid in " + eventSelect,
+ "delete from eventchangelog where eventid in " + eventSelect,
"delete from programmessage where eventid in " + eventSelect,
// delete other entries linked to enrollments
"delete from relationshipitem where enrollmentid in " + enrollmentSelect,
@@ -288,6 +290,7 @@ public int deleteSoftDeletedTrackedEntities() {
"delete from note where noteid not in (select noteid from event_notes union all select noteid from enrollment_notes)",
// delete other objects related to obsolete events
"delete from trackedentitydatavalueaudit where eventid in " + eventSelect,
+ "delete from eventchangelog where eventid in " + eventSelect,
// delete other objects related to obsolete enrollments
"delete from programmessage where enrollmentid in " + enrollmentSelect,
"delete from event where enrollmentid in " + enrollmentSelect,
diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/CommonMergeHandler.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/CommonMergeHandler.java
index 604e4f8d502c..314f6eb58fb1 100644
--- a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/CommonMergeHandler.java
+++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/CommonMergeHandler.java
@@ -36,7 +36,7 @@
import org.hisp.dhis.dataentryform.DataEntryFormService;
import org.hisp.dhis.indicator.Indicator;
import org.hisp.dhis.indicator.IndicatorService;
-import org.springframework.stereotype.Service;
+import org.springframework.stereotype.Component;
/**
* Common Merge handler for metadata entities. The merge operations here are shared by many merge
@@ -45,7 +45,7 @@
*
* @author david mackessy
*/
-@Service
+@Component
@RequiredArgsConstructor
public class CommonMergeHandler {
diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/DefaultMergeValidator.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/DefaultMergeValidator.java
index ea0d6ffde1e2..f170b39348e2 100644
--- a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/DefaultMergeValidator.java
+++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/DefaultMergeValidator.java
@@ -27,10 +27,13 @@
*/
package org.hisp.dhis.merge;
+import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
+import javax.annotation.Nonnull;
import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
import org.hisp.dhis.common.IdentifiableObject;
import org.hisp.dhis.common.IdentifiableObjectManager;
import org.hisp.dhis.common.UID;
@@ -48,45 +51,57 @@
*
* @author david mackessy
*/
+@Slf4j
@Component
@RequiredArgsConstructor
public class DefaultMergeValidator implements MergeValidator {
private final IdentifiableObjectManager manager;
- private static final String INDICATOR_TYPE = "IndicatorType";
- private static final String INDICATOR = "Indicator";
- private static final String DATA_ELEMENT = "DataElement";
- private static final String MERGE_ERROR = "Unexpected value retrieving merge error code: ";
@Override
- public void verifySources(
- Set paramSources, Set verifiedSources, MergeReport mergeReport, Class clazz) {
+ public MergeRequest validateUIDs(
+ @Nonnull MergeParams params, @Nonnull MergeReport mergeReport, @Nonnull MergeType mergeType) {
+ log.info("Validating {} merge request", mergeType);
+ mergeReport.setMergeType(mergeType);
+
+ Set verifiedSources = verifySources(params.getSources(), mergeReport, mergeType);
+
+ checkIsTargetInSources(verifiedSources, params.getTarget(), mergeReport, mergeType);
+
+ return verifyTarget(mergeReport, verifiedSources, params, mergeType);
+ }
+
+ @Override
+ public Set verifySources(
+ Set paramSources, MergeReport mergeReport, MergeType mergeType) {
+ Set verifiedSources = new HashSet<>();
Optional.ofNullable(paramSources)
.filter(CollectionUtils::isNotEmpty)
.ifPresentOrElse(
- ids -> getSourcesAndVerify(ids, mergeReport, verifiedSources, clazz),
+ ids -> getSourcesAndVerify(ids, mergeReport, verifiedSources, mergeType),
() ->
mergeReport.addErrorMessage(
- new ErrorMessage(missingSourceErrorCode(clazz.getSimpleName()))));
+ new ErrorMessage(ErrorCode.E1530, mergeType.getName())));
+ return verifiedSources;
}
@Override
- public void checkIsTargetInSources(
- Set sources, UID target, MergeReport mergeReport, Class clazz) {
+ public void checkIsTargetInSources(
+ Set sources, UID target, MergeReport mergeReport, MergeType mergeType) {
if (sources.contains(target))
mergeReport.addErrorMessage(
- new ErrorMessage(targetNotSourceErrorCode(clazz.getSimpleName())));
+ new ErrorMessage(ErrorCode.E1532, mergeType.getName(), mergeType.getName()));
}
@Override
- public MergeRequest verifyTarget(
- MergeReport mergeReport, Set sources, MergeParams params, Class clazz) {
- return getAndVerify(params.getTarget(), mergeReport, MergeObjectType.TARGET.name(), clazz)
+ public MergeRequest verifyTarget(
+ MergeReport mergeReport, Set sources, MergeParams params, MergeType mergeType) {
+ return getAndVerify(params.getTarget(), mergeReport, MergeObjectType.TARGET, mergeType)
.map(
- t ->
+ uid ->
MergeRequest.builder()
.sources(sources)
- .target(t)
+ .target(uid)
.deleteSources(params.isDeleteSources())
.dataMergeStrategy(params.getDataMergeStrategy())
.build())
@@ -103,19 +118,18 @@ public MergeRequest verifyTarget(
* @param uid to verify
* @param mergeReport to update
* @param mergeObjectType indicating whether this is a source or target
- * @param clazz {@link IdentifiableObject} type
+ * @param mergeType {@link MergeType}
* @return optional UID
*/
- private Optional getAndVerify(
- UID uid, MergeReport mergeReport, String mergeObjectType, Class clazz) {
+ private Optional getAndVerify(
+ UID uid, MergeReport mergeReport, MergeObjectType mergeObjectType, MergeType mergeType) {
if (uid != null) {
- return Optional.ofNullable(manager.get(clazz, uid.getValue()))
+ return Optional.ofNullable(manager.get(mergeType.getClazz(), uid.getValue()))
.map(i -> UID.of(i.getUid()))
- .or(reportError(uid, mergeReport, mergeObjectType, clazz));
+ .or(reportError(uid, mergeReport, mergeObjectType, mergeType));
} else {
- mergeReport.addErrorMessage(
- new ErrorMessage(noTargetErrorCode(clazz.getSimpleName()), mergeObjectType, uid));
+ mergeReport.addErrorMessage(new ErrorMessage(ErrorCode.E1531, mergeType.getName()));
return Optional.empty();
}
}
@@ -128,67 +142,25 @@ private Optional getAndVerify(
* @param uids to verify
* @param report to update with any error
* @param verifiedSources to update with verified uids
- * @param clazz {@link IdentifiableObject} type
+ * @param mergeType {@link MergeType}
*/
- private void getSourcesAndVerify(
- Set uids, MergeReport report, Set verifiedSources, Class clazz) {
+ private void getSourcesAndVerify(
+ Set uids, MergeReport report, Set verifiedSources, MergeType mergeType) {
uids.forEach(
uid ->
- getAndVerify(uid, report, MergeObjectType.SOURCE.name(), clazz)
+ getAndVerify(uid, report, MergeObjectType.SOURCE, mergeType)
.ifPresent(verifiedSources::add));
}
- private Supplier> reportError(
- UID uid, MergeReport mergeReport, String mergeObjectType, Class clazz) {
+ private Supplier> reportError(
+ UID uid, MergeReport mergeReport, MergeObjectType mergeObjectType, MergeType mergeType) {
return () -> {
mergeReport.addErrorMessage(
- new ErrorMessage(doesNotExistErrorCode(clazz.getSimpleName()), mergeObjectType, uid));
+ new ErrorMessage(ErrorCode.E1533, mergeObjectType.toString(), mergeType.getName(), uid));
return Optional.empty();
};
}
- /**
- * Methods to get the appropriate error code based on the Object type
- *
- * @param clazz class name
- * @return error code
- */
- private ErrorCode missingSourceErrorCode(String clazz) {
- return switch (clazz) {
- case INDICATOR_TYPE -> ErrorCode.E1530;
- case INDICATOR -> ErrorCode.E1540;
- case DATA_ELEMENT -> ErrorCode.E1550;
- default -> throw new IllegalStateException(MERGE_ERROR + clazz);
- };
- }
-
- private ErrorCode noTargetErrorCode(String clazz) {
- return switch (clazz) {
- case INDICATOR_TYPE -> ErrorCode.E1531;
- case INDICATOR -> ErrorCode.E1541;
- case DATA_ELEMENT -> ErrorCode.E1551;
- default -> throw new IllegalStateException(MERGE_ERROR + clazz);
- };
- }
-
- private ErrorCode targetNotSourceErrorCode(String clazz) {
- return switch (clazz) {
- case INDICATOR_TYPE -> ErrorCode.E1532;
- case INDICATOR -> ErrorCode.E1542;
- case DATA_ELEMENT -> ErrorCode.E1552;
- default -> throw new IllegalStateException(MERGE_ERROR + clazz);
- };
- }
-
- private ErrorCode doesNotExistErrorCode(String clazz) {
- return switch (clazz) {
- case INDICATOR_TYPE -> ErrorCode.E1533;
- case INDICATOR -> ErrorCode.E1543;
- case DATA_ELEMENT -> ErrorCode.E1553;
- default -> throw new IllegalStateException(MERGE_ERROR + clazz);
- };
- }
-
enum MergeObjectType {
SOURCE,
TARGET
diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/option/CategoryOptionMergeHandler.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/option/CategoryOptionMergeHandler.java
new file mode 100644
index 000000000000..f62280f70d18
--- /dev/null
+++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/option/CategoryOptionMergeHandler.java
@@ -0,0 +1,142 @@
+/*
+ * 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.option;
+
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.hisp.dhis.analytics.CategoryDimensionStore;
+import org.hisp.dhis.category.Category;
+import org.hisp.dhis.category.CategoryDimension;
+import org.hisp.dhis.category.CategoryOption;
+import org.hisp.dhis.category.CategoryOptionCombo;
+import org.hisp.dhis.category.CategoryOptionComboStore;
+import org.hisp.dhis.category.CategoryOptionGroup;
+import org.hisp.dhis.category.CategoryOptionGroupStore;
+import org.hisp.dhis.category.CategoryStore;
+import org.hisp.dhis.common.UID;
+import org.hisp.dhis.organisationunit.OrganisationUnit;
+import org.hisp.dhis.organisationunit.OrganisationUnitStore;
+import org.springframework.stereotype.Component;
+
+/**
+ * Merge handler for metadata entities.
+ *
+ * @author david mackessy
+ */
+@Component
+@RequiredArgsConstructor
+public class CategoryOptionMergeHandler {
+
+ private final CategoryStore categoryStore;
+ private final CategoryOptionComboStore categoryOptionComboStore;
+ private final CategoryOptionGroupStore categoryOptionGroupStore;
+ private final OrganisationUnitStore organisationUnitStore;
+ private final CategoryDimensionStore categoryDimensionStore;
+
+ /**
+ * Remove sources from {@link Category} and add target to {@link Category}
+ *
+ * @param sources to be removed
+ * @param target to add
+ */
+ public void handleCategories(List sources, CategoryOption target) {
+ List sourceCategories =
+ categoryStore.getCategoriesByCategoryOption(UID.toUidValueSet(sources));
+ sourceCategories.forEach(
+ c -> {
+ c.addCategoryOption(target);
+ c.removeCategoryOptions(sources);
+ });
+ }
+
+ /**
+ * Remove sources from {@link CategoryOptionCombo} and add target to {@link CategoryOptionCombo}
+ *
+ * @param sources to be removed
+ * @param target to add
+ */
+ public void handleCategoryOptionCombos(List sources, CategoryOption target) {
+ List sourceCocs =
+ categoryOptionComboStore.getCategoryOptionCombosByCategoryOption(
+ UID.toUidValueSet(sources));
+
+ sources.forEach(s -> s.removeCategoryOptionCombos(sourceCocs));
+ target.addCategoryOptionCombos(sourceCocs);
+ }
+
+ /**
+ * Remove sources from {@link OrganisationUnit} and add target to {@link OrganisationUnit}
+ *
+ * @param sources to be removed
+ * @param target to add
+ */
+ public void handleOrganisationUnits(List sources, CategoryOption target) {
+ List sourceOus =
+ organisationUnitStore.getByCategoryOption(UID.toUidValueSet(sources));
+
+ sourceOus.forEach(
+ ou -> {
+ ou.addCategoryOption(target);
+ ou.removeCategoryOptions(sources);
+ });
+ }
+
+ /**
+ * Remove sources from {@link CategoryOptionGroup} and add target to {@link CategoryOptionGroup}
+ *
+ * @param sources to be removed
+ * @param target to add
+ */
+ public void handleCategoryOptionGroups(List sources, CategoryOption target) {
+ List sourceCogs =
+ categoryOptionGroupStore.getByCategoryOption(UID.toUidValueSet(sources));
+
+ sourceCogs.forEach(
+ cog -> {
+ cog.addCategoryOption(target);
+ cog.removeCategoryOptions(sources);
+ });
+ }
+
+ /**
+ * Remove sources from {@link CategoryDimension} and add target to {@link CategoryDimension}
+ *
+ * @param sources to be removed
+ * @param target to add
+ */
+ public void handleCategoryDimensions(List sources, CategoryOption target) {
+ List sourceCds =
+ categoryDimensionStore.getByCategoryOption(UID.toUidValueSet(sources));
+
+ sourceCds.forEach(
+ cd -> {
+ cd.addItem(target);
+ cd.removeItems(sources);
+ });
+ }
+}
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
new file mode 100644
index 000000000000..3e2ccf59c655
--- /dev/null
+++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/category/option/CategoryOptionMergeService.java
@@ -0,0 +1,120 @@
+/*
+ * 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.option;
+
+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.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;
+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 CategoryOption} merge.
+ *
+ * @author david mackessy
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class CategoryOptionMergeService implements MergeService {
+
+ private final CategoryService categoryService;
+ private final CategoryOptionMergeHandler categoryOptionMergeHandler;
+ private final MergeValidator validator;
+ private final EntityManager entityManager;
+ private List metadataMergeHandlers;
+
+ @Override
+ public MergeRequest validate(@Nonnull MergeParams params, @Nonnull MergeReport mergeReport) {
+ return validator.validateUIDs(params, mergeReport, MergeType.CATEGORY_OPTION);
+ }
+
+ @Override
+ public MergeReport merge(@Nonnull MergeRequest request, @Nonnull MergeReport mergeReport) {
+ log.info("Performing CategoryOption merge");
+
+ List sources =
+ categoryService.getCategoryOptionsByUid(UID.toValueList(request.getSources()));
+ CategoryOption target = categoryService.getCategoryOption(request.getTarget().getValue());
+
+ // merge metadata
+ log.info("Handling CategoryOption reference associations and merges");
+ metadataMergeHandlers.forEach(h -> h.merge(sources, target));
+
+ // 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 CategoryOptions");
+ for (CategoryOption source : sources) {
+ mergeReport.addDeletedSource(source.getUid());
+ categoryService.deleteCategoryOption(source);
+ }
+ }
+
+ @PostConstruct
+ private void initMergeHandlers() {
+ metadataMergeHandlers =
+ List.of(
+ categoryOptionMergeHandler::handleCategories,
+ categoryOptionMergeHandler::handleCategoryOptionCombos,
+ categoryOptionMergeHandler::handleOrganisationUnits,
+ categoryOptionMergeHandler::handleCategoryOptionGroups,
+ categoryOptionMergeHandler::handleCategoryDimensions);
+ }
+
+ /**
+ * Functional interface representing a {@link DataElement} data merge operation.
+ *
+ * @author david mackessy
+ */
+ @FunctionalInterface
+ public interface MetadataMergeHandler {
+ void merge(@Nonnull List sources, @Nonnull CategoryOption target);
+ }
+}
diff --git a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/dataelement/DataElementMergeService.java b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/dataelement/DataElementMergeService.java
index 9ab1fd51826f..1e6b29f801d9 100644
--- a/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/dataelement/DataElementMergeService.java
+++ b/dhis-2/dhis-services/dhis-service-administration/src/main/java/org/hisp/dhis/merge/dataelement/DataElementMergeService.java
@@ -29,9 +29,7 @@
import com.google.common.collect.ImmutableList;
import jakarta.persistence.EntityManager;
-import java.util.HashSet;
import java.util.List;
-import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
@@ -47,13 +45,13 @@
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.hisp.dhis.merge.dataelement.handler.AnalyticalDataElementMergeHandler;
import org.hisp.dhis.merge.dataelement.handler.DataDataElementMergeHandler;
import org.hisp.dhis.merge.dataelement.handler.MetadataDataElementMergeHandler;
import org.hisp.dhis.merge.dataelement.handler.TrackerDataElementMergeHandler;
import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
/**
* Main class for a {@link org.hisp.dhis.dataelement.DataElement} merge.
@@ -81,23 +79,15 @@ public class DataElementMergeService implements MergeService {
@Override
public MergeRequest validate(@Nonnull MergeParams params, @Nonnull MergeReport mergeReport) {
- log.info("Validating DataElement merge request");
+ MergeRequest request = validator.validateUIDs(params, mergeReport, MergeType.DATA_ELEMENT);
+ if (mergeReport.hasErrorMessages()) return request;
+
+ // data element-specific validation
if (params.getDataMergeStrategy() == null) {
- mergeReport.addErrorMessage(new ErrorMessage(ErrorCode.E1556));
- return null;
+ mergeReport.addErrorMessage(new ErrorMessage(ErrorCode.E1534));
+ return request;
}
- // sources
- Set sources = new HashSet<>();
- validator.verifySources(params.getSources(), sources, mergeReport, DataElement.class);
-
- // target
- validator.checkIsTargetInSources(sources, params.getTarget(), mergeReport, DataElement.class);
- MergeRequest request = validator.verifyTarget(mergeReport, sources, params, DataElement.class);
-
- if (mergeReport.hasErrorMessages()) return request;
-
- // get DEs for further type-specific validation
DataElement deTarget = dataElementService.getDataElement(request.getTarget().getValue());
List deSources =
dataElementService.getDataElementsByUid(
@@ -113,7 +103,6 @@ public MergeRequest validate(@Nonnull MergeParams params, @Nonnull MergeReport m
}
@Override
- @Transactional
public MergeReport merge(@Nonnull MergeRequest request, @Nonnull MergeReport mergeReport) {
log.info("Performing DataElement merge");
@@ -197,7 +186,7 @@ private void initMergeHandlers() {
* @author david mackessy
*/
@FunctionalInterface
- public static interface DataElementAuditMergeHandler {
+ public interface DataElementAuditMergeHandler {
void merge(@Nonnull List sources, @Nonnull MergeRequest mergeRequest);
}
@@ -207,7 +196,7 @@ public static interface DataElementAuditMergeHandler {
* @author david mackessy
*/
@FunctionalInterface
- public static interface DataElementDataMergeHandler {
+ public interface DataElementDataMergeHandler {
void merge(
@Nonnull List sources,
@Nonnull DataElement target,
@@ -220,7 +209,7 @@ void merge(
* @author david mackessy
*/
@FunctionalInterface
- public static interface DataElementMergeHandler {
+ public interface DataElementMergeHandler {
void merge(List