diff --git a/.github/workflows/run-api-tests.yml b/.github/workflows/run-api-tests.yml index 0e58ab299626..c244585a27f2 100644 --- a/.github/workflows/run-api-tests.yml +++ b/.github/workflows/run-api-tests.yml @@ -41,13 +41,13 @@ jobs: # build and publish multi-arch images using Jib. Image is used for api tests in # this workflow and can be pulled from Dockerhub by devs to run locally, ... mvn clean verify --threads 2C --batch-mode --no-transfer-progress \ - -DskipTests --update-snapshots --file dhis-2/pom.xml \ + -DskipTests -Dmdep.analyze.skip --update-snapshots --file dhis-2/pom.xml \ --projects dhis-web-server --also-make --activate-profiles jibBuild \ -Djib.to.image=$CORE_IMAGE_NAME -Djib.container.labels=DHIS2_BUILD_REVISION=${{github.event.pull_request.head.sha}},DHIS2_BUILD_BRANCH=${{github.head_ref}} else # only build image for running api tests in this workflow i.e. master, 2.39, ... mvn clean verify --threads 2C --batch-mode --no-transfer-progress \ - -DskipTests --update-snapshots --file dhis-2/pom.xml \ + -DskipTests -Dmdep.analyze.skip --update-snapshots --file dhis-2/pom.xml \ --projects dhis-web-server --also-make --activate-profiles jibDockerBuild \ -Djib.to.image=$CORE_IMAGE_NAME fi diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 105cf42facc2..4226b4bb46e0 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -24,6 +24,9 @@ jobs: - name: Run unit tests run: mvn clean test --threads 2C --batch-mode --no-transfer-progress --update-snapshots --file ./dhis-2/pom.xml --activate-profiles unit-test timeout-minutes: 30 + - name: Run dependency analysis + run: mvn dependency:analyze --file ./dhis-2/pom.xml + timeout-minutes: 2 - name: Report coverage to codecov uses: codecov/codecov-action@v3 with: diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryCombo.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryCombo.java index b8b804117d5a..11246bd5533d 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryCombo.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/category/CategoryCombo.java @@ -27,6 +27,7 @@ */ package org.hisp.dhis.category; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; @@ -195,6 +196,11 @@ public boolean hasOptionCombos() { return optionCombos != null && !optionCombos.isEmpty(); } + @JsonIgnore + public List getDataDimensionCategories() { + return categories.stream().filter(Category::isDataDimension).toList(); + } + // ------------------------------------------------------------------------- // Logic // ------------------------------------------------------------------------- diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionalObjectUtils.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionalObjectUtils.java index f0d2ceae6cc3..a7786ad46813 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionalObjectUtils.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/DimensionalObjectUtils.java @@ -141,7 +141,6 @@ public static DimensionalObject linkAssociations( EventAnalyticalObject eventAnalyticalObject, DimensionalObject dimensionalObject, Attribute parent) { - // Associating event repetitions. List repetitions = eventAnalyticalObject.getEventRepetitions(); if (isNotEmpty(repetitions)) { 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 f1f4a0f3afdf..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 @@ -182,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 ccd0dd1ba624..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 @@ -469,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/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-api/src/main/java/org/hisp/dhis/dashboard/embedded/EmbeddedDashboard.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/embedded/EmbeddedDashboard.java index d580cb9e46ac..c6f08a84c83a 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/embedded/EmbeddedDashboard.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/dashboard/embedded/EmbeddedDashboard.java @@ -34,7 +34,11 @@ import lombok.NoArgsConstructor; import lombok.Setter; -/** Encapsulates metadata for an embedded and externally provided dashboard. */ +/** + * Encapsulates metadata for an embedded and externally provided dashboard. + * + * @author Lars Helge Overland + */ @Getter @Setter @NoArgsConstructor 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 index fb1f02016c2f..90d5edb6da18 100644 --- 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 @@ -34,6 +34,11 @@ import lombok.NoArgsConstructor; import lombok.Setter; +/** + * Encapsulates embedded dashboard filter options. + * + * @author Lars Helge Overland + */ @Getter @Setter @NoArgsConstructor 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/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/OrganisationUnitService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/organisationunit/OrganisationUnitService.java index 3beb007af819..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 @@ -133,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. * 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 3b45942510b6..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; @@ -250,6 +251,14 @@ static UserDetails createUserDetails( 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/UserService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/UserService.java index 9b8b905c2d43..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 @@ -43,6 +43,7 @@ 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; @@ -235,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. * @@ -408,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. 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 95faf611f108..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 @@ -65,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. * diff --git a/dhis-2/dhis-services/dhis-service-administration/pom.xml b/dhis-2/dhis-services/dhis-service-administration/pom.xml index b956e76a0f20..f2100f1a647e 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.3 + 1.5.4 org.apache.commons diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/config/ServiceConfig.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/config/ServiceConfig.java index e8e5ff92ee3c..d7883952b6f9 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/config/ServiceConfig.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/config/ServiceConfig.java @@ -27,18 +27,31 @@ */ package org.hisp.dhis.analytics.config; +import org.hisp.dhis.analytics.AnalyticsTableHookService; import org.hisp.dhis.analytics.AnalyticsTableManager; import org.hisp.dhis.analytics.AnalyticsTableService; +import org.hisp.dhis.analytics.partition.PartitionManager; import org.hisp.dhis.analytics.table.DefaultAnalyticsTableService; +import org.hisp.dhis.analytics.table.JdbcTrackedEntityEventsAnalyticsTableManager; +import org.hisp.dhis.analytics.table.setting.AnalyticsTableSettings; +import org.hisp.dhis.category.CategoryService; +import org.hisp.dhis.common.IdentifiableObjectManager; +import org.hisp.dhis.dataapproval.DataApprovalLevelService; import org.hisp.dhis.dataelement.DataElementService; +import org.hisp.dhis.db.sql.AnalyticsSqlBuilder; +import org.hisp.dhis.db.sql.AnalyticsSqlBuilderProvider; import org.hisp.dhis.db.sql.SqlBuilder; import org.hisp.dhis.db.sql.SqlBuilderProvider; import org.hisp.dhis.organisationunit.OrganisationUnitService; +import org.hisp.dhis.period.PeriodDataProvider; import org.hisp.dhis.resourcetable.ResourceTableService; import org.hisp.dhis.setting.SystemSettingsProvider; +import org.hisp.dhis.system.database.DatabaseInfoProvider; +import org.hisp.dhis.trackedentity.TrackedEntityTypeService; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; /** * @author Luciano Fiandesio @@ -50,6 +63,46 @@ public SqlBuilder sqlBuilder(SqlBuilderProvider provider) { return provider.getSqlBuilder(); } + @Bean + public AnalyticsSqlBuilder analyticsSqlBuilder(AnalyticsSqlBuilderProvider provider) { + return provider.getAnalyticsSqlBuilder(); + } + + @Bean("org.hisp.dhis.analytics.TrackedEntityEventsAnalyticsTableManager") + public AnalyticsTableManager jdbcTrackedEntityEventsAnalyticsTableManager( + IdentifiableObjectManager idObjectManager, + OrganisationUnitService organisationUnitService, + CategoryService categoryService, + SystemSettingsProvider settingsProvider, + DataApprovalLevelService dataApprovalLevelService, + ResourceTableService resourceTableService, + AnalyticsTableHookService tableHookService, + PartitionManager partitionManager, + DatabaseInfoProvider databaseInfoProvider, + @Qualifier("analyticsJdbcTemplate") JdbcTemplate jdbcTemplate, + TrackedEntityTypeService trackedEntityTypeService, + AnalyticsTableSettings analyticsTableSettings, + PeriodDataProvider periodDataProvider, + SqlBuilder sqlBuilder, + AnalyticsSqlBuilder analyticsSqlBuilder) { + return new JdbcTrackedEntityEventsAnalyticsTableManager( + idObjectManager, + organisationUnitService, + categoryService, + settingsProvider, + dataApprovalLevelService, + resourceTableService, + tableHookService, + partitionManager, + databaseInfoProvider, + jdbcTemplate, + trackedEntityTypeService, + analyticsTableSettings, + periodDataProvider, + sqlBuilder, + analyticsSqlBuilder); + } + @Bean("org.hisp.dhis.analytics.TrackedEntityAnalyticsTableService") public AnalyticsTableService trackedEntityAnalyticsTableManager( @Qualifier("org.hisp.dhis.analytics.TrackedEntityAnalyticsTableManager") diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultDataQueryService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultDataQueryService.java index 9d821881af0d..6ed9ff5c5838 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultDataQueryService.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DefaultDataQueryService.java @@ -93,7 +93,7 @@ @Service("org.hisp.dhis.analytics.DataQueryService") @RequiredArgsConstructor public class DefaultDataQueryService implements DataQueryService { - private final DimensionalObjectProducer dimensionalObjectProducer; + private final DimensionalObjectProvider dimensionalObjectProducer; private final IdentifiableObjectManager idObjectManager; diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DimensionalObjectProducer.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DimensionalObjectProvider.java similarity index 99% rename from dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DimensionalObjectProducer.java rename to dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DimensionalObjectProvider.java index c776b35d49f0..c247113b657f 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DimensionalObjectProducer.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/data/DimensionalObjectProvider.java @@ -116,7 +116,7 @@ */ @Component @RequiredArgsConstructor -public class DimensionalObjectProducer { +public class DimensionalObjectProvider { private final IdentifiableObjectManager idObjectManager; private final OrganisationUnitService organisationUnitService; diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/outlier/data/OutlierQueryParser.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/outlier/data/OutlierQueryParser.java index a64dec95202b..65b350d257c9 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/outlier/data/OutlierQueryParser.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/outlier/data/OutlierQueryParser.java @@ -39,7 +39,7 @@ import java.util.UUID; import lombok.AllArgsConstructor; import org.apache.commons.lang3.StringUtils; -import org.hisp.dhis.analytics.data.DimensionalObjectProducer; +import org.hisp.dhis.analytics.data.DimensionalObjectProvider; import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.common.BaseDimensionalObject; import org.hisp.dhis.common.DisplayProperty; @@ -60,7 +60,7 @@ @AllArgsConstructor public class OutlierQueryParser { private final IdentifiableObjectManager idObjectManager; - private final DimensionalObjectProducer dimensionalObjectProducer; + private final DimensionalObjectProvider dimensionalObjectProducer; private final UserService userService; /** diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/outlier/service/AnalyticsOutlierService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/outlier/service/AnalyticsOutlierService.java index 9b7ed5e7a7d1..8dad2acf3de9 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/outlier/service/AnalyticsOutlierService.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/outlier/service/AnalyticsOutlierService.java @@ -63,7 +63,7 @@ import org.hisp.dhis.analytics.cache.OutliersCache; import org.hisp.dhis.analytics.common.ColumnHeader; import org.hisp.dhis.analytics.common.TableInfoReader; -import org.hisp.dhis.analytics.data.DimensionalObjectProducer; +import org.hisp.dhis.analytics.data.DimensionalObjectProvider; import org.hisp.dhis.analytics.outlier.data.Outlier; import org.hisp.dhis.analytics.outlier.data.OutlierRequest; import org.hisp.dhis.category.CategoryOptionCombo; @@ -104,7 +104,7 @@ public class AnalyticsOutlierService { private final IdentifiableObjectManager idObjectManager; - private final DimensionalObjectProducer dimensionalObjectProducer; + private final DimensionalObjectProvider dimensionalObjectProducer; /** * Transform the incoming request into api response (json). diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractEventJdbcTableManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractEventJdbcTableManager.java index 6cbd5950b618..f00f880375ec 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractEventJdbcTableManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractEventJdbcTableManager.java @@ -27,18 +27,13 @@ */ package org.hisp.dhis.analytics.table; -import static org.hisp.dhis.analytics.table.model.Skip.SKIP; import static org.hisp.dhis.analytics.util.AnalyticsUtils.getClosingParentheses; -import static org.hisp.dhis.analytics.util.AnalyticsUtils.getColumnType; -import static org.hisp.dhis.db.model.DataType.TEXT; import static org.hisp.dhis.system.util.MathUtils.NUMERIC_LENIENT_REGEXP; -import java.util.ArrayList; import java.util.List; import java.util.Map; import org.hisp.dhis.analytics.AnalyticsTableHookService; import org.hisp.dhis.analytics.partition.PartitionManager; -import org.hisp.dhis.analytics.table.model.AnalyticsColumnType; import org.hisp.dhis.analytics.table.model.AnalyticsTableColumn; import org.hisp.dhis.analytics.table.model.AnalyticsTablePartition; import org.hisp.dhis.analytics.table.model.Skip; @@ -48,11 +43,9 @@ import org.hisp.dhis.common.ValueType; import org.hisp.dhis.commons.util.TextUtils; import org.hisp.dhis.dataapproval.DataApprovalLevelService; -import org.hisp.dhis.db.model.DataType; import org.hisp.dhis.db.sql.SqlBuilder; import org.hisp.dhis.organisationunit.OrganisationUnitService; import org.hisp.dhis.period.PeriodDataProvider; -import org.hisp.dhis.program.Program; import org.hisp.dhis.resourcetable.ResourceTableService; import org.hisp.dhis.setting.SystemSettingsProvider; import org.hisp.dhis.system.database.DatabaseInfoProvider; @@ -96,67 +89,101 @@ public AbstractEventJdbcTableManager( public static final String OU_NAME_COL_SUFFIX = "_name"; protected final String getNumericClause() { - return " and value " + sqlBuilder.regexpMatch("'" + NUMERIC_LENIENT_REGEXP + "'"); + return " and " + sqlBuilder.regexpMatch("value", "'" + NUMERIC_LENIENT_REGEXP + "'"); } protected final String getDateClause() { - return " and value " + sqlBuilder.regexpMatch("'" + DATE_REGEXP + "'"); + return " and " + sqlBuilder.regexpMatch("value", DATE_REGEXP); } + /** + * Indicates whether creating an index should be skipped. + * + * @param valueType the {@link ValueType}. + * @param hasOptionSet whether an option set exists. + * @return a {@link Skip}. + */ protected Skip skipIndex(ValueType valueType, boolean hasOptionSet) { boolean skipIndex = NO_INDEX_VAL_TYPES.contains(valueType) && !hasOptionSet; return skipIndex ? Skip.SKIP : Skip.INCLUDE; } - protected String getSelectClause(ValueType valueType, String columnName) { - return getSelectClauseInternal(valueType, columnName, false); + /** + * Returns a select expression for a data element value, handling casting to the appropriate data + * type based on the given value type. + * + * @param valueType the {@link ValueType}. + * @param columnName the column name. + * @return a select expression. + */ + protected String getSelectExpression(ValueType valueType, String columnName) { + return getSelectExpression(valueType, columnName, false); } - protected String getSelectClauseForTea(ValueType valueType, String columnName) { - return getSelectClauseInternal(valueType, columnName, true); + /** + * Returns a select expression for a tracked entity attribute, handling casting to the appropriate + * data type based on the given value type. + * + * @param valueType the {@link ValueType}. + * @param columnName the column name. + * @return a select expression. + */ + protected String getSelectExpressionForAttribute(ValueType valueType, String columnName) { + return getSelectExpression(valueType, columnName, true); } /** - * Returns the select clause, potentially with a cast statement, based on the given value type. - * This internal method handles both Data Value and Tracked Entity Attribute (TEA) select clauses. + * Returns a select expression, potentially with a cast statement, based on the given value type. + * Handles data element and tracked entity attribute select expressions. * - * @param valueType The value type to represent as database column type - * @param columnName The name of the column to be selected - * @param isTeaContext Whether the selection is in the context of a Tracked Entity Attribute. When - * true, organization unit selections will include an additional subquery wrapper - * @return A SQL select expression appropriate for the given value type and context + * @param valueType the {@link ValueType} to represent as database column type. + * @param columnExpression the expression or name of the column to be selected. + * @param isTea whether the selection is in the context of a tracked entity attribute. When true, + * organisation unit selections will include an additional subquery wrapper. + * @return a select expression appropriate for the given value type and context. */ - private String getSelectClauseInternal( - ValueType valueType, String columnName, boolean isTeaContext) { - String doubleType = sqlBuilder.dataTypeDouble(); - + private String getSelectExpression(ValueType valueType, String columnExpression, boolean isTea) { if (valueType.isDecimal()) { - return "cast(" + columnName + " as " + doubleType + ")"; + return getCastExpression(columnExpression, NUMERIC_REGEXP, sqlBuilder.dataTypeDouble()); } else if (valueType.isInteger()) { - return "cast(" + columnName + " as bigint)"; + return getCastExpression(columnExpression, NUMERIC_REGEXP, sqlBuilder.dataTypeBigInt()); } else if (valueType.isBoolean()) { return "case when " - + columnName + + columnExpression + " = 'true' then 1 when " - + columnName + + columnExpression + " = 'false' then 0 else null end"; } else if (valueType.isDate()) { - return "cast(" + columnName + " as " + sqlBuilder.dataTypeTimestamp() + ")"; + return getCastExpression(columnExpression, DATE_REGEXP, sqlBuilder.dataTypeTimestamp()); } else if (valueType.isGeo() && isSpatialSupport()) { return "ST_GeomFromGeoJSON('{\"type\":\"Point\", \"coordinates\":' || (" - + columnName + + columnExpression + ") || ', \"crs\":{\"type\":\"name\", \"properties\":{\"name\":\"EPSG:4326\"}}}')"; } else if (valueType.isOrganisationUnit()) { String ouClause = - isTeaContext + isTea ? "ou.uid from ${organisationunit} ou where ou.uid = (select ${columnName}" : "ou.uid from ${organisationunit} ou where ou.uid = ${columnName}"; - return replaceQualify(ouClause, Map.of("columnName", columnName)); + return replaceQualify(ouClause, Map.of("columnName", columnExpression)); } else { - return columnName; + return columnExpression; } } + /** + * Returns a cast expression which includes a value filter for the given value type. + * + * @param columnExpression the column expression. + * @param filterRegex the value type filter regular expression. + * @param dataType the SQL data type. + * @return a cast and validate expression. + */ + String getCastExpression(String columnExpression, String filterRegex, String dataType) { + String filter = sqlBuilder.regexpMatch(columnExpression, filterRegex); + return String.format( + "case when %s then cast(%s as %s) else null end", filter, columnExpression, dataType); + } + @Override public boolean validState() { return tableIsNotEmpty("event"); @@ -192,84 +219,27 @@ protected void populateTableInternal(AnalyticsTablePartition partition, String f invokeTimeAndLog(sql, "Populating table: '{}'", tableName); } - protected List getTrackedEntityAttributeColumns(Program program) { - List columns = new ArrayList<>(); - - for (TrackedEntityAttribute attribute : program.getNonConfidentialTrackedEntityAttributes()) { - DataType dataType = getColumnType(attribute.getValueType(), isSpatialSupport()); - String dataClause = - attribute.isNumericType() - ? getNumericClause() - : attribute.isDateType() ? getDateClause() : ""; - String select = getSelectClauseForTea(attribute.getValueType(), "value"); - Skip skipIndex = skipIndex(attribute.getValueType(), attribute.hasOptionSet()); - - String sql = - replaceQualify( - """ - (select ${select} from ${trackedentityattributevalue} \ - where trackedentityid=en.trackedentityid \ - and trackedentityattributeid=${attributeId}\ - ${dataClause})${closingParentheses} as ${attributeUid}""", - Map.of( - "select", - select, - "attributeId", - String.valueOf(attribute.getId()), - "dataClause", - dataClause, - "closingParentheses", - getClosingParentheses(select), - "attributeUid", - quote(attribute.getUid()))); - columns.add( - AnalyticsTableColumn.builder() - .name(attribute.getUid()) - .columnType(AnalyticsColumnType.DYNAMIC) - .dataType(dataType) - .selectExpression(sql) - .skipIndex(skipIndex) - .build()); - - if (attribute.getValueType().isOrganisationUnit()) { - String fromTypeSql = "ou.name from organisationunit ou where ou.uid = (select value"; - String ouNameSql = selectForInsert(attribute, fromTypeSql, dataClause); - - columns.add( - AnalyticsTableColumn.builder() - .name((attribute.getUid() + OU_NAME_COL_SUFFIX)) - .columnType(AnalyticsColumnType.DYNAMIC) - .dataType(TEXT) - .selectExpression(ouNameSql) - .skipIndex(SKIP) - .build()); - } - } - return columns; - } - /** - * The select statement used by the table population. + * The select subquery statement. * * @param attribute the {@link TrackedEntityAttribute}. - * @param fromType the sql snippet related to "from" part - * @param dataClause the data type related clause like "NUMERIC" - * @return + * @param columnExpression the column expression. + * @param dataClause the data type related clause like "NUMERIC". + * @return a select statement. */ - protected String selectForInsert( - TrackedEntityAttribute attribute, String fromType, String dataClause) { + protected String getSelectSubquery( + TrackedEntityAttribute attribute, String columnExpression, String dataClause) { return replaceQualify( """ - (select ${fromType} from ${trackedentityattributevalue} \ - where trackedentityid=en.trackedentityid \ - and trackedentityattributeid=${attributeId}\ - ${dataClause})\ - ${closingParentheses} as ${attributeUid}""", + (select ${columnExpression} from ${trackedentityattributevalue} \ + where trackedentityid=en.trackedentityid \ + and trackedentityattributeid=${attributeId}${dataClause})\ + ${closingParentheses} as ${attributeUid}""", Map.of( - "fromType", fromType, + "columnExpression", columnExpression, "dataClause", dataClause, "attributeId", String.valueOf(attribute.getId()), - "closingParentheses", getClosingParentheses(fromType), + "closingParentheses", getClosingParentheses(columnExpression), "attributeUid", quote(attribute.getUid()))); } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractJdbcTableManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractJdbcTableManager.java index d9d27b66892d..d21a187ed63c 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractJdbcTableManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/AbstractJdbcTableManager.java @@ -32,9 +32,7 @@ import static org.hisp.dhis.analytics.table.util.PartitionUtils.getStartDate; import static org.hisp.dhis.commons.util.TextUtils.format; import static org.hisp.dhis.db.model.DataType.CHARACTER_11; -import static org.hisp.dhis.db.model.DataType.INTEGER; import static org.hisp.dhis.db.model.DataType.TEXT; -import static org.hisp.dhis.db.model.constraint.Nullable.NOT_NULL; import static org.hisp.dhis.util.DateUtils.toLongDate; import java.util.Collection; @@ -54,7 +52,7 @@ import org.hisp.dhis.analytics.AnalyticsTableType; import org.hisp.dhis.analytics.AnalyticsTableUpdateParams; import org.hisp.dhis.analytics.partition.PartitionManager; -import org.hisp.dhis.analytics.table.model.AnalyticsColumnType; +import org.hisp.dhis.analytics.table.model.AnalyticsDimensionType; import org.hisp.dhis.analytics.table.model.AnalyticsTable; import org.hisp.dhis.analytics.table.model.AnalyticsTableColumn; import org.hisp.dhis.analytics.table.model.AnalyticsTablePartition; @@ -86,6 +84,7 @@ import org.hisp.dhis.setting.SystemSettings; import org.hisp.dhis.setting.SystemSettingsProvider; import org.hisp.dhis.system.database.DatabaseInfoProvider; +import org.hisp.dhis.system.util.MathUtils; import org.hisp.dhis.util.DateUtils; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; @@ -111,7 +110,9 @@ public abstract class AbstractJdbcTableManager implements AnalyticsTableManager * */ protected static final String DATE_REGEXP = - "^\\d{4}-\\d{2}-\\d{2}(\\s|T)?((\\d{2}:)(\\d{2}:)?(\\d{2}))?(|.(\\d{3})|.(\\d{3})Z)?$"; + "'^\\d{4}-\\d{2}-\\d{2}(\\s|T)?((\\d{2}:)(\\d{2}:)?(\\d{2}))?(|.(\\d{3})|.(\\d{3})Z)?$'"; + + protected static final String NUMERIC_REGEXP = "'" + MathUtils.NUMERIC_LENIENT_REGEXP + "'"; protected static final Set NO_INDEX_VAL_TYPES = Set.of(ValueType.TEXT, ValueType.LONG_TEXT); @@ -534,7 +535,7 @@ protected List getOrganisationUnitGroupSetColumns() { String name = ougs.getUid(); return AnalyticsTableColumn.builder() .name(name) - .columnType(AnalyticsColumnType.DYNAMIC) + .dimensionType(AnalyticsDimensionType.DYNAMIC) .dataType(CHARACTER_11) .selectExpression("ougs." + quote(name)) .skipIndex(skipIndex(ougs)) @@ -551,7 +552,7 @@ protected List getDataElementGroupSetColumns() { String name = degs.getUid(); return AnalyticsTableColumn.builder() .name(name) - .columnType(AnalyticsColumnType.DYNAMIC) + .dimensionType(AnalyticsDimensionType.DYNAMIC) .dataType(CHARACTER_11) .selectExpression("degs." + quote(name)) .skipIndex(skipIndex(degs)) @@ -568,7 +569,7 @@ protected List getDisaggregationCategoryOptionGroupSetColu String name = cogs.getUid(); return AnalyticsTableColumn.builder() .name(name) - .columnType(AnalyticsColumnType.DYNAMIC) + .dimensionType(AnalyticsDimensionType.DYNAMIC) .dataType(CHARACTER_11) .selectExpression("dcs." + quote(name)) .skipIndex(skipIndex(cogs)) @@ -585,7 +586,7 @@ protected List getAttributeCategoryOptionGroupSetColumns() String name = cogs.getUid(); return AnalyticsTableColumn.builder() .name(name) - .columnType(AnalyticsColumnType.DYNAMIC) + .dimensionType(AnalyticsDimensionType.DYNAMIC) .dataType(CHARACTER_11) .selectExpression("acs." + quote(name)) .skipIndex(skipIndex(cogs)) @@ -602,7 +603,7 @@ protected List getDisaggregationCategoryColumns() { String name = category.getUid(); return AnalyticsTableColumn.builder() .name(name) - .columnType(AnalyticsColumnType.DYNAMIC) + .dimensionType(AnalyticsDimensionType.DYNAMIC) .dataType(CHARACTER_11) .selectExpression("dcs." + quote(name)) .skipIndex(skipIndex(category)) @@ -619,7 +620,7 @@ protected List getAttributeCategoryColumns() { String name = category.getUid(); return AnalyticsTableColumn.builder() .name(name) - .columnType(AnalyticsColumnType.DYNAMIC) + .dimensionType(AnalyticsDimensionType.DYNAMIC) .dataType(CHARACTER_11) .selectExpression("acs." + quote(name)) .skipIndex(skipIndex(category)) @@ -737,24 +738,6 @@ protected String replaceQualify(String template, Map variables) return TextUtils.replace(template, map); } - protected AnalyticsTableColumn getPartitionColumn() { - return AnalyticsTableColumn.builder() - .name("year") - .dataType(INTEGER) - .nullable(NOT_NULL) - // The expression should use sqlBuilder, but the concept of functions (like YEAR) - // is part of the previous PR (https://github.com/dhis2/dhis2-core/pull/19131/files) - .selectExpression( - """ - CASE - WHEN ev.status = 'SCHEDULE' THEN YEAR(ev.scheduleddate) - ELSE YEAR(ev.occurreddate) - END - """) - .skipIndex(Skip.SKIP) - .build(); - } - // ------------------------------------------------------------------------- // Private supportive methods // ------------------------------------------------------------------------- diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcAnalyticsTableManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcAnalyticsTableManager.java index 96ad39db8973..48bdf885800c 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcAnalyticsTableManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcAnalyticsTableManager.java @@ -78,7 +78,6 @@ import org.hisp.dhis.setting.SystemSettings; import org.hisp.dhis.setting.SystemSettingsProvider; import org.hisp.dhis.system.database.DatabaseInfoProvider; -import org.hisp.dhis.system.util.MathUtils; import org.hisp.dhis.util.DateUtils; import org.hisp.dhis.util.ObjectUtils; import org.springframework.beans.factory.annotation.Qualifier; @@ -278,11 +277,7 @@ public void populateTable(AnalyticsTableUpdateParams params, AnalyticsTableParti String doubleDataType = sqlBuilder.dataTypeDouble(); String numericClause = - skipDataTypeValidation - ? "" - : replace( - "and dv.value ~* '${expression}'", - Map.of("expression", MathUtils.NUMERIC_LENIENT_REGEXP)); + skipDataTypeValidation ? "" : "and " + sqlBuilder.regexpMatch("dv.value", NUMERIC_REGEXP); String zeroValueCondition = includeZeroValues ? " or des.zeroissignificant = true" : ""; String zeroValueClause = replace( diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcEnrollmentAnalyticsTableManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcEnrollmentAnalyticsTableManager.java index ac747204d146..31999aeab2e4 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcEnrollmentAnalyticsTableManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcEnrollmentAnalyticsTableManager.java @@ -27,6 +27,10 @@ */ package org.hisp.dhis.analytics.table; +import static org.hisp.dhis.analytics.table.model.Skip.SKIP; +import static org.hisp.dhis.analytics.util.AnalyticsUtils.getClosingParentheses; +import static org.hisp.dhis.analytics.util.AnalyticsUtils.getColumnType; +import static org.hisp.dhis.db.model.DataType.TEXT; import static org.hisp.dhis.util.DateUtils.toLongDate; import java.util.ArrayList; @@ -37,14 +41,17 @@ import org.hisp.dhis.analytics.AnalyticsTableType; import org.hisp.dhis.analytics.AnalyticsTableUpdateParams; import org.hisp.dhis.analytics.partition.PartitionManager; +import org.hisp.dhis.analytics.table.model.AnalyticsDimensionType; import org.hisp.dhis.analytics.table.model.AnalyticsTable; import org.hisp.dhis.analytics.table.model.AnalyticsTableColumn; import org.hisp.dhis.analytics.table.model.AnalyticsTablePartition; +import org.hisp.dhis.analytics.table.model.Skip; import org.hisp.dhis.analytics.table.setting.AnalyticsTableSettings; import org.hisp.dhis.category.CategoryService; import org.hisp.dhis.common.IdentifiableObjectManager; import org.hisp.dhis.commons.collection.UniqueArrayList; import org.hisp.dhis.dataapproval.DataApprovalLevelService; +import org.hisp.dhis.db.model.DataType; import org.hisp.dhis.db.model.Logged; import org.hisp.dhis.db.sql.SqlBuilder; import org.hisp.dhis.organisationunit.OrganisationUnitService; @@ -53,6 +60,7 @@ import org.hisp.dhis.resourcetable.ResourceTableService; import org.hisp.dhis.setting.SystemSettingsProvider; import org.hisp.dhis.system.database.DatabaseInfoProvider; +import org.hisp.dhis.trackedentity.TrackedEntityAttribute; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; @@ -165,6 +173,12 @@ left join analytics_rs_dateperiodstructure dps on cast(en.enrollmentdate as date populateTableInternal(partition, fromClause); } + /** + * Returns a list of columns for the given program. + * + * @param program the {@link Program}. + * @return a list of {@link AnalyticsTableColumn}. + */ private List getColumns(Program program) { List columns = new ArrayList<>(); columns.addAll(fixedColumns); @@ -173,6 +187,81 @@ private List getColumns(Program program) { columns.addAll(getOrganisationUnitGroupSetColumns()); columns.addAll(getPeriodTypeColumns("dps")); columns.addAll(getTrackedEntityAttributeColumns(program)); + columns.addAll(getTrackedEntityColumns(program)); + + return filterDimensionColumns(columns); + } + + /** + * Returns a list of tracked entity attribute {@link AnalyticsTableColumn}. + * + * @param program the {@link Program}. + * @return a list of {@link AnalyticsTableColumn}. + */ + private List getTrackedEntityAttributeColumns(Program program) { + List columns = new ArrayList<>(); + + for (TrackedEntityAttribute attribute : program.getNonConfidentialTrackedEntityAttributes()) { + DataType dataType = getColumnType(attribute.getValueType(), isSpatialSupport()); + String dataClause = + attribute.isNumericType() + ? getNumericClause() + : attribute.isDateType() ? getDateClause() : ""; + String select = getSelectExpressionForAttribute(attribute.getValueType(), "value"); + Skip skipIndex = skipIndex(attribute.getValueType(), attribute.hasOptionSet()); + + String sql = + replaceQualify( + """ + (select ${select} from ${trackedentityattributevalue} \ + where trackedentityid=en.trackedentityid \ + and trackedentityattributeid=${attributeId}\ + ${dataClause})${closingParentheses} as ${attributeUid}""", + Map.of( + "select", + select, + "attributeId", + String.valueOf(attribute.getId()), + "dataClause", + dataClause, + "closingParentheses", + getClosingParentheses(select), + "attributeUid", + quote(attribute.getUid()))); + columns.add( + AnalyticsTableColumn.builder() + .name(attribute.getUid()) + .dimensionType(AnalyticsDimensionType.DYNAMIC) + .dataType(dataType) + .selectExpression(sql) + .skipIndex(skipIndex) + .build()); + + if (attribute.getValueType().isOrganisationUnit()) { + String fromTypeSql = "ou.name from organisationunit ou where ou.uid = (select value"; + String ouNameSql = getSelectSubquery(attribute, fromTypeSql, dataClause); + + columns.add( + AnalyticsTableColumn.builder() + .name((attribute.getUid() + OU_NAME_COL_SUFFIX)) + .dimensionType(AnalyticsDimensionType.DYNAMIC) + .dataType(TEXT) + .selectExpression(ouNameSql) + .skipIndex(SKIP) + .build()); + } + } + return columns; + } + + /** + * Returns a list of tracked entity {@link AnalyticsTableColumn}. + * + * @param program the {@link Program}. + * @return a list of {@link AnalyticsTableColumn}. + */ + private List getTrackedEntityColumns(Program program) { + List columns = new ArrayList<>(); if (program.isRegistration()) { columns.add(EnrollmentAnalyticsColumn.TRACKED_ENTITY); @@ -181,6 +270,6 @@ private List getColumns(Program program) { } } - return filterDimensionColumns(columns); + return columns; } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManager.java index cf929d61a2de..e28f4314b7e3 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManager.java @@ -28,6 +28,7 @@ package org.hisp.dhis.analytics.table; import static java.util.stream.Collectors.toList; +import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.hisp.dhis.analytics.table.model.Skip.SKIP; import static org.hisp.dhis.analytics.util.AnalyticsUtils.getClosingParentheses; import static org.hisp.dhis.analytics.util.AnalyticsUtils.getColumnType; @@ -38,9 +39,6 @@ import static org.hisp.dhis.db.model.DataType.GEOMETRY; import static org.hisp.dhis.db.model.DataType.INTEGER; import static org.hisp.dhis.db.model.DataType.TEXT; -import static org.hisp.dhis.period.PeriodDataProvider.DataSource.DATABASE; -import static org.hisp.dhis.period.PeriodDataProvider.DataSource.SYSTEM_DEFINED; -import static org.hisp.dhis.system.util.MathUtils.NUMERIC_LENIENT_REGEXP; import static org.hisp.dhis.util.DateUtils.toLongDate; import static org.hisp.dhis.util.DateUtils.toMediumDate; @@ -57,7 +55,7 @@ import org.hisp.dhis.analytics.AnalyticsTableType; import org.hisp.dhis.analytics.AnalyticsTableUpdateParams; import org.hisp.dhis.analytics.partition.PartitionManager; -import org.hisp.dhis.analytics.table.model.AnalyticsColumnType; +import org.hisp.dhis.analytics.table.model.AnalyticsDimensionType; import org.hisp.dhis.analytics.table.model.AnalyticsTable; import org.hisp.dhis.analytics.table.model.AnalyticsTableColumn; import org.hisp.dhis.analytics.table.model.AnalyticsTablePartition; @@ -150,8 +148,7 @@ public List getAnalyticsTables(AnalyticsTableUpdateParams params isSpatialSupport()); List availableDataYears = - periodDataProvider.getAvailableYears( - analyticsTableSettings.getMaxPeriodYearsOffset() == null ? SYSTEM_DEFINED : DATABASE); + periodDataProvider.getAvailableYears(analyticsTableSettings.getPeriodSource()); return params.isLatestUpdate() ? getLatestAnalyticsTables(params) @@ -224,7 +221,7 @@ private List getLatestAnalyticsTables(AnalyticsTableUpdateParams Assert.isTrue( lastFullTableUpdate.getTime() > 0L, - "A full analytics table update process must be run prior to a latest partition update process"); + "A full analytics table update process must be run prior to a latest partition update"); Date startDate = lastFullTableUpdate; Date endDate = params.getStartTime(); @@ -330,8 +327,7 @@ protected List getPartitionChecks(Integer year, Date endDate) { @Override public void populateTable(AnalyticsTableUpdateParams params, AnalyticsTablePartition partition) { List availableDataYears = - periodDataProvider.getAvailableYears( - analyticsTableSettings.getMaxPeriodYearsOffset() == null ? SYSTEM_DEFINED : DATABASE); + periodDataProvider.getAvailableYears(analyticsTableSettings.getPeriodSource()); Integer firstDataYear = availableDataYears.get(0); Integer latestDataYear = availableDataYears.get(availableDataYears.size() - 1); Program program = partition.getMasterTable().getProgram(); @@ -402,59 +398,14 @@ private String getPartitionClause(AnalyticsTablePartition partition) { */ private List getColumns(Program program) { List columns = new ArrayList<>(fixedColumns); - - if (program.hasNonDefaultCategoryCombo()) { - List categories = program.getCategoryCombo().getCategories(); - - for (Category category : categories) { - if (category.isDataDimension()) { - columns.add( - AnalyticsTableColumn.builder() - .name(category.getUid()) - .columnType(AnalyticsColumnType.DYNAMIC) - .dataType(CHARACTER_11) - .selectExpression("acs." + quote(category.getUid())) - .created(category.getCreated()) - .build()); - } - } - } - + columns.addAll(getAttributeCategoryColumns(program)); columns.addAll(getOrganisationUnitLevelColumns()); columns.add(getOrganisationUnitNameHierarchyColumn()); columns.addAll(getOrganisationUnitGroupSetColumns()); columns.addAll(getAttributeCategoryOptionGroupSetColumns()); columns.addAll(getPeriodTypeColumns("dps")); - - columns.addAll( - program.getAnalyticsDataElements().stream() - .map(de -> getColumnFromDataElement(de, false)) - .flatMap(Collection::stream) - .toList()); - - columns.addAll( - program.getAnalyticsDataElementsWithLegendSet().stream() - .map(de -> getColumnFromDataElement(de, true)) - .flatMap(Collection::stream) - .toList()); - - columns.addAll( - program.getNonConfidentialTrackedEntityAttributes().stream() - .map( - tea -> - getColumnFromTrackedEntityAttribute( - tea, getNumericClause(), getDateClause(), false)) - .flatMap(Collection::stream) - .toList()); - - columns.addAll( - program.getNonConfidentialTrackedEntityAttributesWithLegendSet().stream() - .map( - tea -> - getColumnFromTrackedEntityAttribute( - tea, getNumericClause(), getDateClause(), true)) - .flatMap(Collection::stream) - .toList()); + columns.addAll(getDataElementColumns(program)); + columns.addAll(getAttributeColumns(program)); if (program.isRegistration()) { columns.add(EventAnalyticsColumn.TRACKED_ENTITY); @@ -470,8 +421,12 @@ tea, getNumericClause(), getDateClause(), true)) return filterDimensionColumns(columns); } - @Override - protected AnalyticsTableColumn getPartitionColumn() { + /** + * Returns a partition column. + * + * @return an {@link AnalyticsTableColumn}. + */ + private AnalyticsTableColumn getPartitionColumn() { return AnalyticsTableColumn.builder() .name("year") .dataType(INTEGER) @@ -479,40 +434,198 @@ protected AnalyticsTableColumn getPartitionColumn() { .build(); } - private List getColumnFromTrackedEntityAttribute( - TrackedEntityAttribute attribute, - String numericClause, - String dateClause, - boolean withLegendSet) { + /** + * Returns columns for attribute categories of the given program. + * + * @param program the {@link Program}. + * @return a list of {@link AnalyticsTableColumn}. + */ + private List getAttributeCategoryColumns(Program program) { + if (program.hasNonDefaultCategoryCombo()) { + List categories = program.getCategoryCombo().getDataDimensionCategories(); + return categories.stream() + .map( + category -> + AnalyticsTableColumn.builder() + .name(category.getUid()) + .dimensionType(AnalyticsDimensionType.DYNAMIC) + .dataType(CHARACTER_11) + .selectExpression("acs." + quote(category.getUid())) + .created(category.getCreated()) + .build()) + .toList(); + } + + return List.of(); + } + + /** + * Returns columns for data elements of the given program. + * + * @param program the {@link Program}. + * @return a list of {@link AnalyticsTableColumn}. + */ + private List getDataElementColumns(Program program) { + List columns = new ArrayList<>(); + columns.addAll( + program.getAnalyticsDataElements().stream() + .map(de -> getColumnForDataElement(de, false)) + .flatMap(Collection::stream) + .toList()); + columns.addAll( + program.getAnalyticsDataElementsWithLegendSet().stream() + .map(de -> getColumnForDataElement(de, true)) + .flatMap(Collection::stream) + .toList()); + return columns; + } + + /** + * Returns a column for the given data element. If the value type of the data element is {@link + * ValueType#ORGANISATION_UNIT}, an extra column will be included. + * + * @param dataElement the {@link DataElement}. + * @param withLegendSet indicates + * @return + */ + private List getColumnForDataElement( + DataElement dataElement, boolean withLegendSet) { + List columns = new ArrayList<>(); + + DataType dataType = getColumnType(dataElement.getValueType(), isSpatialSupport()); + String columnExpression = + sqlBuilder.jsonExtractNested("eventdatavalues", dataElement.getUid(), "value"); + String selectExpression = getSelectExpression(dataElement.getValueType(), columnExpression); + String dataFilterClause = getDataFilterClause(dataElement); + String sql = getSelectForInsert(dataElement, selectExpression, dataFilterClause); + Skip skipIndex = skipIndex(dataElement.getValueType(), dataElement.hasOptionSet()); + + if (dataElement.getValueType().isOrganisationUnit()) { + columns.addAll(getColumnForOrgUnitDataElement(dataElement, dataFilterClause)); + } + + columns.add( + AnalyticsTableColumn.builder() + .name(dataElement.getUid()) + .dimensionType(AnalyticsDimensionType.DYNAMIC) + .dataType(dataType) + .selectExpression(sql) + .skipIndex(skipIndex) + .build()); + + return withLegendSet + ? getColumnFromDataElementWithLegendSet(dataElement, selectExpression, dataFilterClause) + : columns; + } + + /** + * Returns a list of columns. + * + * @param dataElement the {@link DataElement}. + * @param dataFilterClause the data filter SQL clause. + * @return a list of {@link AnalyticsTableColumn}. + */ + private List getColumnForOrgUnitDataElement( + DataElement dataElement, String dataFilterClause) { + List columns = new ArrayList<>(); + + String columnExpression = + sqlBuilder.jsonExtractNested("eventdatavalues", dataElement.getUid(), "value"); + String fromClause = + qualifyVariables("from ${organisationunit} ou where ou.uid = " + columnExpression); + + if (isSpatialSupport()) { + String fromType = "ou.geometry " + fromClause; + String geoSql = getSelectForInsert(dataElement, fromType, dataFilterClause); + + columns.add( + AnalyticsTableColumn.builder() + .name((dataElement.getUid() + OU_GEOMETRY_COL_SUFFIX)) + .dimensionType(AnalyticsDimensionType.DYNAMIC) + .dataType(GEOMETRY) + .selectExpression(geoSql) + .indexType(IndexType.GIST) + .build()); + } + + String fromTypeSql = "ou.name " + fromClause; + String ouNameSql = getSelectForInsert(dataElement, fromTypeSql, dataFilterClause); + + columns.add( + AnalyticsTableColumn.builder() + .name((dataElement.getUid() + OU_NAME_COL_SUFFIX)) + .dimensionType(AnalyticsDimensionType.DYNAMIC) + .dataType(TEXT) + .selectExpression(ouNameSql) + .skipIndex(SKIP) + .build()); + + return columns; + } + + /** + * Returns columns for attributes of the given program. + * + * @param program the {@link Program}. + * @return a list of {@link AnalyticsTableColumn}. + */ + private List getAttributeColumns(Program program) { + List columns = new ArrayList<>(); + columns.addAll( + program.getNonConfidentialTrackedEntityAttributes().stream() + .map(this::getColumnForAttribute) + .flatMap(Collection::stream) + .toList()); + columns.addAll( + program.getNonConfidentialTrackedEntityAttributesWithLegendSet().stream() + .map(this::getColumnForAttributeWithLegendSet) + .flatMap(Collection::stream) + .toList()); + return columns; + } + + /** + * Returns a list of columns based on the given attribute. + * + * @param attribute the {@link TrackedEntityAttribute}. + * @param withLegendSet indicates whether the attribute has a legend set. + * @return a list of {@link AnaylyticsTableColumn}. + */ + private List getColumnForAttribute(TrackedEntityAttribute attribute) { List columns = new ArrayList<>(); DataType dataType = getColumnType(attribute.getValueType(), isSpatialSupport()); - String dataClause = - attribute.isNumericType() ? numericClause : attribute.isDateType() ? dateClause : ""; - String select = getSelectClauseForTea(attribute.getValueType(), "value"); - String sql = selectForInsert(attribute, select, dataClause); + String selectExpression = getSelectExpressionForAttribute(attribute.getValueType(), "value"); + String dataExpression = getDataFilterClause(attribute); + String sql = getSelectSubquery(attribute, selectExpression, dataExpression); Skip skipIndex = skipIndex(attribute.getValueType(), attribute.hasOptionSet()); if (attribute.getValueType().isOrganisationUnit()) { - columns.addAll(getColumnsFromOrgUnitTrackedEntityAttribute(attribute, dataClause)); + columns.addAll(getColumnsForOrgUnitTrackedEntityAttribute(attribute, dataExpression)); } + columns.add( AnalyticsTableColumn.builder() .name(attribute.getUid()) - .columnType(AnalyticsColumnType.DYNAMIC) + .dimensionType(AnalyticsDimensionType.DYNAMIC) .dataType(dataType) .selectExpression(sql) .skipIndex(skipIndex) .build()); - return withLegendSet - ? getColumnFromTrackedEntityAttributeWithLegendSet(attribute, numericClause) - : columns; + return columns; } - private List getColumnFromTrackedEntityAttributeWithLegendSet( - TrackedEntityAttribute attribute, String numericClause) { - String selectClause = getSelectClause(attribute.getValueType(), "value"); + /** + * Returns a list of columns based on the given attribute with legend set. + * + * @param attribute the {@link TrackedEntityAttribute}. + * @return a list of {@link AnalyticsTableColumn}. + */ + private List getColumnForAttributeWithLegendSet( + TrackedEntityAttribute attribute) { + String selectClause = getSelectExpression(attribute.getValueType(), "value"); + String numericClause = getNumericClause(); String query = """ \s(select l.uid from ${maplegend} l \ @@ -545,49 +658,27 @@ private List getColumnFromTrackedEntityAttributeWithLegend .collect(toList()); } - private List getColumnFromDataElement( - DataElement dataElement, boolean withLegendSet) { + /** + * Returns a list of columns based on the given attribute. + * + * @param attribute the {@link TrackedEntityAttribute}. + * @param dataFilterClause the data filter clause. + * @return a list of {@link AnalyticsTableColumn}. + */ + private List getColumnsForOrgUnitTrackedEntityAttribute( + TrackedEntityAttribute attribute, String dataFilterClause) { List columns = new ArrayList<>(); - DataType dataType = getColumnType(dataElement.getValueType(), isSpatialSupport()); - String dataClause = getDataClause(dataElement.getUid(), dataElement.getValueType()); - String columnName = - sqlBuilder.jsonExtractNested("eventdatavalues", dataElement.getUid(), "value"); - String select = getSelectClause(dataElement.getValueType(), columnName); - String sql = selectForInsert(dataElement, select, dataClause); - Skip skipIndex = skipIndex(dataElement.getValueType(), dataElement.hasOptionSet()); - - if (dataElement.getValueType().isOrganisationUnit()) { - columns.addAll(getColumnFromOrgUnitDataElement(dataElement, dataClause)); - } - columns.add( - AnalyticsTableColumn.builder() - .name(dataElement.getUid()) - .columnType(AnalyticsColumnType.DYNAMIC) - .dataType(dataType) - .selectExpression(sql) - .skipIndex(skipIndex) - .build()); - - return withLegendSet - ? getColumnFromDataElementWithLegendSet(dataElement, select, dataClause) - : columns; - } - - private List getColumnsFromOrgUnitTrackedEntityAttribute( - TrackedEntityAttribute attribute, String dataClause) { - final List columns = new ArrayList<>(); - - final String fromClause = + String fromClause = qualifyVariables("from ${organisationunit} ou where ou.uid = (select value"); if (isSpatialSupport()) { String fromType = "ou.geometry " + fromClause; - String geoSql = selectForInsert(attribute, fromType, dataClause); + String geoSql = getSelectSubquery(attribute, fromType, dataFilterClause); columns.add( AnalyticsTableColumn.builder() .name((attribute.getUid() + OU_GEOMETRY_COL_SUFFIX)) - .columnType(AnalyticsColumnType.DYNAMIC) + .dimensionType(AnalyticsDimensionType.DYNAMIC) .dataType(GEOMETRY) .selectExpression(geoSql) .indexType(IndexType.GIST) @@ -595,51 +686,12 @@ private List getColumnsFromOrgUnitTrackedEntityAttribute( } String fromTypeSql = "ou.name " + fromClause; - String ouNameSql = selectForInsert(attribute, fromTypeSql, dataClause); + String ouNameSql = getSelectSubquery(attribute, fromTypeSql, dataFilterClause); columns.add( AnalyticsTableColumn.builder() .name((attribute.getUid() + OU_NAME_COL_SUFFIX)) - .columnType(AnalyticsColumnType.DYNAMIC) - .dataType(TEXT) - .selectExpression(ouNameSql) - .skipIndex(SKIP) - .build()); - - return columns; - } - - private List getColumnFromOrgUnitDataElement( - DataElement dataElement, String dataClause) { - final List columns = new ArrayList<>(); - - final String columnName = - sqlBuilder.jsonExtractNested("eventdatavalues", dataElement.getUid(), "value"); - - final String fromClause = - qualifyVariables("from ${organisationunit} ou where ou.uid = " + columnName); - - if (isSpatialSupport()) { - String fromType = "ou.geometry " + fromClause; - String geoSql = selectForInsert(dataElement, fromType, dataClause); - - columns.add( - AnalyticsTableColumn.builder() - .name((dataElement.getUid() + OU_GEOMETRY_COL_SUFFIX)) - .columnType(AnalyticsColumnType.DYNAMIC) - .dataType(GEOMETRY) - .selectExpression(geoSql) - .indexType(IndexType.GIST) - .build()); - } - - String fromTypeSql = "ou.name " + fromClause; - String ouNameSql = selectForInsert(dataElement, fromTypeSql, dataClause); - - columns.add( - AnalyticsTableColumn.builder() - .name((dataElement.getUid() + OU_NAME_COL_SUFFIX)) - .columnType(AnalyticsColumnType.DYNAMIC) + .dimensionType(AnalyticsDimensionType.DYNAMIC) .dataType(TEXT) .selectExpression(ouNameSql) .skipIndex(SKIP) @@ -649,41 +701,49 @@ private List getColumnFromOrgUnitDataElement( } /** - * Creates a select statement for data element insertion. + * Creates a select statement for the given select expression. * - * @param dataElement The data element to create the select statement for - * @param fromType The SQL snippet for the "from" part of the query - * @param dataClause The data type related clause - * @return A SQL select expression for the data element + * @param dataElement the data element to create the select statement for. + * @param selectExpression the select expression. + * @param dataFilterClause the data filter clause. + * @return A SQL select expression for the data element. */ - private String selectForInsert(DataElement dataElement, String fromType, String dataClause) { - Map replacements = - Map.of( - "fromType", - fromType, - "dataClause", - dataClause, - "closingParentheses", - getClosingParentheses(fromType), - "dataElementUid", - quote(dataElement.getUid())); - + private String getSelectForInsert( + DataElement dataElement, String selectExpression, String dataFilterClause) { String sqlTemplate = dataElement.getValueType().isOrganisationUnit() - ? "(select ${fromType} ${dataClause})${closingParentheses} as ${dataElementUid}" - : "(select ${fromType} from ${event} where eventid=ev.eventid ${dataClause})${closingParentheses} as ${dataElementUid}"; + ? "(select ${selectExpression} ${dataClause})${closingParentheses} as ${uid}" + : "${selectExpression}${closingParentheses} as ${uid}"; - return replaceQualify(sqlTemplate, replacements); + return replaceQualify( + sqlTemplate, + Map.of( + "selectExpression", + selectExpression, + "dataClause", + dataFilterClause, + "closingParentheses", + getClosingParentheses(selectExpression), + "uid", + quote(dataElement.getUid()))); } + /** + * Returns a list of columns. + * + * @param dataElement the {@link DataElement}. + * @param selectExpression the select expression. + * @param dataFilterClause the data filter clause. + * @return a list of {@link AnayticsTableColumn}. + */ private List getColumnFromDataElementWithLegendSet( - DataElement dataElement, String select, String dataClause) { + DataElement dataElement, String selectExpression, String dataFilterClause) { String query = """ - (select l.uid from ${maplegend} l - inner join ${event} on l.startvalue <= ${select} - and l.endvalue > ${select} - and l.maplegendsetid=${legendSetId} + (select l.uid from ${maplegend} l \ + inner join ${event} on l.startvalue <= ${select} \ + and l.endvalue > ${select} \ + and l.maplegendsetid=${legendSetId} \ ${dataClause} where eventid = ev.eventid) as ${column}"""; return dataElement.getLegendSets().stream() @@ -694,9 +754,9 @@ private List getColumnFromDataElementWithLegendSet( replaceQualify( query, Map.of( - "select", select, + "select", selectExpression, "legendSetId", String.valueOf(ls.getId()), - "dataClause", dataClause, + "dataClause", dataFilterClause, "column", column)); return AnalyticsTableColumn.builder() @@ -705,26 +765,58 @@ private List getColumnFromDataElementWithLegendSet( .selectExpression(sql) .build(); }) - .collect(toList()); + .toList(); } - private String getDataClause(String uid, ValueType valueType) { - if (valueType.isNumeric() || valueType.isDate()) { - String regex = valueType.isNumeric() ? NUMERIC_LENIENT_REGEXP : DATE_REGEXP; + /** + * For numeric and date value types, returns a data filter clause for checking whether the value + * is valid according to the value type. For other value types, returns the empty string. + * + * @param dataElement the {@link DataElement}. + * @return an filter expression. + */ + private String getDataFilterClause(DataElement dataElement) { + String uid = dataElement.getUid(); + ValueType valueType = dataElement.getValueType(); - String jsonValue = sqlBuilder.jsonExtractNested("eventdatavalues", uid, "value"); + if (valueType.isNumeric() || valueType.isDate()) { + String jsonExpression = sqlBuilder.jsonExtractNested("eventdatavalues", uid, "value"); + String regex = valueType.isNumeric() ? NUMERIC_REGEXP : DATE_REGEXP; - return " and " + jsonValue + " " + sqlBuilder.regexpMatch("'" + regex + "'"); + return " and " + sqlBuilder.regexpMatch(jsonExpression, regex); } - return ""; + return EMPTY; } + /** + * For numeric and date value types, returns a data filter clause for checking whether the value + * is valid according to the value type. For other value types, returns the empty string. + * + * @param attribute the {@link TrackedEntityAttribute}. + * @return an filter expression. + */ + private String getDataFilterClause(TrackedEntityAttribute attribute) { + if (attribute.isNumericType()) { + return getNumericClause(); + } + return attribute.isDateType() ? getDateClause() : EMPTY; + } + + /** + * Returns a list of years for which data exist. + * + * @param params the {@link AnalyticsTableUpdateParams}. + * @param program the {@link Program}. + * @param firstDataYear the first year to include. + * @param lastDataYear the last data year to include. + * @return a list of years for which data exist. + */ private List getDataYears( AnalyticsTableUpdateParams params, Program program, Integer firstDataYear, - Integer latestDataYear) { + Integer lastDataYear) { String fromDateClause = params.getFromDate() != null ? replace( @@ -734,7 +826,7 @@ private List getDataYears( eventDateExpression, "fromDate", toMediumDate(params.getFromDate()))) - : ""; + : EMPTY; String sql = replaceQualify( """ @@ -755,7 +847,7 @@ private List getDataYears( "programId", String.valueOf(program.getId()), "fromDateClause", fromDateClause, "firstDataYear", String.valueOf(firstDataYear), - "latestDataYear", String.valueOf(latestDataYear))); + "latestDataYear", String.valueOf(lastDataYear))); return jdbcTemplate.queryForList(sql, Integer.class); } @@ -764,8 +856,8 @@ private List getDataYears( * Retrieve years for partition tables. Year will become a partition key. The default return value * is the list with the recent year. * - * @param dataYears list of years coming from inner join of event and enrollment tables - * @return list of partition key values + * @param dataYears the list of years coming from inner join of event and enrollment tables. + * @return list of partition key values. */ private List getYearsForPartitionTable(List dataYears) { return ListUtils.mutableCopy(!dataYears.isEmpty() ? dataYears : List.of(Year.now().getValue())); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityAnalyticsTableManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityAnalyticsTableManager.java index e23833d08d29..afbfdf81dbe6 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityAnalyticsTableManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityAnalyticsTableManager.java @@ -223,9 +223,6 @@ private List getColumns( .toList()); columns.addAll(getOrganisationUnitGroupSetColumns()); - if (sqlBuilder.supportsDeclarativePartitioning()) { - columns.add(getPartitionColumn()); - } return columns; } @@ -240,22 +237,19 @@ private List getColumns( private Stream getAllTrackedEntityAttributes( TrackedEntityType trackedEntityType, Map> programsByTetUid) { - // Given TET has program(s) defined. if (programsByTetUid.containsKey(trackedEntityType.getUid())) { - // Programs defined for TET -> get attr from program and TET. return getAllTrackedEntityAttributesByPrograms( trackedEntityType, programsByTetUid.get(trackedEntityType.getUid())); } - // No programs defined for TET -> get only attributes from TET. return getAllTrackedEntityAttributesByEntityType(trackedEntityType); } /** * Returns the select clause, potentially with a cast statement, based on the given value type. * (this method is an adapted version of {@link - * JdbcEventAnalyticsTableManager#getSelectClause(ValueType, String)}) + * JdbcEventAnalyticsTableManager#getSelectExpression(ValueType, String)}) * * @param valueType the value type to represent as database column type. */ @@ -279,7 +273,7 @@ private String castBasedOnType(ValueType valueType, String columnName) { " cast(${columnName} as ${type})", Map.of("columnName", columnName, "type", sqlBuilder.dataTypeTimestamp())); } - if (valueType.isGeo() && isSpatialSupport()) { + if (valueType.isGeo() && isSpatialSupport() && sqlBuilder.supportsGeospatialData()) { return replace( """ \s ST_GeomFromGeoJSON('{"type":"Point", "coordinates":' || (${columnName}) || ', @@ -300,9 +294,7 @@ private String castBasedOnType(ValueType valueType, String columnName) { private Stream getAllTrackedEntityAttributesByPrograms( TrackedEntityType trackedEntityType, List programs) { return Stream.concat( - /* all attributes of programs */ trackedEntityAttributeService.getProgramTrackedEntityAttributes(programs).stream(), - /* all attributes of the trackedEntityType */ getAllTrackedEntityAttributesByEntityType(trackedEntityType)) .distinct(); } @@ -377,8 +369,8 @@ public void populateTable(AnalyticsTableUpdateParams params, AnalyticsTableParti sql.append( replaceQualify( """ - \s left join ${trackedentityattributevalue} ${teaUid} on ${teaUid}.trackedentityid=te.trackedentityid \ - and ${teaUid}.trackedentityattributeid = ${teaId}""", + \s left join ${trackedentityattributevalue} ${teaUid} on ${teaUid}.trackedentityid=te.trackedentityid \ + and ${teaUid}.trackedentityattributeid = ${teaId}""", Map.of( "teaUid", quote(tea.getUid()), "teaId", String.valueOf(tea.getId()))))); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityEnrollmentsAnalyticsTableManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityEnrollmentsAnalyticsTableManager.java index 6c9309174ebe..07ef5fe4df1b 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityEnrollmentsAnalyticsTableManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityEnrollmentsAnalyticsTableManager.java @@ -55,6 +55,7 @@ import org.hisp.dhis.analytics.table.model.AnalyticsTable; import org.hisp.dhis.analytics.table.model.AnalyticsTableColumn; import org.hisp.dhis.analytics.table.model.AnalyticsTablePartition; +import org.hisp.dhis.analytics.table.model.Skip; import org.hisp.dhis.analytics.table.setting.AnalyticsTableSettings; import org.hisp.dhis.category.CategoryService; import org.hisp.dhis.common.IdentifiableObjectManager; @@ -115,24 +116,6 @@ public class JdbcTrackedEntityEnrollmentsAnalyticsTableManager extends AbstractJ .dataType(VARCHAR_50) .selectExpression("en.status") .build(), - AnalyticsTableColumn.builder() - .name("enrollmentgeometry") - .dataType(GEOMETRY) - .selectExpression("en.geometry") - .indexType(IndexType.GIST) - .build(), - AnalyticsTableColumn.builder() - .name("enrollmentlongitude") - .dataType(DOUBLE) - .selectExpression( - "case when 'POINT' = GeometryType(en.geometry) then ST_X(en.geometry) end") - .build(), - AnalyticsTableColumn.builder() - .name("enrollmentlatitude") - .dataType(DOUBLE) - .selectExpression( - "case when 'POINT' = GeometryType(en.geometry) then ST_Y(en.geometry) end") - .build(), AnalyticsTableColumn.builder() .name("uidlevel1") .dataType(CHARACTER_11) @@ -243,7 +226,7 @@ public List getAnalyticsTables(AnalyticsTableUpdateParams params private List getColumns() { List columns = new ArrayList<>(); - columns.addAll(FIXED_COLS); + columns.addAll(getFixedCols()); columns.add(getOrganisationUnitNameHierarchyColumn()); if (sqlBuilder.supportsDeclarativePartitioning()) { columns.add(getPartitionColumn()); @@ -290,7 +273,7 @@ public void populateTable(AnalyticsTableUpdateParams params, AnalyticsTableParti and te.lastupdated < '${startTime}' \ left join ${program} p on en.programid=p.programid \ left join analytics_rs_orgunitstructure ous on en.organisationunitid=ous.organisationunitid \ - where exists (select 1 from event ev where ev.deleted = false \ + where exists (select 1 from ${event} ev where ev.deleted = false \ and ev.enrollmentid = en.enrollmentid \ and ev.status in (${statuses})) \ and en.occurreddate is not null \ @@ -303,4 +286,56 @@ and ev.status in (${statuses})) \ invokeTimeAndLog(sql.toString(), "Populating table: '{}'", tableName); } + + private List getFixedCols() { + List columns = new ArrayList<>(); + columns.addAll(FIXED_COLS); + if (sqlBuilder.supportsGeospatialData()) { + columns.addAll(getGeospatialCols()); + } + return columns; + } + + /** + * Returns a list of geospatial columns. + * + * @return a list of {@link AnalyticsTableColumn}. + */ + private List getGeospatialCols() { + + return List.of( + AnalyticsTableColumn.builder() + .name("enrollmentgeometry") + .dataType(GEOMETRY) + .selectExpression("en.geometry") + .indexType(IndexType.GIST) + .build(), + AnalyticsTableColumn.builder() + .name("enrollmentlongitude") + .dataType(DOUBLE) + .selectExpression( + "case when 'POINT' = GeometryType(en.geometry) then ST_X(en.geometry) end") + .build(), + AnalyticsTableColumn.builder() + .name("enrollmentlatitude") + .dataType(DOUBLE) + .selectExpression( + "case when 'POINT' = GeometryType(en.geometry) then ST_Y(en.geometry) end") + .build()); + } + + /** + * Returns a partition column. + * + * @return an {@link AnalyticsTableColumn}. + */ + private AnalyticsTableColumn getPartitionColumn() { + return AnalyticsTableColumn.builder() + .name("year") + .dataType(INTEGER) + .nullable(NOT_NULL) + .selectExpression("extract(year from en.occurreddate)") + .skipIndex(Skip.SKIP) + .build(); + } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityEventsAnalyticsTableManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityEventsAnalyticsTableManager.java index 520e241ce3e8..f3055b6db7bc 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityEventsAnalyticsTableManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityEventsAnalyticsTableManager.java @@ -46,8 +46,8 @@ import static org.hisp.dhis.db.model.DataType.VARCHAR_50; import static org.hisp.dhis.db.model.constraint.Nullable.NOT_NULL; import static org.hisp.dhis.db.model.constraint.Nullable.NULL; -import static org.hisp.dhis.period.PeriodDataProvider.DataSource.DATABASE; -import static org.hisp.dhis.period.PeriodDataProvider.DataSource.SYSTEM_DEFINED; +import static org.hisp.dhis.period.PeriodDataProvider.PeriodSource.DATABASE; +import static org.hisp.dhis.period.PeriodDataProvider.PeriodSource.SYSTEM_DEFINED; import static org.hisp.dhis.util.DateUtils.toLongDate; import static org.hisp.dhis.util.DateUtils.toMediumDate; @@ -71,6 +71,7 @@ import org.hisp.dhis.dataapproval.DataApprovalLevelService; import org.hisp.dhis.db.model.IndexType; import org.hisp.dhis.db.model.Logged; +import org.hisp.dhis.db.sql.AnalyticsSqlBuilder; import org.hisp.dhis.db.sql.SqlBuilder; import org.hisp.dhis.organisationunit.OrganisationUnitService; import org.hisp.dhis.period.PeriodDataProvider; @@ -82,36 +83,10 @@ import org.hisp.dhis.trackedentity.TrackedEntityTypeService; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -@Component("org.hisp.dhis.analytics.TrackedEntityEventsAnalyticsTableManager") public class JdbcTrackedEntityEventsAnalyticsTableManager extends AbstractJdbcTableManager { - private static final String EVENT_DATA_VALUE_REBUILDER = - """ - (select json_object_agg(l2.keys, l2.datavalue) as value - from ( - select l1.uid, - l1.keys, - json_strip_nulls(json_build_object( - 'value', l1.eventdatavalues -> l1.keys ->> 'value', - 'created', l1.eventdatavalues -> l1.keys ->> 'created', - 'storedBy', l1.eventdatavalues -> l1.keys ->> 'storedBy', - 'lastUpdated', l1.eventdatavalues -> l1.keys ->> 'lastUpdated', - 'providedElsewhere', l1.eventdatavalues -> l1.keys -> 'providedElsewhere', - 'value_name', (select ou.name - from organisationunit ou - where ou.uid = l1.eventdatavalues -> l1.keys ->> 'value'), - 'value_code', (select ou.code - from organisationunit ou - where ou.uid = l1.eventdatavalues -> l1.keys ->> 'value'))) as datavalue - from (select inner_evt.*, jsonb_object_keys(inner_evt.eventdatavalues) keys - from event inner_evt) as l1) as l2 - where l2.uid = ev.uid - group by l2.uid)::jsonb - """; - private static final List FIXED_COLS = List.of( AnalyticsTableColumn.builder() @@ -169,24 +144,6 @@ public class JdbcTrackedEntityEventsAnalyticsTableManager extends AbstractJdbcTa .dataType(VARCHAR_50) .selectExpression("ev.status") .build(), - AnalyticsTableColumn.builder() - .name("eventgeometry") - .dataType(GEOMETRY) - .selectExpression("ev.geometry") - .indexType(IndexType.GIST) - .build(), - AnalyticsTableColumn.builder() - .name("evlongitude") - .dataType(DOUBLE) - .selectExpression( - "case when 'POINT' = GeometryType(ev.geometry) then ST_X(ev.geometry) end") - .build(), - AnalyticsTableColumn.builder() - .name("evlatitude") - .dataType(DOUBLE) - .selectExpression( - "case when 'POINT' = GeometryType(ev.geometry) then ST_Y(ev.geometry) end") - .build(), AnalyticsTableColumn.builder() .name("uidlevel1") .dataType(CHARACTER_11) @@ -234,18 +191,12 @@ public class JdbcTrackedEntityEventsAnalyticsTableManager extends AbstractJdbcTa .dataType(INTEGER) .nullable(NULL) .selectExpression("ous.level") - .build(), - AnalyticsTableColumn.builder() - .name("eventdatavalues") - .dataType(JSONB) - .selectExpression(EVENT_DATA_VALUE_REBUILDER) - .skipIndex(Skip.SKIP) .build()); - private static final String AND = " and ("; - private final TrackedEntityTypeService trackedEntityTypeService; + private final AnalyticsSqlBuilder analyticsSqlBuilder; + public JdbcTrackedEntityEventsAnalyticsTableManager( IdentifiableObjectManager idObjectManager, OrganisationUnitService organisationUnitService, @@ -260,7 +211,8 @@ public JdbcTrackedEntityEventsAnalyticsTableManager( TrackedEntityTypeService trackedEntityTypeService, AnalyticsTableSettings analyticsTableSettings, PeriodDataProvider periodDataProvider, - SqlBuilder sqlBuilder) { + SqlBuilder sqlBuilder, + AnalyticsSqlBuilder analyticsSqlBuilder) { super( idObjectManager, organisationUnitService, @@ -276,6 +228,7 @@ public JdbcTrackedEntityEventsAnalyticsTableManager( periodDataProvider, sqlBuilder); this.trackedEntityTypeService = trackedEntityTypeService; + this.analyticsSqlBuilder = analyticsSqlBuilder; } /** @@ -322,6 +275,48 @@ public List getAnalyticsTables(AnalyticsTableUpdateParams params return tables; } + private List getFixedCols() { + List columns = new ArrayList<>(); + columns.addAll(FIXED_COLS); + columns.add(getEventDataValueColumn()); + if (sqlBuilder.supportsGeospatialData()) { + columns.addAll(getGeospatialCols()); + } + return columns; + } + + private AnalyticsTableColumn getEventDataValueColumn() { + return AnalyticsTableColumn.builder() + .name("eventdatavalues") + .dataType(JSONB) + .selectExpression(analyticsSqlBuilder.getEventDataValues()) + .skipIndex(Skip.SKIP) + .build(); + } + + private List getGeospatialCols() { + + return List.of( + AnalyticsTableColumn.builder() + .name("eventgeometry") + .dataType(GEOMETRY) + .selectExpression("ev.geometry") + .indexType(IndexType.GIST) + .build(), + AnalyticsTableColumn.builder() + .name("evlongitude") + .dataType(DOUBLE) + .selectExpression( + "case when 'POINT' = GeometryType(ev.geometry) then ST_X(ev.geometry) end") + .build(), + AnalyticsTableColumn.builder() + .name("evlatitude") + .dataType(DOUBLE) + .selectExpression( + "case when 'POINT' = GeometryType(ev.geometry) then ST_Y(ev.geometry) end") + .build()); + } + private List getDataYears(AnalyticsTableUpdateParams params, TrackedEntityType tet) { StringBuilder sql = new StringBuilder(); sql.append( @@ -344,7 +339,8 @@ private List getDataYears(AnalyticsTableUpdateParams params, TrackedEnt "tetId", String.valueOf(tet.getId())))); if (params.getFromDate() != null) { - sql.append(AND + eventDateExpression + ") >= '" + toMediumDate(params.getFromDate()) + "'"); + sql.append( + " and (" + eventDateExpression + ") >= '" + toMediumDate(params.getFromDate()) + "'"); } List availableDataYears = @@ -367,7 +363,7 @@ private List getDataYears(AnalyticsTableUpdateParams params, TrackedEnt private List getColumns() { List columns = new ArrayList<>(); - columns.addAll(FIXED_COLS); + columns.addAll(getFixedCols()); if (sqlBuilder.supportsDeclarativePartitioning()) { columns.add(getPartitionColumn()); @@ -394,7 +390,8 @@ public void populateTable(AnalyticsTableUpdateParams params, AnalyticsTableParti AnalyticsTable masterTable = partition.getMasterTable(); String tableName = partition.getName(); List columns = partition.getMasterTable().getAnalyticsTableColumns(); - String partitionClause = getPartitionClause(partition); + String partitionClause = + sqlBuilder.supportsDeclarativePartitioning() ? "" : getPartitionClause(partition); StringBuilder sql = new StringBuilder("insert into " + tableName + " ("); @@ -453,4 +450,17 @@ private String getPartitionClause(AnalyticsTablePartition partition) { ? latestFilter : emptyIfTrue(partitionFilter, sqlBuilder.supportsDeclarativePartitioning()); } + + /** + * Returns a partition column. + * + * @return an {@link AnalyticsTableColumn}. + */ + private AnalyticsTableColumn getPartitionColumn() { + return AnalyticsTableColumn.builder() + .name("year") + .dataType(INTEGER) + .selectExpression("ev.lastupdated") + .build(); + } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcValidationResultTableManager.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcValidationResultTableManager.java index 8d7ab2d85e0f..e25bd5756486 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcValidationResultTableManager.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/JdbcValidationResultTableManager.java @@ -31,7 +31,6 @@ import static org.hisp.dhis.commons.util.TextUtils.emptyIfTrue; import static org.hisp.dhis.commons.util.TextUtils.format; import static org.hisp.dhis.commons.util.TextUtils.removeLastComma; -import static org.hisp.dhis.commons.util.TextUtils.replace; import static org.hisp.dhis.db.model.DataType.CHARACTER_11; import static org.hisp.dhis.db.model.DataType.DATE; import static org.hisp.dhis.db.model.DataType.INTEGER; @@ -213,9 +212,9 @@ private List getDataYears(AnalyticsTableUpdateParams params) { String fromDateClause = params.getFromDate() == null ? "" - : replace( - "and ps.startdate >= '${fromDate}'", - Map.of("fromDate", DateUtils.toMediumDate(params.getFromDate()))); + : String.format( + " and ps.startdate >= '%s'", DateUtils.toMediumDate(params.getFromDate())); + String sql = replaceQualify( """ @@ -223,8 +222,7 @@ select distinct(extract(year from ps.startdate)) \ from ${validationresult} vrs \ inner join analytics_rs_periodstructure ps on vrs.periodid=ps.periodid \ where ps.startdate is not null \ - and vrs.created < '${startTime}' - ${fromDateClause}""", + and vrs.created < '${startTime}'${fromDateClause}""", Map.of( "startTime", toLongDate(params.getStartTime()), "fromDateClause", fromDateClause)); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/model/AnalyticsColumnType.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/model/AnalyticsDimensionType.java similarity index 97% rename from dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/model/AnalyticsColumnType.java rename to dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/model/AnalyticsDimensionType.java index e00267ed3717..cd54f16e4e60 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/model/AnalyticsColumnType.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/model/AnalyticsDimensionType.java @@ -31,7 +31,7 @@ * Represents a type of dimension, either static, meaning fixed, or dynamic, meaning based on a * dimensional configuration entity. */ -public enum AnalyticsColumnType { +public enum AnalyticsDimensionType { STATIC, DYNAMIC; } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/model/AnalyticsTableColumn.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/model/AnalyticsTableColumn.java index 2f04beb6f675..537b473493e5 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/model/AnalyticsTableColumn.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/model/AnalyticsTableColumn.java @@ -74,7 +74,8 @@ public class AnalyticsTableColumn { @Builder.Default private final List indexColumns = List.of(); /** The column type indicates the column origin. */ - @Builder.Default private final AnalyticsColumnType columnType = AnalyticsColumnType.STATIC; + @Builder.Default + private final AnalyticsDimensionType dimensionType = AnalyticsDimensionType.STATIC; /** Date of creation of the underlying data dimension. */ private final Date created; @@ -104,8 +105,8 @@ public boolean isSkipIndex() { } /** Indicates whether the column type is set to a non-default value. */ - public boolean isStatic() { - return AnalyticsColumnType.STATIC == columnType; + public boolean isStaticDimension() { + return AnalyticsDimensionType.STATIC == dimensionType; } @Override diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/setting/AnalyticsTableSettings.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/setting/AnalyticsTableSettings.java index 749e1173e694..18ea01e64857 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/setting/AnalyticsTableSettings.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/table/setting/AnalyticsTableSettings.java @@ -37,6 +37,8 @@ import static org.hisp.dhis.external.conf.ConfigurationKey.ANALYTICS_TABLE_SKIP_COLUMN; import static org.hisp.dhis.external.conf.ConfigurationKey.ANALYTICS_TABLE_SKIP_INDEX; import static org.hisp.dhis.external.conf.ConfigurationKey.ANALYTICS_TABLE_UNLOGGED; +import static org.hisp.dhis.period.PeriodDataProvider.PeriodSource.DATABASE; +import static org.hisp.dhis.period.PeriodDataProvider.PeriodSource.SYSTEM_DEFINED; import static org.hisp.dhis.util.ObjectUtils.isNull; import com.google.common.collect.Lists; @@ -50,6 +52,7 @@ import org.hisp.dhis.db.model.Database; import org.hisp.dhis.db.model.Logged; import org.hisp.dhis.external.conf.DhisConfigurationProvider; +import org.hisp.dhis.period.PeriodDataProvider.PeriodSource; import org.hisp.dhis.setting.SystemSettings; import org.hisp.dhis.setting.SystemSettingsProvider; import org.springframework.stereotype.Component; @@ -92,6 +95,15 @@ public Integer getMaxPeriodYearsOffset() { return yearsOffset < 0 ? null : yearsOffset; } + /** + * Returns the {@link PeriodSource} based on the max years offset. + * + * @return the {@link PeriodSource}. + */ + public PeriodSource getPeriodSource() { + return getMaxPeriodYearsOffset() == null ? SYSTEM_DEFINED : DATABASE; + } + /** * Indicates whether an analytics database instance is configured. * diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/trackedentity/DefaultTrackedEntityAnalyticsDimensionsService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/trackedentity/DefaultTrackedEntityAnalyticsDimensionsService.java index 2e2d0e64c819..c71c5441c46e 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/trackedentity/DefaultTrackedEntityAnalyticsDimensionsService.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/trackedentity/DefaultTrackedEntityAnalyticsDimensionsService.java @@ -52,10 +52,10 @@ class DefaultTrackedEntityAnalyticsDimensionsService implements TrackedEntityAnalyticsDimensionsService { private final TrackedEntityTypeService trackedEntityTypeService; - private final EnrollmentAnalyticsDimensionsService enrollmentAnalyticsDimensionsService; - private final ProgramService programService; + private final EnrollmentAnalyticsDimensionsService enrollmentAnalyticsDimensionsService; + @Override public List getQueryDimensionsByTrackedEntityTypeId( String trackedEntityTypeId, Set programUids) { diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/AnalyticsIndexHelper.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/AnalyticsIndexHelper.java index 07d05742ff2e..6d984a71c2b4 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/AnalyticsIndexHelper.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/AnalyticsIndexHelper.java @@ -154,7 +154,7 @@ private static void maybeAddTextLowerIndex( boolean isSingleColumn = indexColumns.size() == 1; if (column.getDataType() == TEXT - && !column.isStatic() + && !column.isStaticDimension() && isValidUid(columnName) && isSingleColumn) { String name = indexName + "_lower"; @@ -178,7 +178,7 @@ private static void maybeAddDateSortOrderIndex( boolean isSingleColumn = indexColumns.size() == 1; - if (column.getDataType() == TIMESTAMP && column.isStatic() && isSingleColumn) { + if (column.getDataType() == TIMESTAMP && column.isStaticDimension() && isSingleColumn) { indexes.add( Index.builder() .name(indexName + "_desc") diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/AnalyticsPeriodCriteriaUtils.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/AnalyticsPeriodCriteriaUtils.java index c45ca97052ea..5fdd51b098fb 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/AnalyticsPeriodCriteriaUtils.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/AnalyticsPeriodCriteriaUtils.java @@ -50,12 +50,12 @@ public final class AnalyticsPeriodCriteriaUtils { * * @param criteria {@link EventsAnalyticsQueryCriteria} query criteria. * @param periodDataProvider {@link EventsAnalyticsQueryCriteria} period data provider. - * @param dataSource {@link PeriodDataProvider.DataSource} source of data. + * @param dataSource {@link PeriodDataProvider.PeriodSource} source of data. */ public static void defineDefaultPeriodForCriteria( EnrollmentAnalyticsQueryCriteria criteria, PeriodDataProvider periodDataProvider, - PeriodDataProvider.DataSource dataSource) { + PeriodDataProvider.PeriodSource dataSource) { List availableYears = periodDataProvider.getAvailableYears(dataSource); if (PeriodCriteriaUtils.hasPeriod(criteria)) { diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/DisplayNameUtils.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/DisplayNameUtils.java index 8e0db07a1880..5377f0807253 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/DisplayNameUtils.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/analytics/util/DisplayNameUtils.java @@ -145,7 +145,8 @@ public static String getDisplayName( private static String extractJsonValue( SqlBuilder sqlBuilder, String tablePrefix, String originColumn, String path) { - String jsonExtracted = sqlBuilder.jsonExtract(tablePrefix, originColumn, path); + String json = tablePrefix + "." + originColumn; + String jsonExtracted = sqlBuilder.jsonExtract(json, path); return sqlBuilder.trim(jsonExtracted); } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/AnalyticsSqlBuilder.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/AnalyticsSqlBuilder.java new file mode 100644 index 000000000000..e8e97f5324c2 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/AnalyticsSqlBuilder.java @@ -0,0 +1,41 @@ +/* + * 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.db.sql; + +/** + * Interface for resolving specific SQL queries for analytics, that requires custom logic that can't + * be resolved by the default SqlBuilder implementations. + */ +public interface AnalyticsSqlBuilder { + /** + * Returns the correct SQL based on the underlying database for fetching the event data values. + * + * @return a SQL snippet. + */ + String getEventDataValues(); +} diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/AnalyticsSqlBuilderProvider.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/AnalyticsSqlBuilderProvider.java new file mode 100644 index 000000000000..3f48b59a8cca --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/AnalyticsSqlBuilderProvider.java @@ -0,0 +1,72 @@ +/* + * 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.db.sql; + +import java.util.Objects; +import org.hisp.dhis.analytics.table.setting.AnalyticsTableSettings; +import org.hisp.dhis.db.model.Database; +import org.hisp.dhis.external.conf.DhisConfigurationProvider; +import org.springframework.stereotype.Service; + +/** Provider of {@link AnalyticsSqlBuilder} implementations. */ +@Service +public class AnalyticsSqlBuilderProvider { + private final AnalyticsSqlBuilder analyticsSqlBuilder; + + public AnalyticsSqlBuilderProvider(AnalyticsTableSettings config) { + Objects.requireNonNull(config); + this.analyticsSqlBuilder = getSqlBuilder(config); + } + + /** + * Returns a {@link AnalyticsSqlBuilder} implementation based on the system configuration. + * + * @return a {@link AnalyticsSqlBuilder}. + */ + public AnalyticsSqlBuilder getAnalyticsSqlBuilder() { + return analyticsSqlBuilder; + } + + /** + * Returns the appropriate {@link AnalyticsSqlBuilder} implementation based on the system + * configuration. + * + * @param config the {@link DhisConfigurationProvider}. + * @return a {@link AnalyticsSqlBuilder}. + */ + private AnalyticsSqlBuilder getSqlBuilder(AnalyticsTableSettings config) { + Database database = config.getAnalyticsDatabase(); + Objects.requireNonNull(database); + + return switch (database) { + case DORIS -> new DorisAnalyticsSqlBuilder(); + case CLICKHOUSE -> new ClickhouseAnalyticsSqlBuilder(); + default -> new PostgresAnalyticsSqlBuilder(); + }; + } +} diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilder.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilder.java index 39ccc53b84d4..42938d1403d8 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilder.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilder.java @@ -198,8 +198,8 @@ public String differenceInSeconds(String columnA, String columnB) { } @Override - public String regexpMatch(String pattern) { - return String.format("match %s", pattern); // TO DO + public String regexpMatch(String value, String pattern) { + return String.format("match(%s, %s)", value, pattern); } @Override @@ -218,19 +218,14 @@ public String coalesce(String expression, String defaultValue) { } @Override - public String jsonExtract(String column, String property) { - return "JSONExtractRaw(" + column + ", '" + property + "')"; + public String jsonExtract(String json, String property) { + return String.format("JSONExtractString(%s, '%s')", json, property); } @Override - public String jsonExtract(String tablePrefix, String column, String jsonPath) { - return String.format("JSONExtractRaw(%s.%s, '%s')", tablePrefix, column, jsonPath); - } - - @Override - public String jsonExtractNested(String column, String... jsonPath) { - String path = String.join(".", jsonPath); - return String.format("JSONExtract(%s, '%s')", column, path); + public String jsonExtractNested(String json, String... expression) { + String path = String.join(".", expression); + return String.format("JSONExtractString(%s, '%s')", json, path); } // Statements diff --git a/dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/filter/UserRoleCanIssueFilter.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/ClickhouseAnalyticsSqlBuilder.java similarity index 67% rename from dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/filter/UserRoleCanIssueFilter.java rename to dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/ClickhouseAnalyticsSqlBuilder.java index d9cab6377453..c5830ebd225d 100644 --- a/dhis-2/dhis-support/dhis-support-system/src/main/java/org/hisp/dhis/system/filter/UserRoleCanIssueFilter.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/ClickhouseAnalyticsSqlBuilder.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,31 +25,11 @@ * (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; - -import org.hisp.dhis.commons.filter.Filter; -import org.hisp.dhis.user.User; -import org.hisp.dhis.user.UserRole; - -/** - * @author Lars Helge Overland - */ -public class UserRoleCanIssueFilter implements Filter { - private User user; - - private boolean canGrantOwnUserRoles = false; - - protected UserRoleCanIssueFilter() {} - - public UserRoleCanIssueFilter(User user, boolean canGrantOwnUserRoles) { - if (user != null) { - this.user = user; - this.canGrantOwnUserRoles = canGrantOwnUserRoles; - } - } +package org.hisp.dhis.db.sql; +public class ClickhouseAnalyticsSqlBuilder implements AnalyticsSqlBuilder { @Override - public boolean retain(UserRole group) { - return user != null && user.canIssueUserRole(group, canGrantOwnUserRoles); + public String getEventDataValues() { + return "ev.eventdatavalues"; } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/DorisAnalyticsSqlBuilder.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/DorisAnalyticsSqlBuilder.java new file mode 100644 index 000000000000..9abb338b5577 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/DorisAnalyticsSqlBuilder.java @@ -0,0 +1,35 @@ +/* + * 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.db.sql; + +public class DorisAnalyticsSqlBuilder implements AnalyticsSqlBuilder { + @Override + public String getEventDataValues() { + return "ev.eventdatavalues"; + } +} diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/DorisSqlBuilder.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/DorisSqlBuilder.java index 48421330ce28..920ca25d1e8e 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/DorisSqlBuilder.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/DorisSqlBuilder.java @@ -201,8 +201,8 @@ public String differenceInSeconds(String columnA, String columnB) { } @Override - public String regexpMatch(String pattern) { - return String.format("regexp %s", pattern); + public String regexpMatch(String value, String pattern) { + return String.format("%s regexp %s", value, pattern); } @Override @@ -221,20 +221,14 @@ public String coalesce(String expression, String defaultValue) { } @Override - public String jsonExtract(String column, String property) { - return "json_unquote(json_extract(" + column + ", '$." + property + "'))"; + public String jsonExtract(String json, String property) { + return String.format("json_unquote(json_extract(%s, '$.%s'))", json, property); } @Override - public String jsonExtract(String tablePrefix, String column, String jsonPath) { - return String.format( - "json_unquote(json_extract(%s.%s, '$.%s'))", tablePrefix, column, jsonPath); - } - - @Override - public String jsonExtractNested(String column, String... jsonPath) { - String path = "$." + String.join(".", jsonPath); - return String.format("JSON_UNQUOTE(JSON_EXTRACT(%s, '%s'))", column, path); + public String jsonExtractNested(String column, String... expression) { + String path = "$." + String.join(".", expression); + return String.format("json_unquote(json_extract(%s, '%s'))", column, path); } // Statements diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/PostgreSqlBuilder.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/PostgreSqlBuilder.java index 456761ebd9b3..aebd74100e11 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/PostgreSqlBuilder.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/PostgreSqlBuilder.java @@ -220,8 +220,8 @@ public String differenceInSeconds(String columnA, String columnB) { } @Override - public String regexpMatch(String pattern) { - return String.format("~* %s", pattern); + public String regexpMatch(String value, String pattern) { + return String.format("%s ~* %s", value, pattern); } @Override @@ -241,17 +241,12 @@ public String coalesce(String expression, String defaultValue) { @Override public String jsonExtract(String column, String property) { - return column + " ->> '" + property + "'"; + return String.format("%s ->> '%s'", column, property); } @Override - public String jsonExtract(String tablePrefix, String column, String jsonPath) { - return String.format("%s.%s ->> '%s'", tablePrefix, column, jsonPath); - } - - @Override - public String jsonExtractNested(String column, String... jsonPath) { - return String.format("%s #>> '{%s}'", column, String.join(", ", jsonPath)); + public String jsonExtractNested(String column, String... expression) { + return String.format("%s #>> '{%s}'", column, String.join(", ", expression)); } // Statements diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/PostgresAnalyticsSqlBuilder.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/PostgresAnalyticsSqlBuilder.java new file mode 100644 index 000000000000..1e1e5dd5fbea --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/PostgresAnalyticsSqlBuilder.java @@ -0,0 +1,67 @@ +/* + * 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.db.sql; + +public class PostgresAnalyticsSqlBuilder implements AnalyticsSqlBuilder { + + /** + * Returns a subquery that expand the event datavalue jsonb with two additional fields: + * + *
    + *
  • value_name: the name of the organisation unit that the datavalue is associated with + *
  • value_code: the code of the organisation unit that the datavalue is associated with + *
+ * + * @return a SQL subquery. + */ + @Override + public String getEventDataValues() { + return """ + (select json_object_agg(l2.keys, l2.datavalue) as value + from ( + select l1.uid, + l1.keys, + json_strip_nulls(json_build_object( + 'value', l1.eventdatavalues -> l1.keys ->> 'value', + 'created', l1.eventdatavalues -> l1.keys ->> 'created', + 'storedBy', l1.eventdatavalues -> l1.keys ->> 'storedBy', + 'lastUpdated', l1.eventdatavalues -> l1.keys ->> 'lastUpdated', + 'providedElsewhere', l1.eventdatavalues -> l1.keys -> 'providedElsewhere', + 'value_name', (select ou.name + from organisationunit ou + where ou.uid = l1.eventdatavalues -> l1.keys ->> 'value'), + 'value_code', (select ou.code + from organisationunit ou + where ou.uid = l1.eventdatavalues -> l1.keys ->> 'value'))) as datavalue + from (select inner_evt.*, jsonb_object_keys(inner_evt.eventdatavalues) keys + from event inner_evt) as l1) as l2 + where l2.uid = ev.uid + group by l2.uid)::jsonb + """; + } +} diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/SqlBuilder.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/SqlBuilder.java index 6cc1d4d95ce5..8655372ff807 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/SqlBuilder.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/db/sql/SqlBuilder.java @@ -237,24 +237,25 @@ public interface SqlBuilder { String differenceInSeconds(String columnA, String columnB); /** - * @param pattern the regular expression pattern to match against - * @return a regular expression matching clause. + * @param value the string value such as a column or expression. + * @param pattern the regular expression pattern to match against. + * @return a regular expression string matching clause. */ - String regexpMatch(String pattern); + String regexpMatch(String value, String pattern); /** * Creates a SQL concatenation function that combines multiple columns or expressions. * - * @param columns the column names or expressions to concatenate - * @return the SQL function for concatenation + * @param columns the column names or expressions to concatenate. + * @return the SQL function for concatenation. */ String concat(String... columns); /** * Creates a SQL trim function that removes leading and trailing spaces from an expression. * - * @param expression the expression to trim - * @return the SQL function for trimming + * @param expression the expression to trim. + * @return the SQL function for trimming. */ String trim(String expression); @@ -262,40 +263,29 @@ public interface SqlBuilder { * Creates a SQL COALESCE function that returns the first non-null expression from the provided * expressions. If the first expression is null, it returns the default expression. * - * @param expression the expression to check for null - * @param defaultValue the value to return if the first expression is null - * @return the SQL function for coalescing + * @param expression the expression to check for null. + * @param defaultValue the value to return if the first expression is null. + * @return the SQL function for coalescing. */ String coalesce(String expression, String defaultValue); /** * Extracts a value from a JSON column using a specified property path. * - * @param column the JSON column name to extract from - * @param property the JSON property path to extract - * @return the SQL function for JSON value extraction + * @param json the JSON column name or value to extract from. + * @param property the JSON property path to extract. + * @return the SQL function for JSON value extraction. */ - String jsonExtract(String column, String property); - - /** - * Extracts a value from a JSON column using a specified JSON path expression, with support for - * table prefix qualification. - * - * @param tablePrefix the prefix/alias of the table containing the JSON column - * @param column the JSON column name to extract from - * @param jsonPath the JSON path expression to extract the value - * @return the SQL function for JSON value extraction - */ - String jsonExtract(String tablePrefix, String column, String jsonPath); + String jsonExtract(String json, String property); /** * Extracts a nested value from a JSON column. * - * @param column the name of the JSON column from which to extract the value. - * @param jsonPath the hierarchical path to the nested value, represented as a sequence of keys. + * @param json the JSON column name or value to extract from. + * @param expression the hierarchical path expression to the nested value. * @return a SQL expression to extract the specified nested value from the JSON column. */ - String jsonExtractNested(String column, String... jsonPath); + String jsonExtractNested(String json, String... expression); // Statements diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/resourcetable/DefaultResourceTableService.java b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/resourcetable/DefaultResourceTableService.java index 00106a5732b6..8f01d9ac7fad 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/resourcetable/DefaultResourceTableService.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/main/java/org/hisp/dhis/resourcetable/DefaultResourceTableService.java @@ -29,8 +29,6 @@ import static java.time.temporal.ChronoUnit.YEARS; import static java.util.Comparator.reverseOrder; -import static org.hisp.dhis.period.PeriodDataProvider.DataSource.DATABASE; -import static org.hisp.dhis.period.PeriodDataProvider.DataSource.SYSTEM_DEFINED; import static org.hisp.dhis.scheduling.JobProgress.FailurePolicy.SKIP_ITEM; import com.google.common.collect.Lists; @@ -185,8 +183,7 @@ private final List getApprovalResourceTables() { */ List getAndValidateAvailableDataYears() { List availableYears = - periodDataProvider.getAvailableYears( - analyticsTableSettings.getMaxPeriodYearsOffset() == null ? SYSTEM_DEFINED : DATABASE); + periodDataProvider.getAvailableYears(analyticsTableSettings.getPeriodSource()); validateYearsOffset(availableYears); return availableYears; } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/common/params/dimension/DimensionIdentifierHelperTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/common/params/dimension/DimensionIdentifierHelperTest.java index f4c5f57ce8b1..e45a6728764d 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/common/params/dimension/DimensionIdentifierHelperTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/common/params/dimension/DimensionIdentifierHelperTest.java @@ -35,17 +35,13 @@ import org.hisp.dhis.common.IllegalQueryException; import org.junit.jupiter.api.Test; -/** Unit tests for {@link DimensionIdentifierHelper} */ class DimensionIdentifierHelperTest { @Test void testFromFullDimensionId() { - // Given String fullDimensionId = "lxAQ7Zs9VYR[0].RaMbOrTys0n[4].bh1Edk21e2n"; - // When StringDimensionIdentifier dimensionIdentifier = fromFullDimensionId(fullDimensionId); - // Then assertEquals( "bh1Edk21e2n", dimensionIdentifier.getDimension().getUid(), @@ -64,12 +60,9 @@ void testFromFullDimensionId() { @Test void testFromDimensionIdWithUnsupportedOffset() { - // Given String dimensionIdWithDashOffset = "lxAQ7Zs9VYR[-].bh1Edk21e2n"; String dimensionIdWithStringOffset = "lxAQ7Zs9VYR[X].bh1Edk21e2n"; - // When - // Then assertThrows(IllegalQueryException.class, () -> fromFullDimensionId(dimensionIdWithDashOffset)); assertThrows( IllegalQueryException.class, () -> fromFullDimensionId(dimensionIdWithStringOffset)); @@ -77,13 +70,10 @@ void testFromDimensionIdWithUnsupportedOffset() { @Test void testFromFullDimensionIdWithSingleDimension() { - // Given String singleDimensionId = "bh1Edk21e2n"; - // When StringDimensionIdentifier dimensionIdentifier = fromFullDimensionId(singleDimensionId); - // Then assertEquals( "bh1Edk21e2n", dimensionIdentifier.getDimension().getUid(), @@ -96,13 +86,10 @@ void testFromFullDimensionIdWithSingleDimension() { @Test void testFromFullDimensionIdWithProgramAndDimension() { - // Given String singleDimensionId = "lxAQ7Zs9VYR.bh1Edk21e2n"; - // When StringDimensionIdentifier dimensionIdentifier = fromFullDimensionId(singleDimensionId); - // Then assertEquals( "bh1Edk21e2n", dimensionIdentifier.getDimension().getUid(), @@ -117,15 +104,12 @@ void testFromFullDimensionIdWithProgramAndDimension() { @Test void testFromFullDimensionIdWhenSingleDimensionHasOffset() { - // Given String singleDimensionWithOffset = "bh1Edk21e2n[2]"; - // When IllegalArgumentException thrown = assertThrows( IllegalArgumentException.class, () -> fromFullDimensionId(singleDimensionWithOffset)); - // Then assertEquals( "Only program and program stage can have offset", thrown.getMessage(), @@ -134,15 +118,12 @@ void testFromFullDimensionIdWhenSingleDimensionHasOffset() { @Test void testFromFullDimensionIdWhenDimensionHasInvalidFormat() { - // Given String invalidFullDimensionId = "lxAQ7Zs9VYR[1].RaMbOrTys0n[4].bh1Edk21e2n.invalid-id"; - // When IllegalArgumentException thrown = assertThrows( IllegalArgumentException.class, () -> fromFullDimensionId(invalidFullDimensionId)); - // Then assertEquals( "Invalid dimension identifier: " + invalidFullDimensionId, thrown.getMessage(), diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/common/processing/CommonRequestParamsMapperTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/common/processing/CommonRequestParamsMapperTest.java index a2657a931d0e..4b1e883c9dae 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/common/processing/CommonRequestParamsMapperTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/common/processing/CommonRequestParamsMapperTest.java @@ -435,8 +435,7 @@ void mapWhenProgramsCannotBeFound() { programService, dimensionIdentifierConverter); - // List has only one Program, but the CommonQueryRequest, below, has - // two. + // List has only one Program, but the CommonQueryRequest, has two List programs = List.of(program1); CommonRequestParams commonRequestParams = diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/common/query/NotConditionRendererTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/common/query/NotConditionRendererTest.java index 20de5f79ff9c..b3ab91830786 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/common/query/NotConditionRendererTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/common/query/NotConditionRendererTest.java @@ -32,7 +32,6 @@ import org.junit.jupiter.api.Test; class NotConditionRendererTest { - @Test void testInWithSingleValueProduceCorrectSql() { Renderable renderable = () -> "test"; diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/AnalyticsManagerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/AnalyticsManagerTest.java index 42bf5627e0b5..e4c6b410c5e3 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/AnalyticsManagerTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/AnalyticsManagerTest.java @@ -59,13 +59,14 @@ import org.hisp.dhis.period.Period; import org.hisp.dhis.period.YearlyPeriodType; import org.hisp.dhis.test.TestBase; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.jdbc.core.JdbcTemplate; @@ -80,9 +81,9 @@ class AnalyticsManagerTest extends TestBase { @Mock private ExecutionPlanStore executionPlanStore; - private final SqlBuilder sqlBuilder = new PostgreSqlBuilder(); + @Spy private final SqlBuilder sqlBuilder = new PostgreSqlBuilder(); - private JdbcAnalyticsManager analyticsManager; + @InjectMocks private JdbcAnalyticsManager analyticsManager; private static Stream data() { return Stream.of( @@ -92,12 +93,6 @@ private static Stream data() { arguments("2017Nov", 26.5D)); } - @BeforeEach - void before() { - analyticsManager = - new JdbcAnalyticsManager(queryPlanner, jdbcTemplate, executionPlanStore, sqlBuilder); - } - @ParameterizedTest @MethodSource("data") public void testWeightedAverage(String financialYear, Double weightedAverage) { diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/AnalyticsServiceReportingRateTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/AnalyticsServiceReportingRateTest.java index 12f852c8e464..975fd51f7844 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/AnalyticsServiceReportingRateTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/AnalyticsServiceReportingRateTest.java @@ -102,7 +102,7 @@ void verifyReportingRatesValueWhenPeriodIsFilter() { DataQueryParams.newBuilder() .withOrganisationUnit(ou) // DATA ELEMENTS - .withDataElements(newArrayList(reportingRateA, reportingRateB, reportingRateC)) + .withDataElements(List.of(reportingRateA, reportingRateB, reportingRateC)) .withIgnoreLimit(true) // FILTERS (OU) .withFilters(List.of(new BaseDimensionalObject("pe", DimensionType.PERIOD, periods))) diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DataQueryServiceDimensionItemKeywordTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DataQueryServiceDimensionItemKeywordTest.java index e0ab2257f7c1..622f2b45975f 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DataQueryServiceDimensionItemKeywordTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DataQueryServiceDimensionItemKeywordTest.java @@ -78,6 +78,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; @@ -103,14 +104,6 @@ class DataQueryServiceDimensionItemKeywordTest { OrganisationUnit.class, Set.of("geometry", "parent", "groups", "children"))); - private DimensionalObjectProducer dimensionalObjectProducer; - - private RequestBuilder rb; - - private OrganisationUnit rootOu; - - private DefaultDataQueryService target; - @Mock private IdentifiableObjectManager idObjectManager; @Mock private OrganisationUnitService organisationUnitService; @@ -129,18 +122,18 @@ class DataQueryServiceDimensionItemKeywordTest { @Mock private I18n i18n; + @InjectMocks private DimensionalObjectProvider dimensionalObjectProducer; + + private DefaultDataQueryService target; + + private RequestBuilder rb; + + private OrganisationUnit rootOu; + @BeforeEach public void setUp() { lenient().when(settingsService.getCurrentSettings()).thenReturn(SystemSettings.of(Map.of())); - dimensionalObjectProducer = - new DimensionalObjectProducer( - idObjectManager, - organisationUnitService, - settingsService, - i18nManager, - dimensionService, - aclService); target = new DefaultDataQueryService(dimensionalObjectProducer, idObjectManager, securityManager); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DataQueryServiceTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DataQueryServiceTest.java index d798cfb0fe63..11e9e08e6ee8 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DataQueryServiceTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DataQueryServiceTest.java @@ -39,41 +39,40 @@ import java.util.Set; import org.hisp.dhis.analytics.AnalyticsSecurityManager; import org.hisp.dhis.analytics.DataQueryParams; -import org.hisp.dhis.analytics.DataQueryService; import org.hisp.dhis.common.BaseIdentifiableObject; import org.hisp.dhis.common.IdentifiableObjectManager; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.user.User; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -/** Unit tests for {@link DataQueryService}. */ @ExtendWith(MockitoExtension.class) class DataQueryServiceTest { + @Mock private DimensionalObjectProvider dimensionalObjectProducer; + + @Mock private IdentifiableObjectManager idObjectManager; + + @Mock private AnalyticsSecurityManager analyticsSecurityManager; + + @InjectMocks private DefaultDataQueryService dataQueryService; + @Test void testGetUserOrgUnitsWithExplicitlyDefinedAnalyticsOrganisationUnits() { - // given OrganisationUnit ouB = createOrganisationUnit('B'); OrganisationUnit ouC = createOrganisationUnit('C'); OrganisationUnit ouD = createOrganisationUnit('D'); DataQueryParams dataQueryParams = DataQueryParams.newBuilder().withUserOrgUnitType(DATA_OUTPUT).build(); User currentUser = mock(User.class); - AnalyticsSecurityManager analyticsSecurityManager = mock(AnalyticsSecurityManager.class); when(currentUser.getDataViewOrganisationUnits()).thenReturn(Set.of(ouB, ouC, ouD)); when(analyticsSecurityManager.getCurrentUser(dataQueryParams)).thenReturn(currentUser); - DataQueryService dataQueryService = - new DefaultDataQueryService( - mock(DimensionalObjectProducer.class), - mock(IdentifiableObjectManager.class), - analyticsSecurityManager); - // when List userOrgUnits = dataQueryService.getUserOrgUnits(dataQueryParams, null); - // then assertEquals(3, userOrgUnits.size()); assertThat( userOrgUnits.stream().map(BaseIdentifiableObject::getName).toList(), @@ -82,25 +81,16 @@ void testGetUserOrgUnitsWithExplicitlyDefinedAnalyticsOrganisationUnits() { @Test void testGetUserOrgUnitsWithNoAnalyticsOrganisationUnitsDefined() { - // given OrganisationUnit ouA = createOrganisationUnit('A'); DataQueryParams dataQueryParams = DataQueryParams.newBuilder().withUserOrgUnitType(DATA_OUTPUT).build(); User currentUser = mock(User.class); - AnalyticsSecurityManager analyticsSecurityManager = mock(AnalyticsSecurityManager.class); when(currentUser.getOrganisationUnits()).thenReturn(Set.of(ouA)); when(currentUser.getDataViewOrganisationUnits()).thenReturn(Set.of()); when(analyticsSecurityManager.getCurrentUser(dataQueryParams)).thenReturn(currentUser); - DataQueryService dataQueryService = - new DefaultDataQueryService( - mock(DimensionalObjectProducer.class), - mock(IdentifiableObjectManager.class), - analyticsSecurityManager); - // when List userOrgUnits = dataQueryService.getUserOrgUnits(dataQueryParams, null); - // then assertEquals(1, userOrgUnits.size()); assertThat( userOrgUnits.stream().map(BaseIdentifiableObject::getName).toList(), @@ -109,24 +99,11 @@ void testGetUserOrgUnitsWithNoAnalyticsOrganisationUnitsDefined() { @Test void testGetUserOrgUnitsWithNoneOrganisationUnitDefined() { - // given DataQueryParams dataQueryParams = DataQueryParams.newBuilder().withUserOrgUnitType(DATA_OUTPUT).build(); - User currentUser = mock(User.class); - AnalyticsSecurityManager analyticsSecurityManager = mock(AnalyticsSecurityManager.class); - when(currentUser.getOrganisationUnits()).thenReturn(Set.of()); - when(currentUser.getDataViewOrganisationUnits()).thenReturn(Set.of()); - when(analyticsSecurityManager.getCurrentUser(dataQueryParams)).thenReturn(currentUser); - DataQueryService dataQueryService = - new DefaultDataQueryService( - mock(DimensionalObjectProducer.class), - mock(IdentifiableObjectManager.class), - analyticsSecurityManager); - // when List userOrgUnits = dataQueryService.getUserOrgUnits(dataQueryParams, null); - // then assertEquals(0, userOrgUnits.size()); } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DimensionalObjectProducerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DimensionalObjectProviderTest.java similarity index 98% rename from dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DimensionalObjectProducerTest.java rename to dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DimensionalObjectProviderTest.java index cabae51fb2e9..d0277d6514a5 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DimensionalObjectProducerTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/DimensionalObjectProviderTest.java @@ -105,17 +105,17 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; /** - * Unit tests for {@link DimensionalObjectProducer}. + * Unit tests for {@link DimensionalObjectProvider}. * * @author maikel arabori */ @ExtendWith(MockitoExtension.class) -class DimensionalObjectProducerTest { - private DimensionalObjectProducer target; +class DimensionalObjectProviderTest { @Mock private IdentifiableObjectManager idObjectManager; @@ -124,6 +124,7 @@ class DimensionalObjectProducerTest { @Mock private DimensionService dimensionService; @Mock private SystemSettingsProvider settingsProvider; + @Mock private SystemSettings settings; @Mock private AclService aclService; @@ -134,17 +135,11 @@ class DimensionalObjectProducerTest { @Mock private I18nFormat i18nFormat; + @InjectMocks private DimensionalObjectProvider target; + @BeforeEach public void setUp() { lenient().when(settingsProvider.getCurrentSettings()).thenReturn(settings); - target = - new DimensionalObjectProducer( - idObjectManager, - organisationUnitService, - settingsProvider, - i18nManager, - dimensionService, - aclService); } @Test diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/JdbcAnalyticsManagerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/JdbcAnalyticsManagerTest.java index 75893f264588..37af80ed2508 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/JdbcAnalyticsManagerTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/JdbcAnalyticsManagerTest.java @@ -57,12 +57,13 @@ import org.hisp.dhis.period.Period; import org.hisp.dhis.period.PeriodType; import org.hisp.dhis.setting.SystemSettingsService; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; +import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.support.rowset.SqlRowSet; @@ -82,20 +83,15 @@ class JdbcAnalyticsManagerTest { @Mock private NestedIndicatorCyclicDependencyInspector nestedIndicatorCyclicDependencyInspector; - private final SqlBuilder sqlBuilder = new PostgreSqlBuilder(); + @Mock private QueryPlanner queryPlanner; - @Captor private ArgumentCaptor sql; + @Spy private SqlBuilder sqlBuilder = new PostgreSqlBuilder(); - private JdbcAnalyticsManager subject; + @Captor private ArgumentCaptor sql; @Mock private ExecutionPlanStore executionPlanStore; - @BeforeEach - public void setUp() { - QueryPlanner queryPlanner = new DefaultQueryPlanner(partitionManager); - - subject = new JdbcAnalyticsManager(queryPlanner, jdbcTemplate, executionPlanStore, sqlBuilder); - } + @InjectMocks private JdbcAnalyticsManager subject; @Test void verifyQueryGeneratedWhenDataElementHasLastAggregationType() { diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/JdbcSubexpressionQueryGeneratorTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/JdbcSubexpressionQueryGeneratorTest.java index 2c954f291ab1..f7f4c363b727 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/JdbcSubexpressionQueryGeneratorTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/JdbcSubexpressionQueryGeneratorTest.java @@ -60,11 +60,12 @@ import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.period.Period; import org.hisp.dhis.subexpression.SubexpressionDimensionItem; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.jdbc.core.JdbcTemplate; @@ -80,20 +81,15 @@ class JdbcSubexpressionQueryGeneratorTest { @Mock private ExecutionPlanStore executionPlanStore; - private final SqlBuilder sqlBuilder = new PostgreSqlBuilder(); + @Mock private QueryPlanner queryPlanner; - private JdbcAnalyticsManager jam; + @Spy private SqlBuilder sqlBuilder = new PostgreSqlBuilder(); + + @InjectMocks private JdbcAnalyticsManager manager; /** Matches a UID with an initial single quote. */ private static final Pattern QUOTED_UID = Pattern.compile("'\\w{11}'"); - @BeforeAll - public void setUp() { - QueryPlanner queryPlanner = new DefaultQueryPlanner(partitionManager); - - jam = new JdbcAnalyticsManager(queryPlanner, jdbcTemplate, executionPlanStore, sqlBuilder); - } - @Test void testGetSql() { OrganisationUnit ouA = createOrganisationUnit('A'); @@ -147,7 +143,7 @@ ORGUNIT_DIM_ID, DimensionType.ORGANISATION_UNIT, getList(ouA))) .build(); JdbcSubexpressionQueryGenerator target = - new JdbcSubexpressionQueryGenerator(jam, params, DATA_VALUE); + new JdbcSubexpressionQueryGenerator(manager, params, DATA_VALUE); String expected = "select ax.\"pe\",'subexprxUID' as \"dx\"," diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/OrgUnitTableJoinerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/OrgUnitTableJoinerTest.java index f9053ebcac55..0ad45adc592f 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/OrgUnitTableJoinerTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/OrgUnitTableJoinerTest.java @@ -55,29 +55,29 @@ * @author Jim Grace */ class OrgUnitTableJoinerTest extends TestBase { - private static final OrgUnitField DEFAULT = new OrgUnitField(null); + private final OrgUnitField DEFAULT = new OrgUnitField(null); - private static final OrgUnitField ATTRIBUTE = new OrgUnitField("AttributeId"); + private final OrgUnitField ATTRIBUTE = new OrgUnitField("AttributeId"); - private static final OrgUnitField OWNER_AT_START = new OrgUnitField("OWNER_AT_START"); + private final OrgUnitField OWNER_AT_START = new OrgUnitField("OWNER_AT_START"); - private static final OrgUnitField OWNER_AT_END = new OrgUnitField("OWNER_AT_END"); + private final OrgUnitField OWNER_AT_END = new OrgUnitField("OWNER_AT_END"); - private static final Program programA = createProgram('A'); + private final Program programA = createProgram('A'); - private static final Period periodDaily = PeriodType.getPeriodFromIsoString("20230101"); + private final Period periodDaily = PeriodType.getPeriodFromIsoString("20230101"); - private static final Period periodMonthly = PeriodType.getPeriodFromIsoString("202201"); + private final Period periodMonthly = PeriodType.getPeriodFromIsoString("202201"); - private static final Period periodQuarterly = PeriodType.getPeriodFromIsoString("2022Q1"); + private final Period periodQuarterly = PeriodType.getPeriodFromIsoString("2022Q1"); - private static final Date dateA = new GregorianCalendar(2022, JANUARY, 1).getTime(); + private final Date dateA = new GregorianCalendar(2022, JANUARY, 1).getTime(); - private static final Date dateB = new GregorianCalendar(2023, JANUARY, 1).getTime(); + private final Date dateB = new GregorianCalendar(2023, JANUARY, 1).getTime(); - private static final DimensionalItemObject ouA = createOrganisationUnit('A'); + private final DimensionalItemObject ouA = createOrganisationUnit('A'); - private static final DimensionalObject ouGroupSetA = + private final DimensionalObject ouGroupSetA = new BaseDimensionalObject( "OrgUnitGrSe", DimensionType.ORGANISATION_UNIT_GROUP_SET, emptyList()); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/QueryItemHelperTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/QueryItemHelperTest.java index 1ff002351e6a..370a5198df97 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/QueryItemHelperTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/QueryItemHelperTest.java @@ -60,21 +60,21 @@ * @author Dusan Bernat */ class QueryItemHelperTest extends TestBase { - private String UID_A = CodeGenerator.generateUid(); + private final String UID_A = CodeGenerator.generateUid(); - private String UID_B = CodeGenerator.generateUid(); + private final String UID_B = CodeGenerator.generateUid(); - private String OPTION_NAME_A = "OptionA"; + private final String OPTION_NAME_A = "OptionA"; - private String OPTION_NAME_B = "OptionB"; + private final String OPTION_NAME_B = "OptionB"; - private String LEGEND_NAME_A = "LegendA"; + private final String LEGEND_NAME_A = "LegendA"; - private String LEGEND_NAME_B = "LegendB"; + private final String LEGEND_NAME_B = "LegendB"; - private String LEGEND_CODE_A = "LegendCodeA"; + private final String LEGEND_CODE_A = "LegendCodeA"; - private String LEGEND_CODE_B = "LegendCodeB"; + private final String LEGEND_CODE_B = "LegendCodeB"; @Test void testGeItemOptionValueWithIdSchemeNAME() { diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/QueryPlannerGroupByAggregationTypeTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/QueryPlannerGroupByAggregationTypeTest.java index 5b1731e89464..3cc54c5d8a81 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/QueryPlannerGroupByAggregationTypeTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/QueryPlannerGroupByAggregationTypeTest.java @@ -49,7 +49,6 @@ import org.hisp.dhis.analytics.DataQueryGroups; import org.hisp.dhis.analytics.DataQueryParams; import org.hisp.dhis.analytics.DataType; -import org.hisp.dhis.analytics.QueryPlanner; import org.hisp.dhis.analytics.QueryPlannerParams; import org.hisp.dhis.analytics.partition.PartitionManager; import org.hisp.dhis.category.CategoryCombo; @@ -61,9 +60,9 @@ import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.period.MonthlyPeriodType; import org.joda.time.DateTime; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -72,14 +71,9 @@ */ @ExtendWith(MockitoExtension.class) class QueryPlannerGroupByAggregationTypeTest { - private QueryPlanner subject; - @Mock private PartitionManager partitionManager; - @BeforeEach - public void setUp() { - subject = new DefaultQueryPlanner(partitionManager); - } + @InjectMocks private DefaultQueryPlanner subject; @Test void verifyMultipleDataElementIsAggregatedWithTwoQueryGroupWhenDataTypeIsDifferent() { diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/SubexpressionPeriodOffsetUtilsTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/SubexpressionPeriodOffsetUtilsTest.java index 5ba3757e7a35..10d80dcab6ee 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/SubexpressionPeriodOffsetUtilsTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/SubexpressionPeriodOffsetUtilsTest.java @@ -55,36 +55,36 @@ */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) class SubexpressionPeriodOffsetUtilsTest { - private DataElement de0 = createDataElement('A'); + private final DataElement de0 = createDataElement('A'); - private DataElement dem1 = createDataElementAWithPeriodOffset(-1); + private final DataElement dem1 = createDataElementAWithPeriodOffset(-1); - private DataElement dem2 = createDataElementAWithPeriodOffset(-2); + private final DataElement dem2 = createDataElementAWithPeriodOffset(-2); - private DataElement dep1 = createDataElementAWithPeriodOffset(1); + private final DataElement dep1 = createDataElementAWithPeriodOffset(1); - private String deAUid = de0.getUid(); + private final String deAUid = de0.getUid(); - private String expression = + private final String expression = format( "subExpression( #{%s} + #{%s}.periodOffset(-1) + #{%s}.periodOffset(-2) + #{%s}.periodOffset(1) )", deAUid, deAUid, deAUid, deAUid); - private List items = List.of(de0, dem1, dem2, dep1); + private final List items = List.of(de0, dem1, dem2, dep1); - private SubexpressionDimensionItem subex = + private final SubexpressionDimensionItem subExpr = new SubexpressionDimensionItem(expression, items, null); - private Period periodA = createPeriod("202309"); + private final Period periodA = createPeriod("202309"); - private Period periodB = createPeriod("202310"); + private final Period periodB = createPeriod("202310"); - private DataQueryParams params = + private final DataQueryParams params = DataQueryParams.newBuilder() .withPeriodType("monthly") .withPeriods(List.of(periodA, periodB)) .addDimension( - new BaseDimensionalObject(DATA_X_DIM_ID, DimensionType.DATA_X, getList(subex))) + new BaseDimensionalObject(DATA_X_DIM_ID, DimensionType.DATA_X, getList(subExpr))) .build(); @Test @@ -120,7 +120,7 @@ void testGetParamsWithDataPeriods() { getPeriodList("202307", "202308", "202309", "202310", "202311"); assertContainsOnly(expectedPeriods, result.getPeriods()); - List expectedData = List.of(subex); + List expectedData = List.of(subExpr); assertContainsOnly(expectedData, result.getAllDataDimensionItems()); } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/handler/DataHandlerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/handler/DataHandlerTest.java index 1b72da84cfc6..79766d139804 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/handler/DataHandlerTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/handler/DataHandlerTest.java @@ -43,12 +43,10 @@ import org.hisp.dhis.dataelement.DataElementOperand.TotalType; import org.junit.jupiter.api.Test; -/** Unit tests for {@link DataHandler}. */ class DataHandlerTest { @Test void testOperandDataQueryParamsOnlyOperands() { - // Given DataElement dataElement = new DataElement("NameA"); dataElement.setUid("uid1234567A"); dataElement.setCode("CodeA"); @@ -63,19 +61,16 @@ void testOperandDataQueryParamsOnlyOperands() { TotalType anyTotalType = COC_ONLY; - // When DataHandler dataHandler = withNullDependencies(); DataQueryParams params = dataHandler.getOperandDataQueryParams( stubParams, List.of(dataElementOperand), anyTotalType); - // Then assertEquals(1, params.getDimensions().size()); } @Test void testOperandDataQueryParamsWithCatOptionCombo() { - // Given DataElement dataElement = new DataElement("NameA"); dataElement.setUid("uid1234567A"); dataElement.setCode("CodeA"); @@ -94,18 +89,15 @@ void testOperandDataQueryParamsWithCatOptionCombo() { new BaseDimensionalObject(DATA_X_DIM_ID, DATA_X, List.of(dataElementOperand))) .build(); - // When DataHandler dataHandler = withNullDependencies(); DataQueryParams params = dataHandler.getOperandDataQueryParams(stubParams, List.of(dataElementOperand), COC_ONLY); - // Then assertEquals(2, params.getDimensions().size()); } @Test void testOperandDataQueryParamsWithCatOptionComboInFilter() { - // Given DataElement dataElement = new DataElement("NameA"); dataElement.setUid("uid1234567A"); dataElement.setCode("CodeA"); @@ -124,19 +116,16 @@ void testOperandDataQueryParamsWithCatOptionComboInFilter() { new BaseDimensionalObject(DATA_X_DIM_ID, DATA_X, List.of(dataElementOperand))) .build(); - // When DataHandler dataHandler = withNullDependencies(); DataQueryParams params = dataHandler.getOperandDataQueryParams(stubParams, List.of(dataElementOperand), COC_ONLY); - // Then assertEquals(0, params.getDimensions().size()); assertEquals(3, params.getFilters().size()); } @Test void testOperandDataQueryParamsWithAttrOptionCombo() { - // Given DataElement dataElement = new DataElement("NameA"); dataElement.setUid("uid1234567A"); dataElement.setCode("CodeA"); @@ -155,18 +144,15 @@ void testOperandDataQueryParamsWithAttrOptionCombo() { new BaseDimensionalObject(DATA_X_DIM_ID, DATA_X, List.of(dataElementOperand))) .build(); - // When DataHandler dataHandler = withNullDependencies(); DataQueryParams params = dataHandler.getOperandDataQueryParams(stubParams, List.of(dataElementOperand), AOC_ONLY); - // Then assertEquals(2, params.getDimensions().size()); } @Test void testOperandDataQueryParamsWithAttrOptionComboInFilter() { - // Given DataElement dataElement = new DataElement("NameA"); dataElement.setUid("uid1234567A"); dataElement.setCode("CodeA"); @@ -185,12 +171,10 @@ void testOperandDataQueryParamsWithAttrOptionComboInFilter() { new BaseDimensionalObject(DATA_X_DIM_ID, DATA_X, List.of(dataElementOperand))) .build(); - // When DataHandler dataHandler = withNullDependencies(); DataQueryParams params = dataHandler.getOperandDataQueryParams(stubParams, List.of(dataElementOperand), AOC_ONLY); - // Then assertEquals(0, params.getDimensions().size()); assertEquals(3, params.getFilters().size()); } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/handler/SchemeIdResponseMapperTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/handler/SchemeIdResponseMapperTest.java index 2f2d8008920c..edd44fbee2e6 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/handler/SchemeIdResponseMapperTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/data/handler/SchemeIdResponseMapperTest.java @@ -69,7 +69,6 @@ import org.hisp.dhis.program.Program; import org.hisp.dhis.program.ProgramIndicator; import org.hisp.dhis.system.grid.ListGrid; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; @@ -79,12 +78,7 @@ */ @ExtendWith(MockitoExtension.class) class SchemeIdResponseMapperTest { - private SchemeIdResponseMapper schemeIdResponseMapper; - - @BeforeEach - public void setUp() { - schemeIdResponseMapper = new SchemeIdResponseMapper(); - } + private final SchemeIdResponseMapper schemeIdResponseMapper = new SchemeIdResponseMapper(); @Test void testGetSchemeIdResponseMapWhenOutputIdSchemeIsSetToName() { diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/AbstractJdbcEventAnalyticsManagerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/AbstractJdbcEventAnalyticsManagerTest.java index 5ae3c01b65d9..cfa4d4c09173 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/AbstractJdbcEventAnalyticsManagerTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/AbstractJdbcEventAnalyticsManagerTest.java @@ -123,6 +123,7 @@ class AbstractJdbcEventAnalyticsManagerTest extends EventAnalyticsTest { @Mock private ProgramIndicatorService programIndicatorService; @Mock private ExecutionPlanStore executionPlanStore; + @Mock private OrganisationUnitService organisationUnitService; private final SqlBuilder sqlBuilder = new PostgreSqlBuilder(); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/ColumnWithNullIfAndAliasTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/ColumnWithNullIfAndAliasTest.java index 3f806ce42c14..6b0d7032775c 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/ColumnWithNullIfAndAliasTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/ColumnWithNullIfAndAliasTest.java @@ -38,12 +38,9 @@ class ColumnWithNullIfAndAliasTest { @Test void testAsSqlReturnsRightInstance() { - // given - // when ColumnAndAlias columnAndAlias = ColumnWithNullIfAndAlias.ofColumnWithNullIfAndAlias(COLUMN, ALIAS); - // then assertEquals(COLUMN, columnAndAlias.getColumn()); assertEquals(ALIAS, columnAndAlias.getAlias()); @@ -51,12 +48,9 @@ void testAsSqlReturnsRightInstance() { @Test void testAsSqlReturnsRightSqlSnippetWhenCalled() { - // given - // when ColumnAndAlias columnAndAlias = ColumnWithNullIfAndAlias.ofColumnWithNullIfAndAlias(COLUMN, ALIAS); - // then assertEquals("nullif(" + COLUMN + ",'') as \"" + ALIAS + "\"", columnAndAlias.asSql()); } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/DefaultEventCoordinateServiceTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/DefaultEventCoordinateServiceTest.java index fb665fca2f99..d7bc5b74197e 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/DefaultEventCoordinateServiceTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/DefaultEventCoordinateServiceTest.java @@ -45,6 +45,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -61,14 +62,13 @@ class DefaultEventCoordinateServiceTest { @Mock private TrackedEntityAttributeService attributeService; + @InjectMocks private DefaultEventCoordinateService service; + @ParameterizedTest @ValueSource(strings = {"eventgeometry", "enrollmentgeometry", "ougeometry"}) void testGetCoordinateFieldOrFail(String geometry) { when(programService.getProgram(any(String.class))).thenReturn(createProgram('A')); - EventCoordinateService service = - new DefaultEventCoordinateService(programService, dataElementService, attributeService); - assertEquals(geometry, service.getCoordinateField("A", geometry, ErrorCode.E7232)); } @@ -76,9 +76,6 @@ void testGetCoordinateFieldOrFail(String geometry) { void testGetFallbackCoordinateFieldsWithFallbackCoordinateFieldParam() { when(programService.getProgram(any(String.class))).thenReturn(createProgram('A')); - EventCoordinateService service = - new DefaultEventCoordinateService(programService, dataElementService, attributeService); - assertEquals(COL_NAME_GEOMETRY_LIST, service.getFallbackCoordinateFields("A", null, true)); } @@ -87,27 +84,18 @@ void testGetFallbackCoordinateFieldsWithFallbackCoordinateFieldParam() { void testGetFallbackCoordinateFieldsWithoutFallbackCoordinateFieldParam(String geometry) { when(programService.getProgram(any(String.class))).thenReturn(createProgram('A')); - EventCoordinateService service = - new DefaultEventCoordinateService(programService, dataElementService, attributeService); - assertEquals(List.of(geometry), service.getFallbackCoordinateFields("A", geometry, true)); } @ParameterizedTest @ValueSource(strings = {"enrollmentgeometry", "eventgeometry", "tegeometry", "ougeometry"}) void testVerifyFallbackCoordinateFieldWithRegistrationProgram(String geometry) { - EventCoordinateService service = - new DefaultEventCoordinateService(programService, dataElementService, attributeService); - assertTrue(service.isFallbackCoordinateFieldValid(true, geometry)); } @ParameterizedTest @ValueSource(strings = {"enrollmentgeometry", "eventgeometry", "ougeometry"}) void testVerifyFallbackCoordinateFieldWithoutRegistrationProgram(String geometry) { - EventCoordinateService service = - new DefaultEventCoordinateService(programService, dataElementService, attributeService); - assertTrue(service.isFallbackCoordinateFieldValid(false, geometry)); } @@ -116,12 +104,8 @@ void testVerifyFallbackCoordinateFieldWithoutRegistrationProgram(String geometry strings = {"badeventgeometry", "badenrollmentgeometry", "badtegeometry", "badougeometry"}) void testVerifyBadFallbackCoordinateField(String geometry) { when(dataElementService.getDataElement(any(String.class))).thenReturn(null); - when(attributeService.getTrackedEntityAttribute(any(String.class))).thenReturn(null); - EventCoordinateService service = - new DefaultEventCoordinateService(programService, dataElementService, attributeService); - assertThrows( IllegalQueryException.class, () -> service.isFallbackCoordinateFieldValid(false, geometry)); } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/EnrollmentAggregateServiceTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/EnrollmentAggregateServiceTest.java index 6987fa94483c..dec8679348ac 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/EnrollmentAggregateServiceTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/EnrollmentAggregateServiceTest.java @@ -69,9 +69,9 @@ import org.hisp.dhis.program.ProgramStage; import org.hisp.dhis.user.SystemUser; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.opengis.geometry.primitive.Point; @@ -84,8 +84,6 @@ @ExtendWith(MockitoExtension.class) class EnrollmentAggregateServiceTest { - private EnrollmentAggregateService dummyAnalyticsService; - @Mock private AnalyticsSecurityManager securityManager; @Mock private EnrollmentAnalyticsManager enrollmentAnalyticsManager; @@ -98,23 +96,13 @@ class EnrollmentAggregateServiceTest { @Mock private SchemeIdHandler schemeIdHandler; + @InjectMocks private EnrollmentAggregateService service; + @BeforeAll static void setup() { injectSecurityContextNoSettings(new SystemUser()); } - @BeforeEach - public void setUp() { - dummyAnalyticsService = - new EnrollmentAggregateService( - enrollmentAnalyticsManager, - queryPlanner, - securityManager, - queryValidator, - metadataHandler, - schemeIdHandler); - } - @Test void verifyHeaderCreationBasedOnQueryItemsAndDimensions() { // Given @@ -146,7 +134,7 @@ void verifyHeaderCreationBasedOnQueryItemsAndDimensions() { // When when(securityManager.withUserConstraints(any(EventQueryParams.class))).thenReturn(params); - Grid grid = dummyAnalyticsService.getEnrollments(params); + Grid grid = service.getEnrollments(params); // Then List headers = grid.getHeaders(); @@ -215,7 +203,7 @@ void verifyHeaderCreationBasedOnQueryItemsAndDimensionsWithSameNamesMultiStage() // When when(securityManager.withUserConstraints(any(EventQueryParams.class))).thenReturn(params); - Grid grid = dummyAnalyticsService.getEnrollments(params); + Grid grid = service.getEnrollments(params); // Then List headers = grid.getHeaders(); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/EventQueryServiceTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/EventQueryServiceTest.java index 0de1cb91b8cf..4d5b916e4475 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/EventQueryServiceTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/EventQueryServiceTest.java @@ -58,9 +58,9 @@ import org.hisp.dhis.system.database.DatabaseInfoProvider; import org.hisp.dhis.user.SystemUser; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -71,8 +71,6 @@ */ @ExtendWith(MockitoExtension.class) class EventQueryServiceTest { - @Mock private EventQueryService eventQueryService; - @Mock private EventQueryValidator queryValidator; @Mock private MetadataItemsHandler metadataHandler; @@ -91,24 +89,13 @@ class EventQueryServiceTest { @Mock private SchemeIdResponseMapper schemeIdResponseMapper; + @InjectMocks private EventQueryService eventQueryService; + @BeforeAll static void setup() { injectSecurityContextNoSettings(new SystemUser()); } - @BeforeEach - public void setUp() { - eventQueryService = - new EventQueryService( - securityManager, - queryValidator, - eventAnalyticsManager, - queryPlanner, - databaseInfoProvider, - metadataHandler, - schemeIdHandler); - } - @Test void testOutputSchemeWhenSchemeIsSet() { IdScheme codeScheme = IdScheme.CODE; diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/EventQueryValidatorTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/EventQueryValidatorTest.java index f2c27791b050..d67cb83988af 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/EventQueryValidatorTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/EventQueryValidatorTest.java @@ -89,6 +89,7 @@ class EventQueryValidatorTest extends TestBase { private OptionSet osA; @Mock private SystemSettingsProvider settingsProvider; + @Mock private SystemSettings settings; @Mock private QueryValidator queryValidator; diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/programindicator/ProgramIndicatorSubqueryBuilderTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/programindicator/ProgramIndicatorSubqueryBuilderTest.java index 3bfee00382e5..0b8057d8ec3c 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/programindicator/ProgramIndicatorSubqueryBuilderTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/programindicator/ProgramIndicatorSubqueryBuilderTest.java @@ -47,6 +47,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -61,22 +62,21 @@ class ProgramIndicatorSubqueryBuilderTest { private static final BeanRandomizer rnd = BeanRandomizer.create(); - @Mock private ProgramIndicatorService programIndicatorService; - private Program program; private Date startDate; private Date endDate; - private DefaultProgramIndicatorSubqueryBuilder subject; + @Mock private ProgramIndicatorService programIndicatorService; + + @InjectMocks private DefaultProgramIndicatorSubqueryBuilder subject; @BeforeEach public void setUp() { program = createProgram('A'); startDate = getDate(2018, 1, 1); endDate = getDate(2018, 6, 30); - subject = new DefaultProgramIndicatorSubqueryBuilder(programIndicatorService); } @Test diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/queryItem/QueryItemLocatorTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/queryItem/QueryItemLocatorTest.java index 512baf4b19ad..97f0a0837eaf 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/queryItem/QueryItemLocatorTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/event/data/queryItem/QueryItemLocatorTest.java @@ -53,7 +53,6 @@ import java.util.Set; import org.hisp.dhis.analytics.DataQueryService; import org.hisp.dhis.analytics.EventOutputType; -import org.hisp.dhis.analytics.event.QueryItemLocator; import org.hisp.dhis.analytics.event.data.DefaultQueryItemLocator; import org.hisp.dhis.common.BaseDimensionalObject; import org.hisp.dhis.common.CodeGenerator; @@ -78,6 +77,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -100,7 +100,7 @@ class QueryItemLocatorTest { @Mock private RelationshipTypeService relationshipTypeService; - private QueryItemLocator subject; + @InjectMocks private DefaultQueryItemLocator subject; private Program programA; @@ -114,16 +114,6 @@ public void setUp() { dimension = CodeGenerator.generateUid(); programStageUid = CodeGenerator.generateUid(); - - subject = - new DefaultQueryItemLocator( - programStageService, - dataElementService, - attributeService, - programIndicatorService, - legendSetService, - relationshipTypeService, - dataQueryService); } @Test diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/orgunit/data/JdbcOrgUnitAnalyticsManagerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/orgunit/data/JdbcOrgUnitAnalyticsManagerTest.java index 7172035e84d5..27bdea994a78 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/orgunit/data/JdbcOrgUnitAnalyticsManagerTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/orgunit/data/JdbcOrgUnitAnalyticsManagerTest.java @@ -43,10 +43,11 @@ import org.hisp.dhis.db.sql.PostgreSqlBuilder; import org.hisp.dhis.db.sql.SqlBuilder; import org.hisp.dhis.organisationunit.OrganisationUnitGroupSet; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.support.rowset.SqlRowSet; @@ -65,15 +66,9 @@ class JdbcOrgUnitAnalyticsManagerTest { @Mock private TableInfoReader tableInfoReader; - private final SqlBuilder sqlBuilder = new PostgreSqlBuilder(); + @Spy private final SqlBuilder sqlBuilder = new PostgreSqlBuilder(); - private JdbcOrgUnitAnalyticsManager jdbcOrgUnitAnalyticsManager; - - @BeforeEach - public void beforeAll() { - jdbcOrgUnitAnalyticsManager = - new JdbcOrgUnitAnalyticsManager(tableInfoReader, sqlBuilder, jdbcTemplate); - } + @InjectMocks private JdbcOrgUnitAnalyticsManager manager; @Test void testGetOrgUnitDataWithSuccess() { @@ -97,7 +92,7 @@ void testGetOrgUnitDataWithSuccess() { "analytics_rs_organisationunitgroupsetstructure", Set.of("abc123", "abc456"))) .thenReturn(Set.of()); when(jdbcTemplate.queryForRowSet(anyString())).thenReturn(sqlRowSet); - Map data = jdbcOrgUnitAnalyticsManager.getOrgUnitData(params); + Map data = manager.getOrgUnitData(params); // Then // Based on the mocked sqlRowSet. @@ -128,15 +123,11 @@ void testGetOrgUnitDataWithInvalidOrgUnitSetDimension() { // Then assertThrows( - QueryRuntimeException.class, - () -> jdbcOrgUnitAnalyticsManager.getOrgUnitData(params), - E7302.getMessage()); + QueryRuntimeException.class, () -> manager.getOrgUnitData(params), E7302.getMessage()); } private void mockSqlRowSet() { - // Simulate 2 results. when(sqlRowSet.next()).thenReturn(true).thenReturn(true).thenReturn(false); - when(sqlRowSet.getString("orgunit")).thenReturn("OrgUnit"); when(sqlRowSet.getString("abc123")).thenReturn("Abc123"); when(sqlRowSet.getString("abc456")).thenReturn("Abc456"); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/outlier/data/OutlierQueryParserTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/outlier/data/OutlierQueryParserTest.java index e8fac6ff1c53..99118eb6e230 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/outlier/data/OutlierQueryParserTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/outlier/data/OutlierQueryParserTest.java @@ -41,7 +41,7 @@ import java.util.List; import java.util.Set; -import org.hisp.dhis.analytics.data.DimensionalObjectProducer; +import org.hisp.dhis.analytics.data.DimensionalObjectProvider; import org.hisp.dhis.common.BaseDimensionalObject; import org.hisp.dhis.common.DisplayProperty; import org.hisp.dhis.common.IdScheme; @@ -55,15 +55,19 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @ExtendWith(MockitoExtension.class) class OutlierQueryParserTest { @Mock private IdentifiableObjectManager idObjectManager; - @Mock private DimensionalObjectProducer dimensionalObjectProducer; + + @Mock private DimensionalObjectProvider dimensionalObjectProducer; + @Mock private UserService userService; - private OutlierQueryParser subject; + + @InjectMocks private OutlierQueryParser subject; @BeforeEach void setup() { @@ -88,8 +92,6 @@ void setup() { user.setDataViewOrganisationUnits(Set.of(organisationUnit)); injectSecurityContextNoSettings(UserDetails.fromUser(user)); when(userService.getUserByUsername(anyString())).thenReturn(user); - - subject = new OutlierQueryParser(idObjectManager, dimensionalObjectProducer, userService); } @Test diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/outlier/service/AnalyticsZscoreSqlStatementProcessorTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/outlier/service/AnalyticsZscoreSqlStatementProcessorTest.java index 270d5c3f3c2b..bcc312f02f10 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/outlier/service/AnalyticsZscoreSqlStatementProcessorTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/outlier/service/AnalyticsZscoreSqlStatementProcessorTest.java @@ -57,10 +57,6 @@ class AnalyticsZscoreSqlStatementProcessorTest { private OutlierSqlStatementProcessor subject; - // ------------------------------------------------------------------------- - // Fixture - // ------------------------------------------------------------------------- - private List dataDimensions; private OrganisationUnit ouA; diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/resolver/CategoryOptionGroupResolverTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/resolver/CategoryOptionGroupResolverTest.java index 71c106898d56..ae5a6bb91c6f 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/resolver/CategoryOptionGroupResolverTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/resolver/CategoryOptionGroupResolverTest.java @@ -47,6 +47,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -62,7 +63,7 @@ class CategoryOptionGroupResolverTest { @Mock private ExpressionService expressionService; - private ExpressionResolver resolver; + @InjectMocks private CategoryOptionGroupResolver resolver; private String uid1; @@ -76,9 +77,9 @@ class CategoryOptionGroupResolverTest { private CategoryOptionCombo coc3; - DimensionalItemId dimensionalItemId; + private DimensionalItemId dimensionalItemId; - private static final String CATEGORY_OPTION_GROUP_PREFIX = "coGroup:"; + private final String CATEGORY_OPTION_GROUP_PREFIX = "coGroup:"; @BeforeEach public void setUp() { @@ -89,10 +90,6 @@ public void setUp() { coc1 = createCategoryOptionCombo('X'); coc2 = createCategoryOptionCombo('Y'); coc3 = createCategoryOptionCombo('Z'); - - resolver = - new CategoryOptionGroupResolver( - expressionService, categoryOptionGroupStore, categoryOptionComboStore); } @Test diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/resolver/CategoryOptionResolverTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/resolver/CategoryOptionResolverTest.java index d5debfd3cec8..245cc06733af 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/resolver/CategoryOptionResolverTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/resolver/CategoryOptionResolverTest.java @@ -46,6 +46,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; @@ -61,7 +62,7 @@ class CategoryOptionResolverTest { @Mock private ExpressionService expressionService; - private ExpressionResolver resolver; + @InjectMocks private CategoryOptionResolver resolver; private String uid1; @@ -75,34 +76,26 @@ class CategoryOptionResolverTest { private CategoryOptionCombo coc3; - DimensionalItemId dimensionalItemId; + private DimensionalItemId dimensionalItemId; - private static final String CATEGORY_OPTION_PREFIX = "co:"; + private final String CATEGORY_OPTION_PREFIX = "co:"; @BeforeEach public void setUp() { uid1 = CodeGenerator.generateUid(); - uid2 = CodeGenerator.generateUid(); - uid3 = CodeGenerator.generateUid(); CategoryOption categoryOption = createCategoryOption('A'); coc1 = createCategoryOptionCombo('X'); - - categoryOption.addCategoryOptionCombo(coc1); - coc2 = createCategoryOptionCombo('Y'); - - categoryOption.addCategoryOptionCombo(coc2); - coc3 = createCategoryOptionCombo('Z'); + categoryOption.addCategoryOptionCombo(coc1); + categoryOption.addCategoryOptionCombo(coc2); categoryOption.addCategoryOptionCombo(coc3); - resolver = new CategoryOptionResolver(expressionService, categoryOptionStore); - when(categoryOptionStore.getByUid(anyString())).thenReturn(categoryOption); } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/resolver/DataElementGroupResolverTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/resolver/DataElementGroupResolverTest.java index f5fb99dfcc85..dc47bf1e99a0 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/resolver/DataElementGroupResolverTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/resolver/DataElementGroupResolverTest.java @@ -46,6 +46,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; @@ -61,7 +62,7 @@ class DataElementGroupResolverTest { @Mock private ExpressionService expressionService; - private ExpressionResolver resolver; + @InjectMocks private DataElementGroupResolver resolver; private String uid1; @@ -75,34 +76,26 @@ class DataElementGroupResolverTest { private DataElement de3; - DimensionalItemId dimensionalItemId; + private DimensionalItemId dimensionalItemId; - private static final String DATA_ELEMENT_GROUP_PREFIX = "deGroup:"; + private final String DATA_ELEMENT_GROUP_PREFIX = "deGroup:"; @BeforeEach public void setUp() { uid1 = CodeGenerator.generateUid(); - uid2 = CodeGenerator.generateUid(); - uid3 = CodeGenerator.generateUid(); de1 = createDataElement('X'); - de2 = createDataElement('Y'); - de3 = createDataElement('Z'); DataElementGroup dataElementGroup = createDataElementGroup('A'); dataElementGroup.addDataElement(de1); - dataElementGroup.addDataElement(de2); - dataElementGroup.addDataElement(de3); - resolver = new DataElementGroupResolver(expressionService, dataElementGroupStore); - when(dataElementGroupStore.getByUid(anyString())).thenReturn(dataElementGroup); } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/AbstractEventJdbcTableManagerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/AbstractEventJdbcTableManagerTest.java new file mode 100644 index 000000000000..80cf1dbee0bf --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/AbstractEventJdbcTableManagerTest.java @@ -0,0 +1,104 @@ +/* + * 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.analytics.table; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.hisp.dhis.common.ValueType; +import org.hisp.dhis.db.sql.PostgreSqlBuilder; +import org.hisp.dhis.db.sql.SqlBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class AbstractEventJdbcTableManagerTest { + + @Spy private SqlBuilder sqlBuilder = new PostgreSqlBuilder(); + + @InjectMocks private JdbcEventAnalyticsTableManager manager; + + @Test + void testGetCastExpression() { + String expected = + """ + case when eventdatavalues #>> '{GieVkTxp4HH, value}' ~* '^(-?[0-9]+)(\\.[0-9]+)?$' \ + then cast(eventdatavalues #>> '{GieVkTxp4HH, value}' as double precision) \ + else null end"""; + + String actual = + manager.getCastExpression( + "eventdatavalues #>> '{GieVkTxp4HH, value}'", + "'^(-?[0-9]+)(\\.[0-9]+)?$'", + "double precision"); + + assertEquals(expected, actual); + } + + @Test + void testGetSelectExpressionNumber() { + String expected = + """ + case when eventdatavalues #>> '{GieVkTxp4HH, value}' ~* '^(-?[0-9]+)(\\.[0-9]+)?$' \ + then cast(eventdatavalues #>> '{GieVkTxp4HH, value}' as double precision) \ + else null end"""; + + String actual = + manager.getSelectExpression(ValueType.NUMBER, "eventdatavalues #>> '{GieVkTxp4HH, value}'"); + + assertEquals(expected, actual); + } + + @Test + void testGetSelectExpressionDate() { + String expected = + """ + case when eventdatavalues #>> '{AL04Wbutskk, value}' ~* '^\\d{4}-\\d{2}-\\d{2}(\\s|T)?((\\d{2}:)(\\d{2}:)?(\\d{2}))?(|.(\\d{3})|.(\\d{3})Z)?$' \ + then cast(eventdatavalues #>> '{AL04Wbutskk, value}' as timestamp) \ + else null end"""; + + String actual = + manager.getSelectExpression(ValueType.DATE, "eventdatavalues #>> '{AL04Wbutskk, value}'"); + + assertEquals(expected, actual); + } + + @Test + void testGetSelectExpressionText() { + String expected = + """ + eventdatavalues #>> '{FwUzmc49Pcr, value}'"""; + + String actual = + manager.getSelectExpression(ValueType.TEXT, "eventdatavalues #>> '{FwUzmc49Pcr, value}'"); + + assertEquals(expected, actual); + } +} diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/AnalyticsTableServiceTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/AnalyticsTableServiceTest.java index 5caaa63a71ea..91389524f5de 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/AnalyticsTableServiceTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/AnalyticsTableServiceTest.java @@ -55,8 +55,8 @@ */ @ExtendWith(MockitoExtension.class) class AnalyticsTableServiceTest { - @Mock private SystemSettingsProvider settingsProvider; + @Mock private SystemSettings settings; @Mock private SqlBuilder sqlBuilder; diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManagerDorisTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManagerDorisTest.java index b003ceea077b..351950d9f48c 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManagerDorisTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManagerDorisTest.java @@ -30,18 +30,16 @@ import static java.time.LocalDate.now; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; import static org.hisp.dhis.db.model.DataType.BIGINT; import static org.hisp.dhis.db.model.DataType.DOUBLE; import static org.hisp.dhis.db.model.DataType.INTEGER; import static org.hisp.dhis.db.model.DataType.TEXT; import static org.hisp.dhis.db.model.DataType.TIMESTAMP; import static org.hisp.dhis.db.model.Table.STAGING_TABLE_SUFFIX; -import static org.hisp.dhis.period.PeriodDataProvider.DataSource.DATABASE; +import static org.hisp.dhis.period.PeriodDataProvider.PeriodSource.DATABASE; import static org.hisp.dhis.test.TestBase.createDataElement; import static org.hisp.dhis.test.TestBase.createProgram; import static org.hisp.dhis.test.TestBase.createProgramStage; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.time.LocalDate; @@ -50,10 +48,8 @@ import java.util.List; import java.util.Set; import org.hisp.dhis.analytics.AggregationType; -import org.hisp.dhis.analytics.AnalyticsTableHookService; import org.hisp.dhis.analytics.AnalyticsTableType; import org.hisp.dhis.analytics.AnalyticsTableUpdateParams; -import org.hisp.dhis.analytics.partition.PartitionManager; import org.hisp.dhis.analytics.table.model.AnalyticsTable; import org.hisp.dhis.analytics.table.model.AnalyticsTableColumn; import org.hisp.dhis.analytics.table.model.Skip; @@ -62,13 +58,13 @@ import org.hisp.dhis.category.CategoryService; import org.hisp.dhis.common.IdentifiableObjectManager; import org.hisp.dhis.common.ValueType; -import org.hisp.dhis.dataapproval.DataApprovalLevelService; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.db.model.IndexType; import org.hisp.dhis.db.sql.DorisSqlBuilder; import org.hisp.dhis.db.sql.SqlBuilder; import org.hisp.dhis.organisationunit.OrganisationUnitService; import org.hisp.dhis.period.PeriodDataProvider; +import org.hisp.dhis.period.PeriodDataProvider.PeriodSource; import org.hisp.dhis.period.PeriodType; import org.hisp.dhis.program.Program; import org.hisp.dhis.program.ProgramStage; @@ -81,7 +77,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.jdbc.core.JdbcTemplate; @@ -97,6 +95,7 @@ class JdbcEventAnalyticsTableManagerDorisTest { @Mock private CategoryService categoryService; @Mock private SystemSettingsProvider settingsProvider; + @Mock private SystemSettings settings; @Mock private DatabaseInfoProvider databaseInfoProvider; @@ -109,9 +108,9 @@ class JdbcEventAnalyticsTableManagerDorisTest { @Mock private AnalyticsTableSettings analyticsTableSettings; - private final SqlBuilder sqlBuilder = new DorisSqlBuilder("dhis2", "driver"); + @Spy private SqlBuilder sqlBuilder = new DorisSqlBuilder("dhis2", "driver"); - private JdbcEventAnalyticsTableManager subject; + @InjectMocks private JdbcEventAnalyticsTableManager subject; private Date today; @@ -119,8 +118,6 @@ class JdbcEventAnalyticsTableManagerDorisTest { private static final String TABLE_PREFIX = "analytics_event_"; - private static final String FROM_CLAUSE = "from dhis2.public.`event` where eventid=ev.eventid"; - private static final int OU_NAME_HIERARCHY_COUNT = 1; private List periodColumns = @@ -147,88 +144,42 @@ void setUp() { when(databaseInfoProvider.getDatabaseInfo()).thenReturn(DatabaseInfo.builder().build()); when(settingsProvider.getCurrentSettings()).thenReturn(settings); when(settings.getLastSuccessfulResourceTablesUpdate()).thenReturn(new Date(0L)); - - subject = - new JdbcEventAnalyticsTableManager( - idObjectManager, - organisationUnitService, - categoryService, - settingsProvider, - mock(DataApprovalLevelService.class), - resourceTableService, - mock(AnalyticsTableHookService.class), - mock(PartitionManager.class), - databaseInfoProvider, - jdbcTemplate, - analyticsTableSettings, - periodDataProvider, - sqlBuilder); - assertThat(subject.getAnalyticsTableType(), is(AnalyticsTableType.EVENT)); + when(analyticsTableSettings.getPeriodSource()).thenReturn(PeriodSource.DATABASE); } @Test void verifyGetTableWithDataElements() { - - subject = - new JdbcEventAnalyticsTableManager( - idObjectManager, - organisationUnitService, - categoryService, - settingsProvider, - mock(DataApprovalLevelService.class), - resourceTableService, - mock(AnalyticsTableHookService.class), - mock(PartitionManager.class), - databaseInfoProvider, - jdbcTemplate, - analyticsTableSettings, - periodDataProvider, - new DorisSqlBuilder("dhis2", "driver")); - when(databaseInfoProvider.getDatabaseInfo()) .thenReturn(DatabaseInfo.builder().spatialSupport(true).build()); Program program = createProgram('A'); - DataElement d1 = createDataElement('Z', ValueType.TEXT, AggregationType.SUM); - DataElement d2 = createDataElement('P', ValueType.PERCENTAGE, AggregationType.SUM); - DataElement d3 = createDataElement('Y', ValueType.BOOLEAN, AggregationType.NONE); - DataElement d4 = createDataElement('W', ValueType.DATE, AggregationType.LAST); - DataElement d5 = createDataElement('G', ValueType.ORGANISATION_UNIT, AggregationType.NONE); - DataElement d6 = createDataElement('H', ValueType.INTEGER, AggregationType.SUM); - DataElement d7 = createDataElement('U', ValueType.COORDINATE, AggregationType.NONE); + DataElement deA = createDataElement('A', ValueType.TEXT, AggregationType.SUM); + DataElement deB = createDataElement('B', ValueType.PERCENTAGE, AggregationType.SUM); + DataElement deC = createDataElement('C', ValueType.BOOLEAN, AggregationType.NONE); + DataElement deD = createDataElement('D', ValueType.DATE, AggregationType.LAST); + DataElement deE = createDataElement('E', ValueType.ORGANISATION_UNIT, AggregationType.NONE); + DataElement deF = createDataElement('F', ValueType.INTEGER, AggregationType.SUM); + DataElement deG = createDataElement('G', ValueType.COORDINATE, AggregationType.NONE); - ProgramStage ps1 = createProgramStage('A', Set.of(d1, d2, d3, d4, d5, d6, d7)); + ProgramStage ps1 = createProgramStage('A', Set.of(deA, deB, deC, deD, deE, deF, deG)); program.setProgramStages(Set.of(ps1)); when(idObjectManager.getAllNoAcl(Program.class)).thenReturn(List.of(program)); - String aliasD1 = - "(select JSON_UNQUOTE(JSON_EXTRACT(eventdatavalues, '$.%s.value')) " - + FROM_CLAUSE - + " ) as `%s`"; - String aliasD2 = - "(select cast(JSON_UNQUOTE(JSON_EXTRACT(eventdatavalues, '$.%s.value')) as double) " - + FROM_CLAUSE - + " and JSON_UNQUOTE(JSON_EXTRACT(eventdatavalues, '$.%s.value')) regexp '^(-?[0-9]+)(\\.[0-9]+)?$') as `%s`"; - String aliasD3 = - "(select case when JSON_UNQUOTE(JSON_EXTRACT(eventdatavalues, '$.%s.value')) = 'true' then 1 when JSON_UNQUOTE(JSON_EXTRACT(eventdatavalues, '$.%s.value')) = 'false' then 0 else null end " - + FROM_CLAUSE - + " ) as `%s`"; - String aliasD4 = - "(select cast(JSON_UNQUOTE(JSON_EXTRACT(eventdatavalues, '$.%s.value')) as datetime) " - + FROM_CLAUSE - + " and JSON_UNQUOTE(JSON_EXTRACT(eventdatavalues, '$.%s.value')) regexp '^\\d{4}-\\d{2}-\\d{2}(\\s|T)?((\\d{2}:)(\\d{2}:)?(\\d{2}))?(|.(\\d{3})|.(\\d{3})Z)?$') as `%s`"; - String aliasD5 = - "(select ou.uid from dhis2.public.`organisationunit` ou where ou.uid = JSON_UNQUOTE(JSON_EXTRACT(eventdatavalues, '$.deabcdefghG.value')) ) as `deabcdefghG`"; - String aliasD5Name = - "(select ou.name from dhis2.public.`organisationunit` ou where ou.uid = JSON_UNQUOTE(JSON_EXTRACT(eventdatavalues, " - + "'$.%s.value')) " - + ") as `%s`"; - String aliasD6 = - "(select cast(JSON_UNQUOTE(JSON_EXTRACT(eventdatavalues, '$.%s.value')) as bigint) " - + FROM_CLAUSE - + " and JSON_UNQUOTE(JSON_EXTRACT(eventdatavalues, '$.%s.value')) regexp '^(-?[0-9]+)(\\.[0-9]+)?$') as `%s`"; + String aliasA = "json_unquote(json_extract(eventdatavalues, '$.%s.value')) as `%s`"; + String aliasB = + "case when json_unquote(json_extract(eventdatavalues, '$.%s.value')) regexp '^(-?[0-9]+)(\\.[0-9]+)?$' then cast(json_unquote(json_extract(eventdatavalues, '$.%s.value')) as double) else null end as `%s`"; + String aliasC = + "case when json_unquote(json_extract(eventdatavalues, '$.%s.value')) = 'true' then 1 when json_unquote(json_extract(eventdatavalues, '$.%s.value')) = 'false' then 0 else null end as `%s`"; + String aliasD = + "case when json_unquote(json_extract(eventdatavalues, '$.%s.value')) regexp '^\\d{4}-\\d{2}-\\d{2}(\\s|T)?((\\d{2}:)(\\d{2}:)?(\\d{2}))?(|.(\\d{3})|.(\\d{3})Z)?$' then cast(json_unquote(json_extract(eventdatavalues, '$.%s.value')) as datetime) else null end as `%s`"; + String aliasE = + "(select ou.uid from dhis2.public.`organisationunit` ou where ou.uid = json_unquote(json_extract(eventdatavalues, '$.%s.value')) ) as `%s`"; + String aliasF = + "(select ou.name from dhis2.public.`organisationunit` ou where ou.uid = json_unquote(json_extract(eventdatavalues, '$.%s.value')) ) as `%s`"; + String aliasG = + "case when json_unquote(json_extract(eventdatavalues, '$.%s.value')) regexp '^(-?[0-9]+)(\\.[0-9]+)?$' then cast(json_unquote(json_extract(eventdatavalues, '$.%s.value')) as bigint) else null end as `%s`"; AnalyticsTableUpdateParams params = AnalyticsTableUpdateParams.newBuilder() @@ -251,39 +202,39 @@ void verifyGetTableWithDataElements() { .withColumnSize(58 + OU_NAME_HIERARCHY_COUNT) .addColumns(periodColumns) .addColumn( - d1.getUid(), + deA.getUid(), TEXT, - toSelectExpression(aliasD1, d1.getUid()), + toSelectExpression(aliasA, deA.getUid()), Skip.SKIP) // ValueType.TEXT .addColumn( - d2.getUid(), + deB.getUid(), DOUBLE, - toSelectExpression(aliasD2, d2.getUid()), + toSelectExpression(aliasB, deB.getUid()), IndexType.BTREE) // ValueType.PERCENTAGE .addColumn( - d3.getUid(), + deC.getUid(), INTEGER, - toSelectExpression(aliasD3, d3.getUid()), + toSelectExpression(aliasC, deC.getUid()), IndexType.BTREE) // ValueType.BOOLEAN .addColumn( - d4.getUid(), + deD.getUid(), TIMESTAMP, - toSelectExpression(aliasD4, d4.getUid()), + toSelectExpression(aliasD, deD.getUid()), IndexType.BTREE) // ValueType.DATE .addColumn( - d5.getUid(), + deE.getUid(), TEXT, - toSelectExpression(aliasD5, d5.getUid()), + toSelectExpression(aliasE, deE.getUid()), IndexType.BTREE) // ValueType.ORGANISATION_UNIT .addColumn( - d6.getUid(), + deF.getUid(), BIGINT, - toSelectExpression(aliasD6, d6.getUid()), + toSelectExpression(aliasG, deF.getUid()), IndexType.BTREE) // ValueType.INTEGER // element d5 also creates a Name column .addColumn( - d5.getUid() + "_name", TEXT, toSelectExpression(aliasD5Name, d5.getUid()), Skip.SKIP) + deE.getUid() + "_name", TEXT, toSelectExpression(aliasF, deE.getUid()), Skip.SKIP) .withDefaultColumns(EventAnalyticsColumn.getColumns(sqlBuilder)) .build() .verify(); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManagerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManagerTest.java index 11e83e26532c..4e64792c8546 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManagerTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcEventAnalyticsTableManagerTest.java @@ -44,7 +44,7 @@ import static org.hisp.dhis.db.model.DataType.TIMESTAMP; import static org.hisp.dhis.db.model.Table.STAGING_TABLE_SUFFIX; import static org.hisp.dhis.db.model.constraint.Nullable.NULL; -import static org.hisp.dhis.period.PeriodDataProvider.DataSource.DATABASE; +import static org.hisp.dhis.period.PeriodDataProvider.PeriodSource.DATABASE; import static org.hisp.dhis.system.util.SqlUtils.quote; import static org.hisp.dhis.test.TestBase.createCategory; import static org.hisp.dhis.test.TestBase.createCategoryCombo; @@ -56,7 +56,6 @@ import static org.hisp.dhis.test.TestBase.getDate; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -70,10 +69,8 @@ import java.util.Map; import java.util.Set; import org.hisp.dhis.analytics.AggregationType; -import org.hisp.dhis.analytics.AnalyticsTableHookService; import org.hisp.dhis.analytics.AnalyticsTableType; import org.hisp.dhis.analytics.AnalyticsTableUpdateParams; -import org.hisp.dhis.analytics.partition.PartitionManager; import org.hisp.dhis.analytics.table.model.AnalyticsTable; import org.hisp.dhis.analytics.table.model.AnalyticsTableColumn; import org.hisp.dhis.analytics.table.model.AnalyticsTablePartition; @@ -86,7 +83,6 @@ import org.hisp.dhis.category.CategoryService; import org.hisp.dhis.common.IdentifiableObjectManager; import org.hisp.dhis.common.ValueType; -import org.hisp.dhis.dataapproval.DataApprovalLevelService; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.db.model.IndexType; import org.hisp.dhis.db.sql.PostgreSqlBuilder; @@ -95,6 +91,7 @@ import org.hisp.dhis.organisationunit.OrganisationUnitLevel; import org.hisp.dhis.organisationunit.OrganisationUnitService; import org.hisp.dhis.period.PeriodDataProvider; +import org.hisp.dhis.period.PeriodDataProvider.PeriodSource; import org.hisp.dhis.period.PeriodType; import org.hisp.dhis.program.Program; import org.hisp.dhis.program.ProgramStage; @@ -111,8 +108,10 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.jdbc.core.JdbcTemplate; @@ -128,6 +127,7 @@ class JdbcEventAnalyticsTableManagerTest { @Mock private CategoryService categoryService; @Mock private SystemSettingsProvider settingsProvider; + @Mock private SystemSettings settings; @Mock private DatabaseInfoProvider databaseInfoProvider; @@ -140,9 +140,9 @@ class JdbcEventAnalyticsTableManagerTest { @Mock private AnalyticsTableSettings analyticsTableSettings; - private final SqlBuilder sqlBuilder = new PostgreSqlBuilder(); + @Spy private SqlBuilder sqlBuilder = new PostgreSqlBuilder(); - private JdbcEventAnalyticsTableManager subject; + @InjectMocks private JdbcEventAnalyticsTableManager subject; private Date today; @@ -150,8 +150,6 @@ class JdbcEventAnalyticsTableManagerTest { private static final String TABLE_PREFIX = "analytics_event_"; - private static final String FROM_CLAUSE = "from \"event\" where eventid=ev.eventid"; - private static final String DATE_CLAUSE = "CASE WHEN 'SCHEDULE' = ev.status THEN ev.scheduleddate ELSE ev.occurreddate END"; @@ -179,23 +177,6 @@ public void setUp() { when(databaseInfoProvider.getDatabaseInfo()).thenReturn(DatabaseInfo.builder().build()); when(settingsProvider.getCurrentSettings()).thenReturn(settings); when(settings.getLastSuccessfulResourceTablesUpdate()).thenReturn(new Date(0L)); - - subject = - new JdbcEventAnalyticsTableManager( - idObjectManager, - organisationUnitService, - categoryService, - settingsProvider, - mock(DataApprovalLevelService.class), - resourceTableService, - mock(AnalyticsTableHookService.class), - mock(PartitionManager.class), - databaseInfoProvider, - jdbcTemplate, - analyticsTableSettings, - periodDataProvider, - sqlBuilder); - assertThat(subject.getAnalyticsTableType(), is(AnalyticsTableType.EVENT)); } @Test @@ -266,8 +247,7 @@ void verifyGetTableWithCategoryCombo() { addCategoryCombo(program, categoryCombo); when(idObjectManager.getAllNoAcl(Program.class)).thenReturn(List.of(program)); - when(periodDataProvider.getAvailableYears(DATABASE)) - .thenReturn(List.of(2018, 2019, now().getYear())); + mockPeriodYears(List.of(2018, 2019, now().getYear())); List availableDataYears = periodDataProvider.getAvailableYears(DATABASE); @@ -311,8 +291,7 @@ void verifyGetTableWithCategoryCombo() { void verifyClientSideTimestampsColumns() { Program program = createProgram('A'); when(idObjectManager.getAllNoAcl(Program.class)).thenReturn(List.of(program)); - when(periodDataProvider.getAvailableYears(DATABASE)) - .thenReturn(List.of(2018, 2019, now().getYear())); + mockPeriodYears(List.of(2018, 2019, now().getYear())); List availableDataYears = periodDataProvider.getAvailableYears(DATABASE); @@ -347,8 +326,7 @@ void verifyClientSideTimestampsColumns() { void verifyAnalyticsEventTableHasDefaultPartition() { Program program = createProgram('A'); when(idObjectManager.getAllNoAcl(Program.class)).thenReturn(List.of(program)); - when(periodDataProvider.getAvailableYears(DATABASE)) - .thenReturn(List.of(2021, 2022, 2023, 2024, 2025)); + mockPeriodYears(List.of(2021, 2022, 2023, 2024, 2025)); List availableDataYears = periodDataProvider.getAvailableYears(DATABASE); @@ -397,19 +375,19 @@ void verifyGetTableWithDataElements() { when(idObjectManager.getAllNoAcl(Program.class)).thenReturn(List.of(program)); - String aliasD1 = "(select eventdatavalues #>> '{%s, value}' " + FROM_CLAUSE + " ) as \"%s\""; + String aliasD1 = "eventdatavalues #>> '{%s, value}' as \"%s\""; String aliasD2 = - "(select cast(eventdatavalues #>> '{%s, value}' as double precision) " - + FROM_CLAUSE - + " and eventdatavalues #>> '{%s, value}' ~* '^(-?[0-9]+)(\\.[0-9]+)?$') as \"%s\""; + """ + case when eventdatavalues #>> '{deabcdefghP, value}' ~* '^(-?[0-9]+)(\\.[0-9]+)?$' \ + then cast(eventdatavalues #>> '{deabcdefghP, value}' as double precision) else null end as "deabcdefghP\""""; String aliasD3 = - "(select case when eventdatavalues #>> '{%s, value}' = 'true' then 1 when eventdatavalues #>> '{%s, value}' = 'false' then 0 else null end " - + FROM_CLAUSE - + " ) as \"%s\""; + """ + case when eventdatavalues #>> '{deabcdefghY, value}' = 'true' then 1 \ + when eventdatavalues #>> '{deabcdefghY, value}' = 'false' then 0 else null end as "deabcdefghY\""""; String aliasD4 = - "(select cast(eventdatavalues #>> '{%s, value}' as timestamp) " - + FROM_CLAUSE - + " and eventdatavalues #>> '{%s, value}' ~* '^\\d{4}-\\d{2}-\\d{2}(\\s|T)?((\\d{2}:)(\\d{2}:)?(\\d{2}))?(|.(\\d{3})|.(\\d{3})Z)?$') as \"%s\""; + """ + case when eventdatavalues #>> '{deabcdefghW, value}' ~* '^\\d{4}-\\d{2}-\\d{2}(\\s|T)?((\\d{2}:)(\\d{2}:)?(\\d{2}))?(|.(\\d{3})|.(\\d{3})Z)?$' \ + then cast(eventdatavalues #>> '{deabcdefghW, value}' as timestamp) else null end as "deabcdefghW\""""; String aliasD5 = "(select ou.uid from \"organisationunit\" ou where ou.uid = " + "eventdatavalues #>> '{" @@ -419,11 +397,12 @@ void verifyGetTableWithDataElements() { + d5.getUid() + "\""; String aliasD6 = - "(select cast(eventdatavalues #>> '{%s, value}' as bigint) " - + FROM_CLAUSE - + " and eventdatavalues #>> '{%s, value}' ~* '^(-?[0-9]+)(\\.[0-9]+)?$') as \"%s\""; + """ + case when eventdatavalues #>> '{deabcdefghH, value}' ~* '^(-?[0-9]+)(\\.[0-9]+)?$' \ + then cast(eventdatavalues #>> '{deabcdefghH, value}' as bigint) else null end as "deabcdefghH\""""; String aliasD7 = - "(select ST_GeomFromGeoJSON('{\"type\":\"Point\", \"coordinates\":' || (eventdatavalues #>> '{%s, value}') || ', \"crs\":{\"type\":\"name\", \"properties\":{\"name\":\"EPSG:4326\"}}}') from \"event\" where eventid=ev.eventid ) as \"%s\""; + """ + ST_GeomFromGeoJSON('{\"type\":\"Point\", \"coordinates\":' || (eventdatavalues #>> '{deabcdefghU, value}') || ', "crs":{"type":"name", "properties":{"name":"EPSG:4326"}}}') as "deabcdefghU\""""; String aliasD5_geo = "(select ou.geometry from \"organisationunit\" ou where ou.uid = eventdatavalues #>> '{" + d5.getUid() @@ -438,7 +417,6 @@ void verifyGetTableWithDataElements() { + ") as \"" + d5.getUid() + "\""; - System.out.println(aliasD5_name); AnalyticsTableUpdateParams params = AnalyticsTableUpdateParams.newBuilder() .lastYears(2) @@ -446,8 +424,7 @@ void verifyGetTableWithDataElements() { .today(today) .build(); - when(periodDataProvider.getAvailableYears(DATABASE)) - .thenReturn(List.of(2018, 2019, now().getYear())); + mockPeriodYears(List.of(2018, 2019, now().getYear())); List availableDataYears = periodDataProvider.getAvailableYears(DATABASE); @@ -529,7 +506,9 @@ void verifyGetTableWithTrackedEntityAttribute() { when(idObjectManager.getAllNoAcl(Program.class)).thenReturn(List.of(program)); - String aliasD1 = "(select eventdatavalues #>> '{%s, value}' " + FROM_CLAUSE + " ) as \"%s\""; + String aliasD1 = + """ + eventdatavalues #>> '{deabcdefghZ, value}' as "deabcdefghZ\""""; String aliasTea1 = "(select %s from \"organisationunit\" ou where ou.uid = (select value from " + "\"trackedentityattributevalue\" where trackedentityid=en.trackedentityid and " @@ -542,8 +521,7 @@ void verifyGetTableWithTrackedEntityAttribute() { .today(today) .build(); - when(periodDataProvider.getAvailableYears(DATABASE)) - .thenReturn(List.of(2018, 2019, now().getYear())); + mockPeriodYears(List.of(2018, 2019, now().getYear())); List availableDataYears = periodDataProvider.getAvailableYears(DATABASE); @@ -606,8 +584,7 @@ void verifyDataElementTypeOrgUnitFetchesOuNameWhenPopulatingEventAnalyticsTable( .today(today) .build(); - when(periodDataProvider.getAvailableYears(DATABASE)) - .thenReturn(List.of(2018, 2019, now().getYear())); + mockPeriodYears(List.of(2018, 2019, now().getYear())); List availableDataYears = periodDataProvider.getAvailableYears(DATABASE); @@ -650,8 +627,7 @@ void verifyTeiTypeOrgUnitFetchesOuNameWhenPopulatingEventAnalyticsTable() { programA.setProgramAttributes(List.of(programTrackedEntityAttribute)); when(idObjectManager.getAllNoAcl(Program.class)).thenReturn(List.of(programA)); - when(periodDataProvider.getAvailableYears(DATABASE)) - .thenReturn(List.of(2018, 2019, now().getYear())); + mockPeriodYears(List.of(2018, 2019, now().getYear())); List availableDataYears = periodDataProvider.getAvailableYears(DATABASE); @@ -700,8 +676,7 @@ void verifyOrgUnitOwnershipJoinsWhenPopulatingEventAnalyticsTable() { programA.setProgramAttributes(List.of(programTrackedEntityAttribute)); when(idObjectManager.getAllNoAcl(Program.class)).thenReturn(List.of(programA)); - when(periodDataProvider.getAvailableYears(DATABASE)) - .thenReturn(List.of(2018, 2019, now().getYear())); + mockPeriodYears(List.of(2018, 2019, now().getYear())); List availableDataYears = periodDataProvider.getAvailableYears(DATABASE); @@ -739,8 +714,7 @@ void verifyGetAnalyticsTableWithOuLevels() { Program programA = rnd.nextObject(Program.class); programA.setId(0); - when(periodDataProvider.getAvailableYears(DATABASE)) - .thenReturn(List.of(2018, 2019, now().getYear())); + mockPeriodYears(List.of(2018, 2019, now().getYear())); List availableDataYears = periodDataProvider.getAvailableYears(DATABASE); int startYear = availableDataYears.get(0); @@ -802,8 +776,7 @@ void verifyGetAnalyticsTableWithOuGroupSet() { when(idObjectManager.getAllNoAcl(Program.class)).thenReturn(List.of(programA)); when(idObjectManager.getDataDimensionsNoAcl(OrganisationUnitGroupSet.class)) .thenReturn(ouGroupSet); - when(periodDataProvider.getAvailableYears(DATABASE)) - .thenReturn(List.of(2018, 2019, now().getYear())); + mockPeriodYears(List.of(2018, 2019, now().getYear())); List availableDataYears = periodDataProvider.getAvailableYears(DATABASE); @@ -843,8 +816,7 @@ void verifyGetAnalyticsTableWithOptionGroupSets() { when(idObjectManager.getAllNoAcl(Program.class)).thenReturn(List.of(programA)); when(categoryService.getAttributeCategoryOptionGroupSetsNoAcl()).thenReturn(cogs); - when(periodDataProvider.getAvailableYears(DATABASE)) - .thenReturn(List.of(2018, 2019, now().getYear())); + mockPeriodYears(List.of(2018, 2019, now().getYear())); List availableDataYears = periodDataProvider.getAvailableYears(DATABASE); @@ -921,15 +893,13 @@ void verifyTeaTypeOrgUnitFetchesOuNameWhenPopulatingEventAnalyticsTable() { programA.setProgramAttributes(List.of(programTrackedEntityAttribute)); - when(periodDataProvider.getAvailableYears(DATABASE)) - .thenReturn(List.of(2018, 2019, now().getYear())); + when(idObjectManager.getAllNoAcl(Program.class)).thenReturn(List.of(programA)); + mockPeriodYears(List.of(2018, 2019, now().getYear())); List availableDataYears = periodDataProvider.getAvailableYears(DATABASE); int startYear = availableDataYears.get(0); int latestYear = availableDataYears.get(availableDataYears.size() - 1); - when(idObjectManager.getAllNoAcl(Program.class)).thenReturn(List.of(programA)); - when(jdbcTemplate.queryForList( "select temp.supportedyear from (select distinct extract(year from " + DATE_CLAUSE @@ -1018,4 +988,9 @@ private String getYearQueryForCurrentYear( return sql; } + + private void mockPeriodYears(List years) { + when(analyticsTableSettings.getPeriodSource()).thenReturn(PeriodSource.DATABASE); + when(periodDataProvider.getAvailableYears(DATABASE)).thenReturn(years); + } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManagerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManagerTest.java index 9ec3e6847827..b399999ff371 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManagerTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcOwnershipAnalyticsTableManagerTest.java @@ -87,8 +87,10 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; +import org.mockito.Spy; import org.mockito.invocation.Invocation; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.jdbc.core.JdbcTemplate; @@ -135,7 +137,9 @@ class JdbcOwnershipAnalyticsTableManagerTest extends TestBase { @Mock private PeriodDataProvider periodDataProvider; - private final SqlBuilder sqlBuilder = new PostgreSqlBuilder(); + @Spy private final SqlBuilder sqlBuilder = new PostgreSqlBuilder(); + + @InjectMocks private JdbcOwnershipAnalyticsTableManager manager; private static final Program programA = createProgram('A'); @@ -147,27 +151,9 @@ class JdbcOwnershipAnalyticsTableManagerTest extends TestBase { private static AnalyticsTablePartition partitionA; - private JdbcOwnershipAnalyticsTableManager target; - @BeforeEach public void setUp() { lenient().when(settingsProvider.getCurrentSettings()).thenReturn(SystemSettings.of(Map.of())); - target = - new JdbcOwnershipAnalyticsTableManager( - idObjectManager, - organisationUnitService, - categoryService, - settingsProvider, - dataApprovalLevelService, - resourceTableService, - tableHookService, - partitionManager, - databaseInfoProvider, - jdbcTemplate, - jdbcConfiguration, - analyticsTableSettings, - periodDataProvider, - sqlBuilder); tableA = new AnalyticsTable( @@ -188,7 +174,7 @@ public void setUp() { @Test void testGetAnalyticsTableType() { - assertEquals(AnalyticsTableType.OWNERSHIP, target.getAnalyticsTableType()); + assertEquals(AnalyticsTableType.OWNERSHIP, manager.getAnalyticsTableType()); } @Test @@ -197,19 +183,19 @@ void testGetAnalyticsTables() { AnalyticsTableUpdateParams params = AnalyticsTableUpdateParams.newBuilder().build(); - assertEquals(List.of(tableA, tableB), target.getAnalyticsTables(params)); + assertEquals(List.of(tableA, tableB), manager.getAnalyticsTables(params)); params = AnalyticsTableUpdateParams.newBuilder() .lastYears(AnalyticsTablePartition.LATEST_PARTITION) .build(); - assertEquals(emptyList(), target.getAnalyticsTables(params)); + assertEquals(emptyList(), manager.getAnalyticsTables(params)); } @Test void testGetPartitionChecks() { - assertTrue(target.getPartitionChecks(1, new Date()).isEmpty()); + assertTrue(manager.getPartitionChecks(1, new Date()).isEmpty()); } @Test @@ -275,7 +261,7 @@ void testPopulateTable() throws SQLException { try (MockedStatic mocked = mockStatic(JdbcOwnershipWriter.class)) { mocked.when(() -> JdbcOwnershipWriter.getInstance(any())).thenReturn(writer); - target.populateTable(params, partitionA); + manager.populateTable(params, partitionA); } List jdbcInvocations = getInvocations(jdbcTemplate); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityAnalyticsTableManagerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityAnalyticsTableManagerTest.java index b0377a320e30..606d62214bbe 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityAnalyticsTableManagerTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityAnalyticsTableManagerTest.java @@ -68,7 +68,6 @@ @ExtendWith(MockitoExtension.class) class JdbcTrackedEntityAnalyticsTableManagerTest { - @Mock private JdbcTemplate jdbcTemplate; @Mock private AnalyticsTableSettings analyticsTableSettings; @Mock private PeriodDataProvider periodDataProvider; @@ -109,7 +108,6 @@ void verifyNonConfidentialTeasAreSkipped() { when(sqlBuilder.qualifyTable(anyString())) .thenAnswer(inv -> SqlUtils.quote(inv.getArgument(0))); - when(sqlBuilder.jsonExtract(anyString(), anyString(), anyString())).thenReturn("jsonExtract"); when(sqlBuilder.jsonExtract(anyString(), anyString())).thenReturn("jsonExtract"); when(sqlBuilder.coalesce(anyString(), anyString())) diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityEventsAnalyticsTableManagerTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityEventsAnalyticsTableManagerTest.java new file mode 100644 index 000000000000..353a3be397c0 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/JdbcTrackedEntityEventsAnalyticsTableManagerTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2004-2022, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.analytics.table; + +import static java.time.LocalDate.now; +import static org.hisp.dhis.analytics.AnalyticsTableType.TRACKED_ENTITY_INSTANCE_EVENTS; +import static org.hisp.dhis.analytics.util.SqlComparisonUtils.normalizeSql; +import static org.hisp.dhis.period.PeriodDataProvider.PeriodSource.DATABASE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.startsWith; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import org.hisp.dhis.analytics.AnalyticsTableHookService; +import org.hisp.dhis.analytics.AnalyticsTableUpdateParams; +import org.hisp.dhis.analytics.partition.PartitionManager; +import org.hisp.dhis.analytics.table.model.AnalyticsTable; +import org.hisp.dhis.analytics.table.model.AnalyticsTablePartition; +import org.hisp.dhis.analytics.table.setting.AnalyticsTableSettings; +import org.hisp.dhis.category.CategoryService; +import org.hisp.dhis.common.IdentifiableObjectManager; +import org.hisp.dhis.dataapproval.DataApprovalLevelService; +import org.hisp.dhis.db.sql.PostgreSqlBuilder; +import org.hisp.dhis.db.sql.PostgresAnalyticsSqlBuilder; +import org.hisp.dhis.organisationunit.OrganisationUnitService; +import org.hisp.dhis.period.PeriodDataProvider; +import org.hisp.dhis.resourcetable.ResourceTableService; +import org.hisp.dhis.setting.SystemSettingsProvider; +import org.hisp.dhis.system.database.DatabaseInfoProvider; +import org.hisp.dhis.trackedentity.TrackedEntityAttributeService; +import org.hisp.dhis.trackedentity.TrackedEntityType; +import org.hisp.dhis.trackedentity.TrackedEntityTypeService; +import org.joda.time.DateTime; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.jdbc.core.JdbcTemplate; + +@ExtendWith(MockitoExtension.class) +class JdbcTrackedEntityEventsAnalyticsTableManagerTest { + + @Mock private JdbcTemplate jdbcTemplate; + @Mock private AnalyticsTableSettings analyticsTableSettings; + @Mock private PeriodDataProvider periodDataProvider; + @Spy private PostgreSqlBuilder sqlBuilder; + @Spy private PostgresAnalyticsSqlBuilder analyticsSqlBuilder; + @Mock private PartitionManager partitionManager; + @Mock private SystemSettingsProvider systemSettingsProvider; + @Mock private IdentifiableObjectManager identifiableObjectManager; + @Mock private TrackedEntityTypeService trackedEntityTypeService; + @Mock private TrackedEntityAttributeService trackedEntityAttributeService; + @Mock private CategoryService categoryService; + @Mock private DataApprovalLevelService dataApprovalLevelService; + @Mock private OrganisationUnitService organisationUnitService; + @Mock private ResourceTableService resourceTableService; + @Mock private AnalyticsTableHookService analyticsTableHookService; + @Mock private DatabaseInfoProvider databaseInfoProvider; + + @InjectMocks private JdbcTrackedEntityEventsAnalyticsTableManager tableManager; + + private static final Date START_TIME = new DateTime(2019, 8, 1, 0, 0).toDate(); + + @Test + void testTableName() { + assertEquals(TRACKED_ENTITY_INSTANCE_EVENTS.getTableName(), tableManager.getTableName()); + } + + @Test + void testPopulate() { + + TrackedEntityType tet = mock(TrackedEntityType.class); + when(tet.getUid()).thenReturn("tetUid"); + when(trackedEntityTypeService.getAllTrackedEntityType()).thenReturn(List.of(tet)); + + when(periodDataProvider.getAvailableYears(DATABASE)) + .thenReturn(List.of(2018, 2019, now().getYear())); + + when(jdbcTemplate.queryForList(startsWith("select temp.supportedyear from"), any(Class.class))) + .thenReturn(new ArrayList<>(List.of(2018, 2019))); + + AnalyticsTableUpdateParams params = + AnalyticsTableUpdateParams.newBuilder().lastYears(2).startTime(START_TIME).build(); + List analyticsTables = tableManager.getAnalyticsTables(params); + assertFalse(analyticsTables.isEmpty()); + AnalyticsTablePartition partition = new AnalyticsTablePartition(analyticsTables.get(0)); + + tableManager.populateTable(params, partition); + + String subQuery = + """ + (select json_object_agg(l2.keys, l2.datavalue) as value + from ( + select l1.uid, + l1.keys, + json_strip_nulls(json_build_object( + 'value', l1.eventdatavalues -> l1.keys ->> 'value', + 'created', l1.eventdatavalues -> l1.keys ->> 'created', + 'storedBy', l1.eventdatavalues -> l1.keys ->> 'storedBy', + 'lastUpdated', l1.eventdatavalues -> l1.keys ->> 'lastUpdated', + 'providedElsewhere', l1.eventdatavalues -> l1.keys -> 'providedElsewhere', + 'value_name', (select ou.name + from organisationunit ou + where ou.uid = l1.eventdatavalues -> l1.keys ->> 'value'), + 'value_code', (select ou.code + from organisationunit ou + where ou.uid = l1.eventdatavalues -> l1.keys ->> 'value'))) as datavalue + from (select inner_evt.*, jsonb_object_keys(inner_evt.eventdatavalues) keys + from event inner_evt) as l1) as l2 + where l2.uid = ev.uid + group by l2.uid)::jsonb + """; + + String expectedSql = + """ + insert into analytics_te_event_tetuid_temp ("trackedentity","program","enrollment","programstage","event","occurreddate","lastupdated","created", + "scheduleddate","status","uidlevel1","uidlevel2","uidlevel3","uidlevel4","ou","ouname","oucode","oulevel","eventdatavalues","eventgeometry", + "evlongitude","evlatitude","ounamehierarchy") select distinct te.uid,p.uid,en.uid,ps.uid,ev.uid,ev.occurreddate,ev.lastupdated, + ev.created,ev.scheduleddate,ev.status,ous.uidlevel1,ous.uidlevel2,ous.uidlevel3,ous.uidlevel4,ous.organisationunituid,ous.name,ous.code,ous.level, + %s, + ev.geometry,case when 'POINT' = GeometryType(ev.geometry) then ST_X(ev.geometry) end,case when 'POINT' = GeometryType(ev.geometry) then ST_Y(ev.geometry) end,concat_ws(' / ',) as ounamehierarchy + from "event" ev inner join "enrollment" en on en.enrollmentid=ev.enrollmentid + and en.deleted = false inner join "trackedentity" te on te.trackedentityid=en.trackedentityid + and te.deleted = false and te.trackedentitytypeid = 0 + and te.lastupdated < '2019-08-01T00:00:00' left join "programstage" ps on ev.programstageid=ps.programstageid + left join "program" p on ps.programid=p.programid + left join analytics_rs_orgunitstructure ous on ev.organisationunitid=ous.organisationunitid + where ev.status in ('COMPLETED','ACTIVE','SCHEDULE') + and (CASE WHEN 'SCHEDULE' = ev.status THEN ev.scheduleddate ELSE ev.occurreddate END) >= 'null' + and (CASE WHEN 'SCHEDULE' = ev.status THEN ev.scheduleddate ELSE ev.occurreddate END) < 'null' + and ev.deleted = false""" + .formatted(subQuery); + + ArgumentCaptor sqlCaptor = ArgumentCaptor.forClass(String.class); + verify(jdbcTemplate, times(1)).execute(sqlCaptor.capture()); // verify it was called twice + assertEquals(normalizeSql(expectedSql), normalizeSql(sqlCaptor.getAllValues().get(0))); // che + } +} diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/model/AnalyticsTableColumnTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/model/AnalyticsTableColumnTest.java index 3492631a5a36..a016958cacfe 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/model/AnalyticsTableColumnTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/model/AnalyticsTableColumnTest.java @@ -41,7 +41,6 @@ * @author Lars Helge Overland */ class AnalyticsTableColumnTest { - @Test void testIsNotNull() { AnalyticsTableColumn colA = diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/scheduling/ContinuousAnalyticsTableJobTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/scheduling/ContinuousAnalyticsTableJobTest.java index cd4c7b92641a..581a9a70d1bc 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/scheduling/ContinuousAnalyticsTableJobTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/scheduling/ContinuousAnalyticsTableJobTest.java @@ -40,6 +40,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -48,11 +49,12 @@ class ContinuousAnalyticsTableJobTest { @Mock private AnalyticsTableGenerator analyticsTableGenerator; @Mock private SystemSettingsService settingsService; + @Mock private SystemSettings settings; @Mock private TableInfoReader tableInfoReader; - private ContinuousAnalyticsTableJob job; + @InjectMocks private ContinuousAnalyticsTableJob job; private final Date dateA = getDate(2024, 1, 4, 23, 0); private final Date dateB = getDate(2024, 1, 5, 2, 0); @@ -61,8 +63,6 @@ class ContinuousAnalyticsTableJobTest { @BeforeEach public void beforeEach() { when(settingsService.getCurrentSettings()).thenReturn(settings); - job = - new ContinuousAnalyticsTableJob(analyticsTableGenerator, settingsService, tableInfoReader); } @Test diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/setting/AnalyticsTableSettingsTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/setting/AnalyticsTableSettingsTest.java index ef1bb6236b94..8a860b571a2e 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/setting/AnalyticsTableSettingsTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/table/setting/AnalyticsTableSettingsTest.java @@ -37,9 +37,9 @@ import org.hisp.dhis.external.conf.ConfigurationKey; import org.hisp.dhis.external.conf.DhisConfigurationProvider; import org.hisp.dhis.setting.SystemSettingsService; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -49,12 +49,7 @@ class AnalyticsTableSettingsTest { @Mock private SystemSettingsService systemSettings; - private AnalyticsTableSettings settings; - - @BeforeEach - public void before() { - settings = new AnalyticsTableSettings(config, systemSettings); - } + @InjectMocks private AnalyticsTableSettings settings; @Test void testGetAndValidateDatabase() { diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/TrackedEntityAnalyticsDimensionsServiceTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/TrackedEntityAnalyticsDimensionsServiceTest.java index 040a647c90ef..cb98c9e24b7c 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/TrackedEntityAnalyticsDimensionsServiceTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/TrackedEntityAnalyticsDimensionsServiceTest.java @@ -28,57 +28,43 @@ package org.hisp.dhis.analytics.trackedentity; import static java.util.Collections.emptySet; -import static org.hisp.dhis.analytics.common.AnalyticsDimensionsTestSupport.allValueTypeDataElements; -import static org.hisp.dhis.analytics.common.AnalyticsDimensionsTestSupport.allValueTypeTEAs; import static org.hisp.dhis.analytics.common.AnalyticsDimensionsTestSupport.trackedEntityType; import static org.hisp.dhis.analytics.common.DimensionServiceCommonTest.queryDisallowedValueTypesPredicate; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.Collection; -import org.hisp.dhis.analytics.event.data.DefaultEnrollmentAnalyticsDimensionsService; import org.hisp.dhis.common.BaseIdentifiableObject; import org.hisp.dhis.common.PrefixedDimension; import org.hisp.dhis.dataelement.DataElement; -import org.hisp.dhis.program.Program; import org.hisp.dhis.program.ProgramService; -import org.hisp.dhis.security.acl.AclService; import org.hisp.dhis.trackedentity.TrackedEntityAttribute; import org.hisp.dhis.trackedentity.TrackedEntityTypeService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; -/** Unit tests for {@link TrackedEntityAnalyticsDimensionsService}. */ +@ExtendWith(MockitoExtension.class) class TrackedEntityAnalyticsDimensionsServiceTest { - private TrackedEntityAnalyticsDimensionsService trackedEntityAnalyticsDimensionsService; + @Mock private TrackedEntityTypeService trackedEntityTypeService; + + @Mock private ProgramService programService; + + @InjectMocks private DefaultTrackedEntityAnalyticsDimensionsService service; @BeforeEach void setup() { - ProgramService programService = mock(ProgramService.class); - Program program = mock(Program.class); - TrackedEntityTypeService trackedEntityTypeService = mock(TrackedEntityTypeService.class); - - when(programService.getProgram(any())).thenReturn(program); - when(program.getDataElements()).thenReturn(allValueTypeDataElements()); - when(program.getProgramIndicators()).thenReturn(emptySet()); - when(program.getTrackedEntityAttributes()).thenReturn(allValueTypeTEAs()); when(trackedEntityTypeService.getTrackedEntityType(any())).thenReturn(trackedEntityType()); - - trackedEntityAnalyticsDimensionsService = - new DefaultTrackedEntityAnalyticsDimensionsService( - trackedEntityTypeService, - new DefaultEnrollmentAnalyticsDimensionsService(programService, mock(AclService.class)), - programService); } @Test void testQueryDoesNotContainDisallowedValueTypes() { Collection analyticsDimensions = - trackedEntityAnalyticsDimensionsService - .getQueryDimensionsByTrackedEntityTypeId("aTeiId", emptySet()) - .stream() + service.getQueryDimensionsByTrackedEntityTypeId("aTeiId", emptySet()).stream() .map(PrefixedDimension::getItem) .toList(); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/TrackedEntityQueryRequestValidatorTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/TrackedEntityQueryRequestValidatorTest.java index 401d6acceae6..1e1b37f8629f 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/TrackedEntityQueryRequestValidatorTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/TrackedEntityQueryRequestValidatorTest.java @@ -40,42 +40,35 @@ * @author maikel arabori */ class TrackedEntityQueryRequestValidatorTest { - @Test void testValidateWhenTrackedEntityTypeIsInvalid() { String teiUid = CodeGenerator.generateUid() + "invalid"; - // Given TrackedEntityRequestParams trackedEntityRequestParams = new TrackedEntityRequestParams(teiUid); TrackedEntityQueryRequestValidator trackedEntityQueryRequestValidator = new TrackedEntityQueryRequestValidator(); - // When IllegalQueryException exception = assertThrows( IllegalQueryException.class, () -> trackedEntityQueryRequestValidator.validate(trackedEntityRequestParams)); - // Then assertEquals( "Invalid UID `" + teiUid + "` for property `trackedEntityType`", exception.getMessage()); } @Test void testValidateWhenNoTrackedEntityType() { - // Given TrackedEntityRequestParams trackedEntityRequestParams = new TrackedEntityRequestParams(null); TrackedEntityQueryRequestValidator trackedEntityQueryRequestValidator = new TrackedEntityQueryRequestValidator(); - // When IllegalQueryException exception = assertThrows( IllegalQueryException.class, () -> trackedEntityQueryRequestValidator.validate(trackedEntityRequestParams)); - // Then assertEquals("Invalid UID `null` for property `trackedEntityType`", exception.getMessage()); } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/TrackedEntityRequestParamsMapperTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/TrackedEntityRequestParamsMapperTest.java index d7d088f3aed7..3b6159970d1a 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/TrackedEntityRequestParamsMapperTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/TrackedEntityRequestParamsMapperTest.java @@ -30,7 +30,6 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import java.util.Set; @@ -40,22 +39,19 @@ import org.hisp.dhis.program.ProgramService; import org.hisp.dhis.trackedentity.TrackedEntityType; import org.hisp.dhis.trackedentity.TrackedEntityTypeService; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +@ExtendWith(MockitoExtension.class) class TrackedEntityRequestParamsMapperTest { - private final TrackedEntityTypeService trackedEntityTypeService = - mock(TrackedEntityTypeService.class); + @Mock private TrackedEntityTypeService trackedEntityTypeService; - private final ProgramService programService = mock(ProgramService.class); + @Mock private ProgramService programService; - private TrackedEntityQueryRequestMapper trackedEntityQueryRequestMapper; - - @BeforeEach - public void setUp() { - trackedEntityQueryRequestMapper = - new TrackedEntityQueryRequestMapper(trackedEntityTypeService, programService); - } + @InjectMocks private TrackedEntityQueryRequestMapper mapper; @Test void testOneProgramFailing() { @@ -86,8 +82,7 @@ void testValidateTrackedEntityType(String trackedEntityTypeUid, String expectedM final IllegalQueryException thrown = assertThrows( - IllegalQueryException.class, - () -> trackedEntityQueryRequestMapper.map(trackedEntityTypeUid, requestParams)); + IllegalQueryException.class, () -> mapper.map(trackedEntityTypeUid, requestParams)); assertEquals(expectedMessage, thrown.getMessage()); } @@ -109,8 +104,7 @@ void testOK() { when(programService.getPrograms(Set.of("A", "B"))).thenReturn(Set.of(programA, programB)); - assertDoesNotThrow( - () -> trackedEntityQueryRequestMapper.map(trackedEntityTypeUid, requestParams)); + assertDoesNotThrow(() -> mapper.map(trackedEntityTypeUid, requestParams)); } @Test @@ -124,10 +118,7 @@ void testOKWhenNoPrograms() { when(trackedEntityTypeService.getTrackedEntityType(trackedEntityTypeUid)) .thenReturn(trackedEntityType); - when(programService.getPrograms(Set.of("A", "B"))).thenReturn(Set.of()); - - assertDoesNotThrow( - () -> trackedEntityQueryRequestMapper.map(trackedEntityTypeUid, requestParams)); + assertDoesNotThrow(() -> mapper.map(trackedEntityTypeUid, requestParams)); } private Program stubProgram(String uid, String tetUid) { diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/DataElementConditionTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/DataElementConditionTest.java index 23d66775b561..8047be617731 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/DataElementConditionTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/DataElementConditionTest.java @@ -62,7 +62,6 @@ void testWhenNoLegendSet() { SqlParameterManager sqlParameterManager = new SqlParameterManager(); QueryContext queryContext = QueryContext.of(null, sqlParameterManager); - // SETUP DimensionParam dimensionParam = mock(DimensionParam.class); QueryItem queryItem = mock(QueryItem.class); DimensionParamItem dimensionParamItem = mock(DimensionParamItem.class); @@ -78,13 +77,11 @@ void testWhenNoLegendSet() { when(dimensionParamItem.getOperator()).thenReturn(AnalyticsQueryOperator.of(QueryOperator.EQ)); when(dimensionParamItem.getValues()).thenReturn(List.of("value")); - // CALL DataElementCondition dataElementCondition = DataElementCondition.of(queryContext, dimensionIdentifier); String rendered = dataElementCondition.render(); - // ASSERT assertEquals("(\"eventdatavalues\" -> 'uid' ->> 'value')::TEXT = :1", rendered); assertEquals("value", queryContext.getParametersPlaceHolder().get("1")); } @@ -94,7 +91,6 @@ void testWhenLegendSet() { SqlParameterManager sqlParameterManager = new SqlParameterManager(); QueryContext queryContext = QueryContext.of(null, sqlParameterManager); - // SETUP DimensionParam dimensionParam = mock(DimensionParam.class); QueryItem queryItem = mock(QueryItem.class); DimensionParamItem dimensionParamItem = mock(DimensionParamItem.class); @@ -105,7 +101,6 @@ void testWhenLegendSet() { when(dimensionParam.getUid()).thenReturn("uid"); when(queryItem.hasLegendSet()).thenReturn(true); - // in the assertion we will check that the type is force to DECIMAL when(dimensionParam.getValueType()).thenReturn(ValueType.TEXT); when(dimensionParam.getItems()).thenReturn(List.of(dimensionParamItem)); @@ -121,13 +116,11 @@ void testWhenLegendSet() { when(legend.getStartValue()).thenReturn(1.0); when(legend.getEndValue()).thenReturn(2.0); - // CALL DataElementCondition dataElementCondition = DataElementCondition.of(queryContext, dimensionIdentifier); String rendered = dataElementCondition.render(); - // ASSERT assertEquals( "(\"eventdatavalues\" -> 'uid' ->> 'value')::DECIMAL >= :1 " + "and (\"eventdatavalues\" -> 'uid' ->> 'value')::DECIMAL < :2", diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/OrganisationUnitConditionTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/OrganisationUnitConditionTest.java index 7fac1fb4b9e9..606f35a0cc5a 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/OrganisationUnitConditionTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/OrganisationUnitConditionTest.java @@ -60,11 +60,9 @@ import org.hisp.dhis.trackedentity.TrackedEntityType; import org.junit.jupiter.api.Test; -/** Unit tests for {@link OrganisationUnitCondition}. */ class OrganisationUnitConditionTest { @Test void testTeiOuMultipleOusProduceCorrectSql() { - // Given List ous = List.of("ou1", "ou2"); DimensionIdentifier dimensionIdentifier = stubDimensionIdentifier(ous, null, null); @@ -82,17 +80,14 @@ void testTeiOuMultipleOusProduceCorrectSql() { OrganisationUnitCondition organisationUnitCondition = OrganisationUnitCondition.of(dimensionIdentifier, queryContext); - // When String render = organisationUnitCondition.render(); - // Then assertEquals("t_1.\"ou\" in (:1)", render); assertEquals(ous, queryContext.getParametersPlaceHolder().get("1")); } @Test void testChildrenOuModeProduceCorrectSql() { - // Given List ous = List.of("ou1", "ou2"); DimensionIdentifier dimensionIdentifier = @@ -125,13 +120,11 @@ void testChildrenOuModeProduceCorrectSql() { OrganisationUnitCondition organisationUnitCondition = OrganisationUnitCondition.of(dimensionIdentifier, queryContext); - // When String render = organisationUnitCondition.render(); List expected = ous.stream().flatMap(ouId -> Stream.of(ouId + "_children1", ouId + "_children2")).toList(); - // Then assertEquals("t_1.\"ou\" in (:1)", render); assertTrue( isEqualCollection( @@ -140,7 +133,6 @@ void testChildrenOuModeProduceCorrectSql() { @Test void testDescendantOuModeProduceCorrectSql() { - // Given List ous = List.of("ou1", "ou2"); DimensionIdentifier dimensionIdentifier = @@ -155,7 +147,6 @@ void testDescendantOuModeProduceCorrectSql() { return organisationUnit; }); - // Descendant is the default ouMode. CommonRequestParams requestParams = new CommonRequestParams(); ContextParams contextParams = @@ -169,16 +160,13 @@ void testDescendantOuModeProduceCorrectSql() { OrganisationUnitCondition organisationUnitCondition = OrganisationUnitCondition.of(dimensionIdentifier, queryContext); - // When String render = organisationUnitCondition.render(); - // Then assertEquals("(t_1.\"uidlevel1\" = :1 or t_1.\"uidlevel1\" = :2)", render); } @Test void testEmptyOuProduceFalse() { - // Given List ous = List.of(); DimensionIdentifier dimensionIdentifier = stubDimensionIdentifier(ous, null, null); @@ -197,17 +185,14 @@ void testEmptyOuProduceFalse() { OrganisationUnitCondition organisationUnitCondition = OrganisationUnitCondition.of(dimensionIdentifier, queryContext); - // When String render = organisationUnitCondition.render(); - // Then assertEquals("false", render); assertTrue(queryContext.getParametersPlaceHolder().isEmpty()); } @Test void testTeiOuSingleOusProduceCorrectSql() { - // Given List ous = List.of("ou1"); DimensionIdentifier dimensionIdentifier = @@ -227,17 +212,14 @@ void testTeiOuSingleOusProduceCorrectSql() { OrganisationUnitCondition organisationUnitCondition = OrganisationUnitCondition.of(dimensionIdentifier, queryContext); - // When String render = organisationUnitCondition.render(); - // Then assertEquals("t_1.\"ou\" = :1", render); assertEquals(ous.get(0), queryContext.getParametersPlaceHolder().get("1")); } @Test void testEventOuSingleOusProduceCorrectSql() { - // Given List ous = List.of("ou1"); DimensionIdentifier dimensionIdentifier = @@ -257,17 +239,14 @@ void testEventOuSingleOusProduceCorrectSql() { OrganisationUnitCondition organisationUnitCondition = OrganisationUnitCondition.of(dimensionIdentifier, queryContext); - // When String statement = organisationUnitCondition.render(); - // Then assertEquals("\"Z8z5uu61HAb.tO8L1aBitDm\".\"ou\" = :1", statement); assertEquals(ous.get(0), queryContext.getParametersPlaceHolder().get("1")); } @Test void testEventOuMultipleOusProduceCorrectSql() { - // Given List ous = List.of("ou1", "ou2"); DimensionIdentifier dimensionIdentifier = @@ -291,17 +270,14 @@ void testEventOuMultipleOusProduceCorrectSql() { OrganisationUnitCondition organisationUnitCondition = OrganisationUnitCondition.of(dimensionIdentifier, queryContext); - // When String statement = organisationUnitCondition.render(); - // Then assertEquals("\"Z8z5uu61HAb.tO8L1aBitDm\".\"ou\" in (:1)", statement); assertEquals(ous, queryContext.getParametersPlaceHolder().get("1")); } @Test void testEnrollmentOuSingleOusProduceCorrectSql() { - // Given List ous = List.of("ou1"); DimensionIdentifier dimensionIdentifier = @@ -325,17 +301,14 @@ void testEnrollmentOuSingleOusProduceCorrectSql() { OrganisationUnitCondition organisationUnitCondition = OrganisationUnitCondition.of(dimensionIdentifier, queryContext); - // When String statement = organisationUnitCondition.render(); - // Then assertEquals("\"Z8z5uu61HAb\".\"ou\" = :1", statement); assertEquals(ous.get(0), queryContext.getParametersPlaceHolder().get("1")); } @Test void testEnrollmentOuMultipleOusProduceCorrectSql() { - // Given List ous = List.of("ou1", "ou2"); DimensionIdentifier dimensionIdentifier = @@ -358,10 +331,8 @@ void testEnrollmentOuMultipleOusProduceCorrectSql() { OrganisationUnitCondition organisationUnitCondition = OrganisationUnitCondition.of(dimensionIdentifier, queryContext); - // When String statement = organisationUnitCondition.render(); - // Then assertEquals("\"Z8z5uu61HAb\".\"ou\" in (:1)", statement); assertEquals(ous, queryContext.getParametersPlaceHolder().get("1")); } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/RenderableDataValueTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/RenderableDataValueTest.java index 547005a9ac66..edfe1644c305 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/RenderableDataValueTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/RenderableDataValueTest.java @@ -39,7 +39,6 @@ import org.junit.jupiter.api.Test; class RenderableDataValueTest { - @Test void testRender() { RenderableDataValue renderableDataValue = diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/StatusConditionTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/StatusConditionTest.java index 11e563192ba8..6be1c6d0a446 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/StatusConditionTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/StatusConditionTest.java @@ -46,7 +46,6 @@ class StatusConditionTest { @Test void testProgramStatusCompletedProduceCorrectSql() { - // SETUP List values = List.of("COMPLETED"); DimensionIdentifier dimensionIdentifier = @@ -56,7 +55,6 @@ void testProgramStatusCompletedProduceCorrectSql() { DimensionParam.StaticDimension.ENROLLMENT_STATUS, values); - // CALL SqlParameterManager sqlParameterManager = new SqlParameterManager(); QueryContext queryContext = QueryContext.of(null, sqlParameterManager); @@ -70,7 +68,6 @@ void testProgramStatusCompletedProduceCorrectSql() { @Test void testProgramStatusCompletedActiveProduceCorrectSql() { - // SETUP List values = List.of("COMPLETED", "ACTIVE"); DimensionIdentifier dimensionIdentifier = @@ -80,7 +77,6 @@ void testProgramStatusCompletedActiveProduceCorrectSql() { DimensionParam.StaticDimension.ENROLLMENT_STATUS, values); - // CALL SqlParameterManager sqlParameterManager = new SqlParameterManager(); QueryContext queryContext = QueryContext.of(null, sqlParameterManager); @@ -94,14 +90,12 @@ void testProgramStatusCompletedActiveProduceCorrectSql() { @Test void testEventStatusCompletedProduceCorrectSql() { - // SETUP List values = List.of("COMPLETED"); DimensionIdentifier dimensionIdentifier = getProgramAttributeDimensionIdentifier( "programUid", "programStageUid", DimensionParam.StaticDimension.EVENT_STATUS, values); - // CALL SqlParameterManager sqlParameterManager = new SqlParameterManager(); QueryContext queryContext = QueryContext.of(null, sqlParameterManager); @@ -115,14 +109,12 @@ void testEventStatusCompletedProduceCorrectSql() { @Test void testEventStatusCompletedScheduleProduceCorrectSql() { - // SETUP List values = List.of("COMPLETED", "SCHEDULE"); DimensionIdentifier dimensionIdentifier = getProgramAttributeDimensionIdentifier( "programUid", "programStageUid", DimensionParam.StaticDimension.EVENT_STATUS, values); - // CALL SqlParameterManager sqlParameterManager = new SqlParameterManager(); QueryContext queryContext = QueryContext.of(null, sqlParameterManager); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/TrackedEntityAttributeConditionTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/TrackedEntityAttributeConditionTest.java index d8f15c6cffa3..21bd12db5e88 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/TrackedEntityAttributeConditionTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/TrackedEntityAttributeConditionTest.java @@ -46,13 +46,11 @@ class TrackedEntityAttributeConditionTest { @Test void testProgramAttributeConditionProduceCorrectSql() { - // SETUP String attr = "attr"; List values = List.of("eq:v1"); DimensionIdentifier dimensionIdentifier = getProgramAttributeDimensionIdentifier("attr", values); - // CALL SqlParameterManager sqlParameterManager = new SqlParameterManager(); QueryContext queryContext = QueryContext.of(null, sqlParameterManager); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/context/querybuilder/OffsetHelperTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/context/querybuilder/OffsetHelperTest.java index ba1176e86bd4..5ddfb28318ea 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/context/querybuilder/OffsetHelperTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/context/querybuilder/OffsetHelperTest.java @@ -35,21 +35,17 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -/** Tests for {@link OffsetHelper}. */ class OffsetHelperTest { @ParameterizedTest(name = "testGetItemBasedOnOffset - {index}") @CsvSource({"2,d", "1,e", "0,a", "-1,b", "-2,c"}) void testGetItemBasedOnOffset(String offsetParam, String expectedResponse) { - // Given Stream stream = Stream.of("a", "b", "c", "d", "e"); Comparator comparator = Comparator.naturalOrder(); int offset = Integer.parseInt(offsetParam); - // When Optional result = OffsetHelper.getItemBasedOnOffset(stream, comparator, offset); - // Then Assertions.assertTrue(result.isPresent()); Assertions.assertEquals(expectedResponse, result.get()); } @@ -57,10 +53,8 @@ void testGetItemBasedOnOffset(String offsetParam, String expectedResponse) { @ParameterizedTest @CsvSource({"1,1,asc", "2,2,asc", "0,1,desc", "-1,2,desc", "-2,3,desc"}) void testGetOffset(String offsetParam, String expectedOffset, String expectedDirection) { - // When Offset offset = OffsetHelper.getOffset(Integer.parseInt(offsetParam)); - // Then Assertions.assertEquals(expectedOffset, offset.offset()); Assertions.assertEquals(expectedDirection, offset.direction()); } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/context/querybuilder/SqlQueryHelperTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/context/querybuilder/SqlQueryHelperTest.java index 3bd06e1d8ae6..286a29c25fee 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/context/querybuilder/SqlQueryHelperTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/context/querybuilder/SqlQueryHelperTest.java @@ -41,18 +41,27 @@ import org.hisp.dhis.program.Program; import org.hisp.dhis.program.ProgramStage; import org.hisp.dhis.trackedentity.TrackedEntityType; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; -/** Unit tests for {@link SqlQueryHelper}. */ +@ExtendWith(MockitoExtension.class) @SuppressWarnings("unchecked") class SqlQueryHelperTest { - @Test - void test_throws_when_undetected_type() { - DimensionParam dimensionParam = mock(DimensionParam.class); - DimensionIdentifier testedDimension = mock(DimensionIdentifier.class); + @Mock private DimensionParam dimensionParam; + + @Mock private DimensionIdentifier testedDimension; + + @BeforeEach + void beforeEach() { when(testedDimension.getDimension()).thenReturn(dimensionParam); + } + @Test + void test_throws_when_undetected_type() { assertThrows( IllegalArgumentException.class, () -> SqlQueryHelper.buildOrderSubQuery(testedDimension, () -> "field")); @@ -64,10 +73,6 @@ void test_throws_when_undetected_type() { @Test void test_subQuery_TE() { - DimensionParam dimensionParam = mock(DimensionParam.class); - DimensionIdentifier testedDimension = mock(DimensionIdentifier.class); - when(testedDimension.getDimension()).thenReturn(dimensionParam); - when(testedDimension.isTeDimension()).thenReturn(true); assertEquals( @@ -80,9 +85,6 @@ void test_subQuery_TE() { @Test void test_subQuery_enrollment() { - DimensionParam dimensionParam = mock(DimensionParam.class); - DimensionIdentifier testedDimension = mock(DimensionIdentifier.class); - when(testedDimension.getDimension()).thenReturn(dimensionParam); when(testedDimension.getPrefix()).thenReturn("prefix"); TrackedEntityType trackedEntityType = mock(TrackedEntityType.class); @@ -100,34 +102,31 @@ void test_subQuery_enrollment() { assertEquals( """ - (select field - from (select *, - row_number() over ( partition by trackedentity - order by enrollmentdate desc ) as rn - from analytics_te_enrollment_trackedentitytype - where program = 'programUid' - and t_1.trackedentity = trackedentity) en - where en.rn = 1)""", + (select field + from (select *, + row_number() over ( partition by trackedentity + order by enrollmentdate desc ) as rn + from analytics_te_enrollment_trackedentitytype + where program = 'programUid' + and t_1.trackedentity = trackedentity) en + where en.rn = 1)""", SqlQueryHelper.buildOrderSubQuery(testedDimension, () -> "field").render()); assertEquals( """ - exists(select 1 - from (select * - from (select *, row_number() over (partition by trackedentity order by enrollmentdate desc) as rn - from analytics_te_enrollment_trackedentitytype - where program = 'programUid' - and trackedentity = t_1.trackedentity) en - where en.rn = 1) as "prefix" - where field)""", + exists(select 1 + from (select * + from (select *, row_number() over (partition by trackedentity order by enrollmentdate desc) as rn + from analytics_te_enrollment_trackedentitytype + where program = 'programUid' + and trackedentity = t_1.trackedentity) en + where en.rn = 1) as "prefix" + where field)""", SqlQueryHelper.buildExistsValueSubquery(testedDimension, () -> "field").render()); } @Test void test_subQuery_event() { - DimensionParam dimensionParam = mock(DimensionParam.class); - DimensionIdentifier testedDimension = mock(DimensionIdentifier.class); - when(testedDimension.getDimension()).thenReturn(dimensionParam); when(testedDimension.getPrefix()).thenReturn("prefix"); TrackedEntityType trackedEntityType = mock(TrackedEntityType.class); @@ -149,53 +148,48 @@ void test_subQuery_event() { assertEquals( """ - (select field - from (select *, - row_number() over ( partition by enrollment - order by occurreddate desc ) as rn - from analytics_te_event_trackedentitytype events - where programstage = 'programStageUid' - and enrollment = (select enrollment - from (select *, - row_number() over ( partition by trackedentity - order by enrollmentdate desc ) as rn - from analytics_te_enrollment_trackedentitytype - where program = 'programUid' - and t_1.trackedentity = trackedentity) en - where en.rn = 1) - and status != 'SCHEDULE') ev - where ev.rn = 1)""", + (select field + from (select *, + row_number() over ( partition by enrollment + order by occurreddate desc ) as rn + from analytics_te_event_trackedentitytype events + where programstage = 'programStageUid' + and enrollment = (select enrollment + from (select *, + row_number() over ( partition by trackedentity + order by enrollmentdate desc ) as rn + from analytics_te_enrollment_trackedentitytype + where program = 'programUid' + and t_1.trackedentity = trackedentity) en + where en.rn = 1) + and status != 'SCHEDULE') ev + where ev.rn = 1)""", SqlQueryHelper.buildOrderSubQuery(testedDimension, () -> "field").render()); assertEquals( """ - exists(select 1 - from (select * - from (select *, row_number() over (partition by trackedentity order by enrollmentdate desc) as rn - from analytics_te_enrollment_trackedentitytype - where program = 'programUid' - and trackedentity = t_1.trackedentity) en - where en.rn = 1) as "enrollmentSubqueryAlias" - where exists(select 1 - from (select * - from (select *, row_number() over ( partition by enrollment order by occurreddate desc ) as rn - from analytics_te_event_trackedentitytype - where "enrollmentSubqueryAlias".enrollment = enrollment - and programstage = 'programStageUid' - and status != 'SCHEDULE') ev - where ev.rn = 1) as "prefix" - where field))""", + exists(select 1 + from (select * + from (select *, row_number() over (partition by trackedentity order by enrollmentdate desc) as rn + from analytics_te_enrollment_trackedentitytype + where program = 'programUid' + and trackedentity = t_1.trackedentity) en + where en.rn = 1) as "enrollmentSubqueryAlias" + where exists(select 1 + from (select * + from (select *, row_number() over ( partition by enrollment order by occurreddate desc ) as rn + from analytics_te_event_trackedentitytype + where "enrollmentSubqueryAlias".enrollment = enrollment + and programstage = 'programStageUid' + and status != 'SCHEDULE') ev + where ev.rn = 1) as "prefix" + where field))""", SqlQueryHelper.buildExistsValueSubquery(testedDimension, () -> "field").render()); } @Test void test_subQuery_data_element() { - DimensionParam dimensionParam = mock(DimensionParam.class); - when(dimensionParam.isOfType(any())).thenReturn(true); - - DimensionIdentifier testedDimension = mock(DimensionIdentifier.class); - when(testedDimension.getDimension()).thenReturn(dimensionParam); when(testedDimension.getPrefix()).thenReturn("prefix"); TrackedEntityType trackedEntityType = mock(TrackedEntityType.class); @@ -217,47 +211,47 @@ void test_subQuery_data_element() { assertEquals( """ - (select field - from analytics_te_event_trackedentitytype - where event = (select event - from (select *, - row_number() over ( partition by enrollment - order by occurreddate desc ) as rn - from analytics_te_event_trackedentitytype events - where programstage = 'programStageUid' - and enrollment = (select enrollment - from (select *, - row_number() over ( partition by trackedentity - order by enrollmentdate desc ) as rn - from analytics_te_enrollment_trackedentitytype - where program = 'programUid' - and t_1.trackedentity = trackedentity) en - where en.rn = 1) - and status != 'SCHEDULE') ev - where ev.rn = 1))""", + (select field + from analytics_te_event_trackedentitytype + where event = (select event + from (select *, + row_number() over ( partition by enrollment + order by occurreddate desc ) as rn + from analytics_te_event_trackedentitytype events + where programstage = 'programStageUid' + and enrollment = (select enrollment + from (select *, + row_number() over ( partition by trackedentity + order by enrollmentdate desc ) as rn + from analytics_te_enrollment_trackedentitytype + where program = 'programUid' + and t_1.trackedentity = trackedentity) en + where en.rn = 1) + and status != 'SCHEDULE') ev + where ev.rn = 1))""", SqlQueryHelper.buildOrderSubQuery(testedDimension, () -> "field").render()); assertEquals( """ - exists(select 1 - from (select * - from (select *, row_number() over (partition by trackedentity order by enrollmentdate desc) as rn - from analytics_te_enrollment_trackedentitytype - where program = 'programUid' - and trackedentity = t_1.trackedentity) en - where en.rn = 1) as "enrollmentSubqueryAlias" - where exists(select 1 - from (select * - from (select *, row_number() over ( partition by enrollment order by occurreddate desc ) as rn - from analytics_te_event_trackedentitytype - where "enrollmentSubqueryAlias".enrollment = enrollment - and programstage = 'programStageUid' - and status != 'SCHEDULE') ev - where ev.rn = 1) as "prefix" - where exists(select 1 - from analytics_te_event_trackedentitytype - where "prefix".event = event - and field)))""", + exists(select 1 + from (select * + from (select *, row_number() over (partition by trackedentity order by enrollmentdate desc) as rn + from analytics_te_enrollment_trackedentitytype + where program = 'programUid' + and trackedentity = t_1.trackedentity) en + where en.rn = 1) as "enrollmentSubqueryAlias" + where exists(select 1 + from (select * + from (select *, row_number() over ( partition by enrollment order by occurreddate desc ) as rn + from analytics_te_event_trackedentitytype + where "enrollmentSubqueryAlias".enrollment = enrollment + and programstage = 'programStageUid' + and status != 'SCHEDULE') ev + where ev.rn = 1) as "prefix" + where exists(select 1 + from analytics_te_event_trackedentitytype + where "prefix".event = event + and field)))""", SqlQueryHelper.buildExistsValueSubquery(testedDimension, () -> "field").render()); } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/context/sql/SqlQueryCreatorServiceTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/context/sql/SqlQueryCreatorServiceTest.java index ad9781863681..5be9d61daf2a 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/context/sql/SqlQueryCreatorServiceTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/trackedentity/query/context/sql/SqlQueryCreatorServiceTest.java @@ -93,7 +93,6 @@ void setUp() { @Test void testSqlQueryRenderingWithOrgUnitNameObject() { - // given TrackedEntityQueryParams trackedEntityQueryParams = TrackedEntityQueryParams.builder().trackedEntityType(createTrackedEntityType('A')).build(); @@ -104,18 +103,15 @@ void testSqlQueryRenderingWithOrgUnitNameObject() { .commonParsed(stubSortingCommonParams(null, 1, "ouname")) .build(); - // when String sql = sqlQueryCreatorService.getSqlQueryCreator(contextParams).createForSelect().getStatement(); - // then assertTrue(sql.contains("ouname")); assertContains("order by t_1.\"ouname\" desc", sql); } @Test void testSqlQueryRenderingWithCommonDimensionalObject() { - // when DimensionalObject dimensionalObject = new BaseDimensionalObject("abc"); TrackedEntityType trackedEntityType = createTrackedEntityType('A'); @@ -132,17 +128,14 @@ void testSqlQueryRenderingWithCommonDimensionalObject() { .commonParsed(stubSortingCommonParams(program, 1, dimensionalObject)) .build(); - // when String sql = sqlQueryCreatorService.getSqlQueryCreator(contextParams).createForSelect().getStatement(); - // then assertTrue(sql.contains(" order by (select (\"eventdatavalues\" -> 'abc' ->> 'value')::TEXT")); } @Test void testEnrolledInProgramWhenSpecifiedInRequest() { - // given CommonParsedParams commonParsed = CommonParsedParams.builder() .programs(List.of(mockProgram("program1"), mockProgram("program2"))) @@ -162,18 +155,15 @@ void testEnrolledInProgramWhenSpecifiedInRequest() { .commonParsed(commonParsed) .build(); - // when String sql = sqlQueryCreatorService.getSqlQueryCreator(contextParams).createForSelect().getStatement(); - // then assertTrue(sql.contains("ouname")); assertContains("t_1.\"program1\" and t_1.\"program2\"", sql); } @Test void testEnrolledInProgramWhenNotSpecifiedInRequest() { - // given CommonParsedParams commonParsed = CommonParsedParams.builder() .programs(List.of(mockProgram("program1"), mockProgram("program2"))) @@ -191,11 +181,9 @@ void testEnrolledInProgramWhenNotSpecifiedInRequest() { .commonRaw(requestParams) .build(); - // when String sql = sqlQueryCreatorService.getSqlQueryCreator(contextParams).createForSelect().getStatement(); - // then assertTrue(sql.contains("ouname")); assertContains("(t_1.\"program1\" or t_1.\"program2\")", sql); } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/util/AnalyticsColumnAsserter.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/util/AnalyticsColumnAsserter.java index fe6208e8935d..2c9278516e70 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/util/AnalyticsColumnAsserter.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/util/AnalyticsColumnAsserter.java @@ -30,19 +30,17 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; +import lombok.RequiredArgsConstructor; +import lombok.Setter; import org.hisp.dhis.analytics.table.model.AnalyticsTableColumn; /** * @author Luciano Fiandesio */ +@Setter public class AnalyticsColumnAsserter { - /** The analytics table column to verify. */ private AnalyticsTableColumn actual; - private void setActual(AnalyticsTableColumn actual) { - this.actual = actual; - } - public void verify(AnalyticsTableColumn expected) { assertThat("Column name does not match", expected.getName(), is(actual.getName())); assertThat( @@ -62,12 +60,9 @@ public void verify(AnalyticsTableColumn expected) { is(actual.getIndexType())); } + @RequiredArgsConstructor public static class Builder { - AnalyticsTableColumn _column; - - public Builder(AnalyticsTableColumn column) { - _column = column; - } + final AnalyticsTableColumn _column; public AnalyticsColumnAsserter build() { AnalyticsColumnAsserter asserter = new AnalyticsColumnAsserter(); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/util/AnalyticsPeriodCriteriaUtilsTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/util/AnalyticsPeriodCriteriaUtilsTest.java index 7626a7f5bad7..c42379f577d6 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/util/AnalyticsPeriodCriteriaUtilsTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/util/AnalyticsPeriodCriteriaUtilsTest.java @@ -48,7 +48,7 @@ void testDefineDefaultPeriodForCriteria_without_period() { // when AnalyticsPeriodCriteriaUtils.defineDefaultPeriodForCriteria( - criteria, periodDataProvider, PeriodDataProvider.DataSource.SYSTEM_DEFINED); + criteria, periodDataProvider, PeriodDataProvider.PeriodSource.SYSTEM_DEFINED); // then assertEquals( @@ -69,7 +69,7 @@ void testDefineDefaultPeriodForCriteria_with_period() { // when AnalyticsPeriodCriteriaUtils.defineDefaultPeriodForCriteria( - criteria, periodDataProvider, PeriodDataProvider.DataSource.SYSTEM_DEFINED); + criteria, periodDataProvider, PeriodDataProvider.PeriodSource.SYSTEM_DEFINED); // then assertNull(criteria.getStartDate()); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/util/AnalyticsTableAsserter.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/util/AnalyticsTableAsserter.java index f536a8a0b04c..682240cc5185 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/util/AnalyticsTableAsserter.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/util/AnalyticsTableAsserter.java @@ -39,6 +39,8 @@ import java.util.Map; import java.util.function.Consumer; import java.util.stream.Collectors; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; import org.hisp.dhis.analytics.AnalyticsTableType; import org.hisp.dhis.analytics.table.model.AnalyticsTable; import org.hisp.dhis.analytics.table.model.AnalyticsTableColumn; @@ -49,6 +51,7 @@ /** * @author Luciano Fiandesio */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) public class AnalyticsTableAsserter { /** The analytics table to verify. */ private AnalyticsTable table; @@ -67,8 +70,6 @@ public class AnalyticsTableAsserter { private String mainName; - private AnalyticsTableAsserter() {} - public void verify() { // verify column size assertThat(table.getDimensionColumns(), hasSize(columnsSize)); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/util/SqlComparisonUtils.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/util/SqlComparisonUtils.java new file mode 100644 index 000000000000..3b6082b86b94 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/analytics/util/SqlComparisonUtils.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.analytics.util; + +public class SqlComparisonUtils { + + /** + * Normalizes a SQL string by removing excess whitespace and standardizing formatting. This + * method: + * + *
    + *
  • Replaces multiple spaces with a single space + *
  • Removes spaces around commas, equals signs, and parentheses + *
  • Converts the string to lowercase + *
  • Trims leading and trailing whitespace + *
+ * + * @param sql the SQL string to normalize + * @return the normalized SQL string + */ + public static String normalizeSql(String sql) { + return sql.replaceAll("\\s+", " ") // Replace multiple spaces with single space + .replaceAll("\\s*,\\s*", ",") // Remove spaces around commas + .replaceAll("\\s*=\\s*", "=") // Remove spaces around equals + .replaceAll("\\s*\\(\\s*", "(") // Remove spaces around parentheses + .replaceAll("\\s*\\)\\s*", ")") + .trim() + .toLowerCase(); // Convert to lowercase if case-insensitive comparison is desired + } +} diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/common/BaseDimensionalItemObjectTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/common/BaseDimensionalItemObjectTest.java index 3259349deebe..57912122b542 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/common/BaseDimensionalItemObjectTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/common/BaseDimensionalItemObjectTest.java @@ -35,25 +35,19 @@ class BaseDimensionalItemObjectTest { @Test void testWhenBaseDimensionalItemObjectAggregationTypeIsNoneTotalAggregationTypeIsNone() { - // given BaseDimensionalItemObject baseDimensionalItemObject = new BaseDimensionalItemObject(); - // when baseDimensionalItemObject.setAggregationType(AggregationType.NONE); - // then assertSame(TotalAggregationType.NONE, baseDimensionalItemObject.getTotalAggregationType()); } @Test void testWhenBaseDimensionalItemObjectAggregationTypeIsNotNoneTotalAggregationTypeIsSum() { - // given BaseDimensionalItemObject baseDimensionalItemObject = new BaseDimensionalItemObject(); - // when baseDimensionalItemObject.setAggregationType(AggregationType.AVERAGE_SUM_ORG_UNIT); - // then assertSame(TotalAggregationType.SUM, baseDimensionalItemObject.getTotalAggregationType()); } } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/model/ColumnTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/model/ColumnTest.java index a8079fdbbee7..e1d2f48443c5 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/model/ColumnTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/model/ColumnTest.java @@ -34,7 +34,6 @@ import org.junit.jupiter.api.Test; class ColumnTest { - @Test void testIsNotNull() { Column colA = new Column("dx", DataType.CHARACTER_11, Nullable.NOT_NULL); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/model/IndexTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/model/IndexTest.java index 53d77d00775e..c7d46a847a70 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/model/IndexTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/model/IndexTest.java @@ -60,13 +60,10 @@ void testIsUnique() { @Test void testDefaults() { - // given Index.IndexBuilder builder = Index.builder(); - // when Index index = builder.build(); - // then assertNull(index.getName()); assertNull(index.getTableName()); assertNull(index.getCondition()); diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilderTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilderTest.java index 0f6bb575a289..5087172105e4 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilderTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/sql/ClickHouseSqlBuilderTest.java @@ -195,6 +195,38 @@ void testDateTrunc() { "date_trunc('month', pe.startdate)", sqlBuilder.dateTrunc("month", "pe.startdate")); } + @Test + void testDifferenceInSeconds() { + assertEquals( + "(toUnixTimestamp(a.startdate) - toUnixTimestamp(b.enddate))", + sqlBuilder.differenceInSeconds("a.startdate", "b.enddate")); + assertEquals( + "(toUnixTimestamp(a.\"startdate\") - toUnixTimestamp(b.\"enddate\"))", + sqlBuilder.differenceInSeconds( + sqlBuilder.quote("a", "startdate"), sqlBuilder.quote("b", "enddate"))); + } + + @Test + void testRegexpMatch() { + assertEquals("match(value, 'test')", sqlBuilder.regexpMatch("value", "'test'")); + assertEquals("match(number, '\\d')", sqlBuilder.regexpMatch("number", "'\\d'")); + assertEquals("match(color, '^Blue$')", sqlBuilder.regexpMatch("color", "'^Blue$'")); + assertEquals("match(id, '[a-z]\\w+\\d{3}')", sqlBuilder.regexpMatch("id", "'[a-z]\\w+\\d{3}'")); + } + + @Test + void testJsonExtract() { + assertEquals( + "JSONExtractString(value, 'D7m8vpzxHDJ')", sqlBuilder.jsonExtract("value", "D7m8vpzxHDJ")); + } + + @Test + void testJsonExtractNested() { + assertEquals( + "JSONExtractString(eventdatavalues, 'D7m8vpzxHDJ.value')", + sqlBuilder.jsonExtractNested("eventdatavalues", "D7m8vpzxHDJ", "value")); + } + // Statements @Test diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/sql/DorisSqlBuilderTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/sql/DorisSqlBuilderTest.java index d1cc7fd94648..ad372141c9bc 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/sql/DorisSqlBuilderTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/sql/DorisSqlBuilderTest.java @@ -191,10 +191,24 @@ void testDifferenceInSeconds() { @Test void testRegexpMatch() { - assertEquals("regexp test", sqlBuilder.regexpMatch("test")); - assertEquals("regexp \\d", sqlBuilder.regexpMatch("\\d")); - assertEquals("regexp ", sqlBuilder.regexpMatch("")); - assertEquals("regexp [a-z]\\w+\\d{3}", sqlBuilder.regexpMatch("[a-z]\\w+\\d{3}")); + assertEquals("value regexp 'test'", sqlBuilder.regexpMatch("value", "'test'")); + assertEquals("number regexp '\\d'", sqlBuilder.regexpMatch("number", "'\\d'")); + assertEquals("color regexp '^Blue$'", sqlBuilder.regexpMatch("color", "'^Blue$'")); + assertEquals("id regexp '[a-z]\\w+\\d{3}'", sqlBuilder.regexpMatch("id", "'[a-z]\\w+\\d{3}'")); + } + + @Test + void testJsonExtract() { + assertEquals( + "json_unquote(json_extract(value, '$.D7m8vpzxHDJ'))", + sqlBuilder.jsonExtract("value", "D7m8vpzxHDJ")); + } + + @Test + void testJsonExtractNested() { + assertEquals( + "json_unquote(json_extract(eventdatavalues, '$.D7m8vpzxHDJ.value'))", + sqlBuilder.jsonExtractNested("eventdatavalues", "D7m8vpzxHDJ", "value")); } // Statements diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/sql/PostgreSqlBuilderTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/sql/PostgreSqlBuilderTest.java index 090e7df37517..2d621ae766e4 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/sql/PostgreSqlBuilderTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/db/sql/PostgreSqlBuilderTest.java @@ -219,11 +219,22 @@ void testDifferenceInSeconds() { @Test void testRegexpMatch() { - assertEquals("~* test", sqlBuilder.regexpMatch("test")); - assertEquals("~* ", sqlBuilder.regexpMatch("")); - assertEquals("~* null", sqlBuilder.regexpMatch(null)); - assertEquals("~* .*[a-z]\\d+", sqlBuilder.regexpMatch(".*[a-z]\\d+")); - assertEquals("~* ", sqlBuilder.regexpMatch(" ")); + assertEquals("value ~* 'test'", sqlBuilder.regexpMatch("value", "'test'")); + assertEquals("number ~* '\\d'", sqlBuilder.regexpMatch("number", "'\\d'")); + assertEquals("color ~* '^Blue$'", sqlBuilder.regexpMatch("color", "'^Blue$'")); + assertEquals("id ~* '[a-z]\\w+\\d{3}'", sqlBuilder.regexpMatch("id", "'[a-z]\\w+\\d{3}'")); + } + + @Test + void testJsonExtract() { + assertEquals("value ->> 'D7m8vpzxHDJ'", sqlBuilder.jsonExtract("value", "D7m8vpzxHDJ")); + } + + @Test + void testJsonExtractNested() { + assertEquals( + "eventdatavalues #>> '{D7m8vpzxHDJ, value}'", + sqlBuilder.jsonExtractNested("eventdatavalues", "D7m8vpzxHDJ", "value")); } // Statements diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/resourcetable/DefaultResourceTableServiceTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/resourcetable/DefaultResourceTableServiceTest.java index 894393258de9..a20545a91410 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/resourcetable/DefaultResourceTableServiceTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/resourcetable/DefaultResourceTableServiceTest.java @@ -28,7 +28,7 @@ package org.hisp.dhis.resourcetable; import static java.time.temporal.ChronoUnit.YEARS; -import static org.hisp.dhis.period.PeriodDataProvider.DataSource.DATABASE; +import static org.hisp.dhis.period.PeriodDataProvider.PeriodSource.DATABASE; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -38,6 +38,7 @@ import java.util.List; import org.hisp.dhis.analytics.table.setting.AnalyticsTableSettings; import org.hisp.dhis.period.PeriodDataProvider; +import org.hisp.dhis.period.PeriodDataProvider.PeriodSource; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -60,8 +61,9 @@ void generateDatePeriodTableWhenYearIsOutOfRange() { List yearsToCheck = List.of(2000, 2001, 2002, 2003, 2004); int defaultOffset = 22; - when(periodDataProvider.getAvailableYears(DATABASE)).thenReturn(yearsToCheck); + when(analyticsTableSettings.getPeriodSource()).thenReturn(PeriodSource.DATABASE); when(analyticsTableSettings.getMaxPeriodYearsOffset()).thenReturn(defaultOffset); + when(periodDataProvider.getAvailableYears(DATABASE)).thenReturn(yearsToCheck); RuntimeException exception = assertThrows( @@ -77,8 +79,9 @@ void generateDatePeriodTableWhenOffsetIsZeroWithPreviousYears() { List yearsToCheck = List.of(2000, 2001, 2002, 2003, 2004); int zeroOffset = 0; - when(periodDataProvider.getAvailableYears(DATABASE)).thenReturn(yearsToCheck); + when(analyticsTableSettings.getPeriodSource()).thenReturn(PeriodSource.DATABASE); when(analyticsTableSettings.getMaxPeriodYearsOffset()).thenReturn(zeroOffset); + when(periodDataProvider.getAvailableYears(DATABASE)).thenReturn(yearsToCheck); RuntimeException exception = assertThrows( @@ -94,8 +97,9 @@ void generateDatePeriodTableWhenOffsetIsZeroWithCurrentYear() { List yearsToCheck = List.of(Year.now().getValue()); int zeroOffset = 0; - when(periodDataProvider.getAvailableYears(DATABASE)).thenReturn(yearsToCheck); + when(analyticsTableSettings.getPeriodSource()).thenReturn(PeriodSource.DATABASE); when(analyticsTableSettings.getMaxPeriodYearsOffset()).thenReturn(zeroOffset); + when(periodDataProvider.getAvailableYears(DATABASE)).thenReturn(yearsToCheck); assertDoesNotThrow(() -> defaultResourceTableService.getAndValidateAvailableDataYears()); } @@ -109,8 +113,9 @@ void generateDatePeriodTableWhenYearsAreInExpectedRange() { Year.now().plus(2, YEARS).getValue()); int defaultOffset = 2; - when(periodDataProvider.getAvailableYears(DATABASE)).thenReturn(yearsToCheck); + when(analyticsTableSettings.getPeriodSource()).thenReturn(PeriodSource.DATABASE); when(analyticsTableSettings.getMaxPeriodYearsOffset()).thenReturn(defaultOffset); + when(periodDataProvider.getAvailableYears(DATABASE)).thenReturn(yearsToCheck); assertDoesNotThrow(() -> defaultResourceTableService.getAndValidateAvailableDataYears()); } diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/resourcetable/table/OrganisationUnitStructureResourceTableTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/resourcetable/table/OrganisationUnitStructureResourceTableTest.java index be8d39e20a8d..2080837b84e6 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/resourcetable/table/OrganisationUnitStructureResourceTableTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/resourcetable/table/OrganisationUnitStructureResourceTableTest.java @@ -28,19 +28,18 @@ package org.hisp.dhis.resourcetable.table; import static org.hisp.dhis.test.TestBase.createOrganisationUnit; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import java.util.ArrayList; import java.util.List; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.junit.jupiter.api.Test; -/** Unit tests for {@link OrganisationUnitStructureResourceTable}. */ class OrganisationUnitStructureResourceTableTest { - @Test void testCreateBatchObjectsWhenLevelsAreSame() { - // Given int maxOrgUnitLevels = 3; int currentLevel = 3; @@ -65,13 +64,11 @@ void testCreateBatchObjectsWhenLevelsAreSame() { OrganisationUnitStructureResourceTable resourceTable = new OrganisationUnitStructureResourceTable(null, maxOrgUnitLevels, null); - // Then assertDoesNotThrow(() -> resourceTable.createBatchObjects(organisationUnits, currentLevel)); } @Test void testCreateBatchObjectsWhenHierarchyLevelIsLowerThanMaxLevel() { - // Given int maxOrgUnitLevels = 3; int currentLevel = 2; @@ -87,13 +84,11 @@ void testCreateBatchObjectsWhenHierarchyLevelIsLowerThanMaxLevel() { OrganisationUnitStructureResourceTable resourceTable = new OrganisationUnitStructureResourceTable(null, maxOrgUnitLevels, null); - // Then assertDoesNotThrow(() -> resourceTable.createBatchObjects(organisationUnits, currentLevel)); } @Test void testCreateBatchObjectsWhenCurrentLevelIsLargerThanMaxLevel() { - // Given int maxOrgUnitLevels = 2; int currentLevel = 3; @@ -110,7 +105,6 @@ void testCreateBatchObjectsWhenCurrentLevelIsLargerThanMaxLevel() { OrganisationUnitStructureResourceTable resourceTable = new OrganisationUnitStructureResourceTable(null, maxOrgUnitLevels, null); - // Then Exception ex = assertThrows( IllegalStateException.class, @@ -123,7 +117,6 @@ void testCreateBatchObjectsWhenCurrentLevelIsLargerThanMaxLevel() { @Test void testCreateBatchObjectsWhenCurrentLevelHasNoParent() { - // Given int maxOrgUnitLevels = 2; int currentLevel = 3; @@ -140,7 +133,6 @@ void testCreateBatchObjectsWhenCurrentLevelHasNoParent() { OrganisationUnitStructureResourceTable resourceTable = new OrganisationUnitStructureResourceTable(null, maxOrgUnitLevels, null); - // Then Exception ex = assertThrows( IllegalStateException.class, diff --git a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/resourcetable/table/UniqueNameContextTest.java b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/resourcetable/table/UniqueNameContextTest.java index 0e1bc425adc6..380e698dc9ed 100644 --- a/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/resourcetable/table/UniqueNameContextTest.java +++ b/dhis-2/dhis-services/dhis-service-analytics/src/test/java/org/hisp/dhis/resourcetable/table/UniqueNameContextTest.java @@ -38,7 +38,6 @@ * @author Jan Bernitt */ class UniqueNameContextTest { - private final UniqueNameContext context = new UniqueNameContext(); @Test diff --git a/dhis-2/dhis-services/dhis-service-core/pom.xml b/dhis-2/dhis-services/dhis-service-core/pom.xml index 86fa7ae4513f..2e76d7dac6b4 100644 --- a/dhis-2/dhis-services/dhis-service-core/pom.xml +++ b/dhis-2/dhis-services/dhis-service-core/pom.xml @@ -30,6 +30,7 @@ org.hisp.dhis dhis-service-node + test org.hisp.dhis diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/common/DefaultIdentifiableObjectManager.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/common/DefaultIdentifiableObjectManager.java index 0ea7377fea9f..cb904f2a4441 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/common/DefaultIdentifiableObjectManager.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/common/DefaultIdentifiableObjectManager.java @@ -39,9 +39,7 @@ import jakarta.persistence.EntityManager; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Date; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -409,34 +407,6 @@ public T search(@Nonnull Class type, @Nonnull return object; } - @Nonnull - @Override - @Transactional(readOnly = true) - public List filter( - @Nonnull Class type, @Nonnull String query) { - Set uniqueObjects = new HashSet<>(); - - T uidObject = get(type, query); - - if (uidObject != null) { - uniqueObjects.add(uidObject); - } - - T codeObject = getByCode(type, query); - - if (codeObject != null) { - uniqueObjects.add(codeObject); - } - - uniqueObjects.addAll(getLikeName(type, query, false)); - - List objects = new ArrayList<>(uniqueObjects); - - Collections.sort(objects); - - return objects; - } - @Nonnull @Override @Transactional(readOnly = true) diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/organisationunit/DefaultOrganisationUnitService.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/organisationunit/DefaultOrganisationUnitService.java index c1e34b6580bc..280d0e6c2ec2 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/organisationunit/DefaultOrganisationUnitService.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/organisationunit/DefaultOrganisationUnitService.java @@ -163,12 +163,6 @@ public List getOrganisationUnitsByUid(@Nonnull Collection(uids)); } - @Override - @Transactional(readOnly = true) - public List getOrganisationUnitsByQuery(OrganisationUnitQueryParams params) { - return organisationUnitStore.getOrganisationUnits(params); - } - @Override @Transactional(readOnly = true) public OrganisationUnit getOrganisationUnit(String uid) { diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/period/PeriodDataProvider.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/period/PeriodDataProvider.java index 51d5c1f9b892..47f41b78ee4a 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/period/PeriodDataProvider.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/period/PeriodDataProvider.java @@ -31,7 +31,7 @@ import static java.util.Collections.sort; import static java.util.Collections.unmodifiableList; import static org.apache.commons.collections4.CollectionUtils.isEmpty; -import static org.hisp.dhis.period.PeriodDataProvider.DataSource.SYSTEM_DEFINED; +import static org.hisp.dhis.period.PeriodDataProvider.PeriodSource.SYSTEM_DEFINED; import java.util.ArrayList; import java.util.List; @@ -56,8 +56,10 @@ public class PeriodDataProvider { private final JdbcTemplate jdbcTemplate; - public enum DataSource { + public enum PeriodSource { + /** Fixed boundaries. Backwards compatible. */ SYSTEM_DEFINED, + /** Dynamic from data in database. */ DATABASE } @@ -65,21 +67,19 @@ public enum DataSource { * Returns a distinct union of all years available in the "event" table + "period" table, both * from aggregate and tracker, with 5 years previous and future additions. * - *

ie: [extra_5_previous_years, data_years, extra_5_future_year] + *

[extra_5_previous_years, data_years, extra_5_future_year] * - * @param source the source ({@link DataSource}) where the years wil be retrieved from. + * @param source the source ({@link PeriodSource}) where the years wil be retrieved from. * @return an unmodifiable list of distinct years and the respective additions. */ - public List getAvailableYears(DataSource source) { + public List getAvailableYears(PeriodSource source) { List availableDataYears = new ArrayList<>(); if (source == SYSTEM_DEFINED) { - // Add default hard-coded years (keeps it backward compatible). for (int year = DEFAULT_FIRST_YEAR_SUPPORTED; year <= DEFAULT_LATEST_YEAR_SUPPORTED; year++) { availableDataYears.add(year); } } else { - // Add years dynamically, based on the database. availableDataYears.addAll(fetchAvailableYears()); addSafetyBuffer(availableDataYears, BEFORE_AND_AFTER_DATA_YEARS_SUPPORTED); } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Criteria.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Criteria.java index 5cbe43026ac2..cf3d4b84bdb6 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Criteria.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Criteria.java @@ -61,7 +61,7 @@ public Criteria add(Criterion... criterions) { return this; } - public Criteria add(Collection criterions) { + public Criteria add(Collection criterions) { this.criterions.addAll(criterions); return this; } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Criterion.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Criterion.java index 470a6b110825..6e1f47cd0dec 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Criterion.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Criterion.java @@ -30,4 +30,13 @@ /** * @author Morten Olav Hansen */ -public interface Criterion {} +public interface Criterion { + + /** + * @return true, when the condition cannot match any rows, e.g. an in-operator with an empty + * collection to test against + */ + default boolean isAlwaysFalse() { + return false; + } +} diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/DefaultJpaQueryParser.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/DefaultJpaQueryParser.java index 8028a8425667..c405c5cfd989 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/DefaultJpaQueryParser.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/DefaultJpaQueryParser.java @@ -28,10 +28,15 @@ package org.hisp.dhis.query; import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.joining; import static org.hisp.dhis.query.QueryUtils.parseValue; +import com.google.common.collect.Lists; +import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.stream.Stream; +import javax.annotation.Nonnull; import org.hisp.dhis.common.CodeGenerator; import org.hisp.dhis.query.operators.MatchMode; import org.hisp.dhis.schema.Property; @@ -52,16 +57,17 @@ public DefaultJpaQueryParser(SchemaService schemaService) { } @Override - public Query parse(Class klass, List filters) throws QueryParserException { + public Query parse(Class klass, @Nonnull List filters) throws QueryParserException { return parse(klass, filters, Junction.Type.AND); } @Override - public Query parse(Class klass, List filters, Junction.Type rootJunction) + public Query parse(Class klass, @Nonnull List filters, Junction.Type rootJunction) throws QueryParserException { Schema schema = schemaService.getDynamicSchema(klass); Query query = Query.from(schema, rootJunction); + List mentions = new ArrayList<>(); for (String filter : filters) { String[] split = filter.split(":"); @@ -69,16 +75,22 @@ public Query parse(Class klass, List filters, Junction.Type rootJunct throw new QueryParserException("Invalid filter => " + filter); } + String path = split[0]; + String operator = split[1]; if (split.length >= 3) { - int index = split[0].length() + ":".length() + split[1].length() + ":".length(); - - if (split[0].equals(IDENTIFIABLE) && !schema.hasProperty(IDENTIFIABLE)) { - handleIdentifiablePath(schema, split[1], filter.substring(index), query.addDisjunction()); + String arg = split.length == 3 ? split[2] : Stream.of(split).skip(2).collect(joining(":")); + if ("mentions".equals(path) && "in".equals(operator)) { + mentions.add(arg); + } else if (path.equals(IDENTIFIABLE) && !schema.hasProperty(IDENTIFIABLE)) { + handleIdentifiablePath(schema, operator, arg, query.addDisjunction()); } else { - query.add(getRestriction(schema, split[0], split[1], filter.substring(index))); + query.add(getRestriction(schema, path, operator, arg)); } } else { - query.add(getRestriction(schema, split[0], split[1], null)); + query.add(getRestriction(schema, path, operator, null)); + } + if (!mentions.isEmpty()) { + getDisjunctionsFromCustomMentions(mentions, query.getSchema()); } } return query; @@ -137,36 +149,8 @@ private Restriction getRestriction( case "endsWith", "ilike$" -> Restrictions.ilike(path, parseValue(valueType, arg), MatchMode.END); case "!ilike$" -> Restrictions.notIlike(path, parseValue(valueType, arg), MatchMode.END); - case "in" -> { - Collection values = null; - - if (property.isCollection()) { - values = parseValue(Collection.class, property.getItemKlass(), arg); - } else { - values = parseValue(Collection.class, valueType, arg); - } - - if (values == null || values.isEmpty()) { - throw new QueryParserException("Invalid argument `" + arg + "` for in operator."); - } - - yield Restrictions.in(path, values); - } - case "!in" -> { - Collection values = null; - - if (property.isCollection()) { - values = parseValue(Collection.class, property.getItemKlass(), arg); - } else { - values = parseValue(Collection.class, valueType, arg); - } - - if (values == null || values.isEmpty()) { - throw new QueryParserException("Invalid argument `" + arg + "` for in operator."); - } - - yield Restrictions.notIn(path, values); - } + case "in" -> Restrictions.in(path, parseValues(property, valueType, arg)); + case "!in" -> Restrictions.notIn(path, parseValues(property, valueType, arg)); case "null" -> Restrictions.isNull(path); case "!null" -> Restrictions.isNotNull(path); case "empty" -> Restrictions.isEmpty(path); @@ -174,6 +158,34 @@ private Restriction getRestriction( }; } + @Nonnull + @SuppressWarnings("rawtypes") + private Collection parseValues(Property property, Class valueType, Object arg) { + Collection values = + property.isCollection() + ? parseValue(Collection.class, property.getItemKlass(), arg) + : parseValue(Collection.class, valueType, arg); + + if (values == null || values.isEmpty()) { + throw new QueryParserException("Invalid argument `" + arg + "` for in operator."); + } + return values; + } + + private Collection getDisjunctionsFromCustomMentions( + List mentions, Schema schema) { + Collection disjunctions = new ArrayList<>(); + for (String m : mentions) { + Disjunction disjunction = new Disjunction(schema); + String[] split = m.substring(1, m.length() - 1).split(","); + List items = Lists.newArrayList(split); + disjunction.add(Restrictions.in("mentions.username", items)); + disjunction.add(Restrictions.in("comments.mentions.username", items)); + disjunctions.add(disjunction); + } + return disjunctions; + } + @Override public Property getProperty(Schema schema, String path) throws QueryParserException { String[] paths = path.split("\\."); diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/DefaultQueryService.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/DefaultQueryService.java index c58e2167162b..da638c64310e 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/DefaultQueryService.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/DefaultQueryService.java @@ -35,6 +35,8 @@ import org.hisp.dhis.preheat.Preheat; import org.hisp.dhis.query.planner.QueryPlan; import org.hisp.dhis.query.planner.QueryPlanner; +import org.hisp.dhis.schema.Schema; +import org.hisp.dhis.schema.SchemaService; import org.springframework.stereotype.Component; /** @@ -49,11 +51,10 @@ public class DefaultQueryService implements QueryService { private static final Junction.Type DEFAULT_JUNCTION_TYPE = Junction.Type.AND; private final QueryParser queryParser; - private final QueryPlanner queryPlanner; + private final SchemaService schemaService; private final JpaCriteriaQueryEngine criteriaQueryEngine; - private final InMemoryQueryEngine inMemoryQueryEngine; @Override @@ -85,23 +86,16 @@ public long count(Query query) { } @Override - public Query getQueryFromUrl( - Class klass, List filters, List orders, Pagination pagination) + public Query getQueryFromUrl(Class type, GetObjectListParams params) throws QueryParserException { - return getQueryFromUrl(klass, filters, orders, pagination, DEFAULT_JUNCTION_TYPE); - } + List filters = params.getFilters(); + if (filters == null) filters = List.of(); + Query query = queryParser.parse(type, filters, params.getRootJunction()); - @Override - public Query getQueryFromUrl( - Class klass, - List filters, - List orders, - Pagination pagination, - Junction.Type rootJunction) - throws QueryParserException { - Query query = queryParser.parse(klass, filters, rootJunction); - query.addOrders(orders); + Schema schema = schemaService.getDynamicSchema(type); + query.addOrders(QueryUtils.convertOrderStrings(params.getOrders(), schema)); + Pagination pagination = params.getPagination(); if (pagination.hasPagination()) { query.setFirstResult(pagination.getFirstResult()); query.setMaxResults(pagination.getSize()); @@ -110,12 +104,6 @@ public Query getQueryFromUrl( return query; } - @Override - public Query getQueryFromUrl(Class klass, List filters, List orders) - throws QueryParserException { - return getQueryFromUrl(klass, filters, orders, new Pagination(), DEFAULT_JUNCTION_TYPE); - } - // --------------------------------------------------------------------------------------------- // Helper methods // --------------------------------------------------------------------------------------------- diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/GetObjectListParams.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/GetObjectListParams.java new file mode 100644 index 000000000000..fb440e4aacb8 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/GetObjectListParams.java @@ -0,0 +1,148 @@ +/* + * 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.query; + +import static java.lang.Math.max; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; + +/** + * Base for parameters supported by CRUD {@code CRUD.getObjectList}. + * + * @author Jan Bernitt + */ +@Data +@Accessors(chain = true) +@EqualsAndHashCode(callSuper = true) +public class GetObjectListParams extends GetObjectParams { + + @JsonProperty("filter") + @CheckForNull + List filters; + + @JsonProperty("order") + @CheckForNull + List orders; + + @JsonProperty Junction.Type rootJunction = Junction.Type.AND; + + @JsonProperty boolean paging = true; + @JsonProperty int page = 1; + @JsonProperty int pageSize = 50; + + /** + * A special filter that matches the query term against the ID and code (equals) and against name + * (contains). Can be used in combination with regular filters. + */ + @JsonProperty String query; + + @Nonnull + @JsonIgnore + public Pagination getPagination() { + if (!paging) return new Pagination(); + // ignore if page < 0 + return new Pagination((max(page, 1) - 1) * pageSize, pageSize); + } + + public GetObjectListParams addFilter(String filter) { + if (filters == null) filters = new ArrayList<>(); + filters.add(filter); + return this; + } + + public GetObjectListParams addOrder(String order) { + if (orders == null) orders = new ArrayList<>(); + orders.add(order); + return this; + } + + /** + * For spring URL parameter injection only. + * + *

The issue is that a filter value may contain comma as part of the value, not to separate + * values which can be misunderstood by spring so the splitting needs to be done with custom + * logic. + * + * @param filters all filters as a comma seperated string + */ + public void setFilter(String filters) { + this.filters = splitFilters(filters); + } + + public void setOrder(List orders) { + this.orders = orders; + } + + /** + * Pattern to split {@code filter} expressions which have 2 or 3 components: {@code + * property:operator} or {@code property:operator:value}. + * + *

Examples + * + *

+   * name:eq:Peter
+   * organisationUnits:empty
+   * 
+ * + * Unfortunately the value can also contain : and when used with [] it can also contain , which + * needs extra caution to not read too little or too much into operation and value. + */ + private static final Pattern FILTER_PARTS = + Pattern.compile( + "(?[a-zA-Z0-9.]+):(?[!$a-zA-Z]{2,10})(?::(?\\[[^]]*]|[^,\\n\\r]+))?"); + + /** + * Splits a string {@code filter,filter} into a list {@code [filter,filter]}. + * + *

This is non-trivial since filters can contain comma symbols. + * + * @param filters a comma seperated list of filter expressions + * @return a list where each element is a single filter expression + */ + @CheckForNull + static List splitFilters(@CheckForNull String filters) { + if (filters == null || filters.isEmpty()) return null; + if (!filters.contains(",")) return new ArrayList<>(List.of(filters)); + List res = new ArrayList<>(); + Matcher m = FILTER_PARTS.matcher(filters); + while (m.find()) { + res.add(m.group()); + } + return res; + } +} diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/GetObjectParams.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/GetObjectParams.java new file mode 100644 index 000000000000..484ca06890b1 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/GetObjectParams.java @@ -0,0 +1,130 @@ +/* + * 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.query; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import org.hisp.dhis.common.OpenApi; +import org.hisp.dhis.fieldfilter.Defaults; +import org.hisp.dhis.fieldfiltering.FieldPreset; + +/** + * Base for parameters supported by CRUD {@code CRUD.getObject}. + * + * @author Jan Bernitt + */ +@Getter +@EqualsAndHashCode +@ToString +@Accessors(chain = true) +@OpenApi.Shared +public class GetObjectParams { + + /** + * Fields allow {@code property[sub,sub]} syntax where a comma occurs as part of the property + * name. These commas need to be ignored when splitting a {@code fields} parameter list. + */ + private static final String FIELD_SPLIT = ",(?![^\\[\\]]*\\]|[^\\(\\)]*\\)|([a-zA-Z0-9]+,?)+\\))"; + + @OpenApi.Shared.Inline + @OpenApi.Property(OpenApi.PropertyNames[].class) + @JsonProperty + @CheckForNull + List fields; + + @Setter @JsonProperty @Nonnull Defaults defaults = Defaults.INCLUDE; + + /** + * Since splitting on comma is too unsophisticated to be correct this needs custom split code when + * called by spring injecting URL parmaeters + * + * @param fields field expression + */ + public void setFields(String fields) { + this.fields = fields == null ? null : List.of(fields.split(FIELD_SPLIT)); + } + + public GetObjectParams addField(String field) { + if (fields == null) fields = new ArrayList<>(); + fields.add(field); + return this; + } + + /** + * @return list of fields with defaults as used when viewing a single object + */ + @Nonnull + @JsonIgnore + public List getFieldsObject() { + if (fields == null || fields.isEmpty()) return List.of("*"); + return fields; + } + + /** + * @return list of fields with defaults as used when viewing a list as JSON + */ + @Nonnull + @JsonIgnore + public List getFieldsJsonList() { + if (fields == null || fields.isEmpty()) return FieldPreset.defaultPreset().getFields(); + return fields; + } + + /** + * @return list of fields with defaults as used when viewing a list as CSV + */ + @Nonnull + @JsonIgnore + public List getFieldsCsvList() { + List res = new ArrayList<>(); + if (fields != null) res.addAll(fields); + if (res.isEmpty() || res.contains("*") || res.contains(":all")) + res.addAll(FieldPreset.defaultPreset().getFields()); + return res; + } + + /** + * @return elevates a single object params object to a list params object as a way to adapt to the + * list based query engine that is also used for single object lookup + */ + public GetObjectListParams toListParams() { + GetObjectListParams res = new GetObjectListParams(); + if (fields != null) fields.forEach(res::addField); + res.setDefaults(defaults); + return res; + } +} diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/JpaCriteriaQueryEngine.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/JpaCriteriaQueryEngine.java index ab03313c93f4..9f9a02b251c7 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/JpaCriteriaQueryEngine.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/JpaCriteriaQueryEngine.java @@ -28,6 +28,7 @@ package org.hisp.dhis.query; import static com.google.common.base.Preconditions.checkNotNull; +import static org.hisp.dhis.user.CurrentUserUtil.getCurrentUserDetails; import jakarta.persistence.EntityManager; import jakarta.persistence.TypedQuery; @@ -39,6 +40,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Function; +import javax.annotation.Nonnull; import org.hibernate.jpa.QueryHints; import org.hisp.dhis.cache.QueryCacheManager; import org.hisp.dhis.common.IdentifiableObject; @@ -47,7 +50,6 @@ import org.hisp.dhis.query.planner.QueryPlan; import org.hisp.dhis.query.planner.QueryPlanner; import org.hisp.dhis.schema.Schema; -import org.hisp.dhis.user.CurrentUserUtil; import org.hisp.dhis.user.UserDetails; import org.springframework.stereotype.Component; @@ -64,7 +66,7 @@ public class JpaCriteriaQueryEngine implements Que private final QueryCacheManager queryCacheManager; - private Map, IdentifiableObjectStore> stores = new HashMap<>(); + private final Map, IdentifiableObjectStore> stores = new HashMap<>(); public JpaCriteriaQueryEngine( QueryPlanner queryPlanner, @@ -86,16 +88,17 @@ public JpaCriteriaQueryEngine( public List query(Query query) { Schema schema = query.getSchema(); + @SuppressWarnings("unchecked") Class klass = (Class) schema.getKlass(); - InternalHibernateGenericStore store = (InternalHibernateGenericStore) getStore(klass); + InternalHibernateGenericStore store = getStore(klass); if (store == null) { return new ArrayList<>(); } if (query.getCurrentUserDetails() == null) { - query.setCurrentUserDetails(CurrentUserUtil.getCurrentUserDetails()); + query.setCurrentUserDetails(getCurrentUserDetails()); } if (!query.isPlannedQuery()) { @@ -108,68 +111,11 @@ public List query(Query query) { CriteriaQuery criteriaQuery = builder.createQuery(klass); Root root = criteriaQuery.from(klass); - if (query.isEmpty()) { - - UserDetails userDetails = - query.getCurrentUserDetails() != null - ? query.getCurrentUserDetails() - : CurrentUserUtil.getCurrentUserDetails(); - - Predicate predicate = builder.conjunction(); - boolean shareable = schema.isShareable(); - if (shareable && !query.isSkipSharing()) { - predicate - .getExpressions() - .addAll( - store.getSharingPredicates(builder, userDetails).stream() - .map(t -> t.apply(root)) - .toList()); - } - criteriaQuery.where(predicate); - - TypedQuery typedQuery = entityManager.createQuery(criteriaQuery); - - typedQuery.setFirstResult(query.getFirstResult()); - typedQuery.setMaxResults(query.getMaxResults()); - - return typedQuery.getResultList(); - } - Predicate predicate = buildPredicates(builder, root, query); - boolean shareable = schema.isShareable(); - - String username = - CurrentUserUtil.getCurrentUsername() != null - ? CurrentUserUtil.getCurrentUsername() - : "system-process"; - - if (!username.equals("system-process") && shareable && !query.isSkipSharing()) { - - UserDetails userDetails = - query.getCurrentUserDetails() != null - ? query.getCurrentUserDetails() - : CurrentUserUtil.getCurrentUserDetails(); - - predicate - .getExpressions() - .addAll( - store.getSharingPredicates(builder, userDetails).stream() - .map(t -> t.apply(root)) - .toList()); - } - + addSharingPredicates(query, schema, predicate, store, builder, root); criteriaQuery.where(predicate); - if (!query.getOrders().isEmpty()) { - criteriaQuery.orderBy( - query.getOrders().stream() - .map( - o -> - o.isAscending() - ? builder.asc(root.get(o.getProperty().getFieldName())) - : builder.desc(root.get(o.getProperty().getFieldName()))) - .toList()); - } + if (!query.getOrders().isEmpty()) criteriaQuery.orderBy(getOrders(query, builder, root)); TypedQuery typedQuery = entityManager.createQuery(criteriaQuery); @@ -186,20 +132,42 @@ public List query(Query query) { return typedQuery.getResultList(); } + private static void addSharingPredicates( + Query query, + Schema schema, + Predicate predicate, + InternalHibernateGenericStore store, + CriteriaBuilder builder, + Root root) { + boolean shareable = schema.isShareable(); + if (!shareable) return; + UserDetails user = query.getCurrentUserDetails(); + if (user == null) user = getCurrentUserDetails(); + List, Predicate>> predicates = List.of(); + if (query.isDataSharing()) { + predicates = store.getDataSharingPredicates(builder, user); + } else if (!query.isSkipSharing()) { + predicates = store.getSharingPredicates(builder, user); + } + if (!predicates.isEmpty()) + predicate.getExpressions().addAll(predicates.stream().map(t -> t.apply(root)).toList()); + } + @Override public long count(Query query) { Schema schema = query.getSchema(); + @SuppressWarnings("unchecked") Class klass = (Class) schema.getKlass(); - InternalHibernateGenericStore store = (InternalHibernateGenericStore) getStore(klass); + InternalHibernateGenericStore store = getStore(klass); if (store == null) { return 0; } if (query.getCurrentUserDetails() == null) { - query.setCurrentUserDetails(CurrentUserUtil.getCurrentUserDetails()); + query.setCurrentUserDetails(getCurrentUserDetails()); } if (!query.isPlannedQuery()) { @@ -215,36 +183,26 @@ public long count(Query query) { criteriaQuery.select(builder.count(root)); Predicate predicate = buildPredicates(builder, root, query); - - boolean shareable = schema.isShareable(); - if (shareable && !query.isSkipSharing()) { - UserDetails currentUserDetails = query.getCurrentUserDetails(); - predicate - .getExpressions() - .addAll( - store.getSharingPredicates(builder, currentUserDetails).stream() - .map(t -> t.apply(root)) - .toList()); - } - + addSharingPredicates(query, schema, predicate, store, builder, root); criteriaQuery.where(predicate); - if (!query.getOrders().isEmpty()) { - criteriaQuery.orderBy( - query.getOrders().stream() - .map( - o -> - o.isAscending() - ? builder.asc(root.get(o.getProperty().getName())) - : builder.desc(root.get(o.getProperty().getName()))) - .toList()); - } - TypedQuery typedQuery = entityManager.createQuery(criteriaQuery); return typedQuery.getSingleResult(); } + @Nonnull + private static List getOrders( + Query query, CriteriaBuilder builder, Root root) { + return query.getOrders().stream() + .map( + o -> + o.isAscending() + ? builder.asc(root.get(o.getProperty().getFieldName())) + : builder.desc(root.get(o.getProperty().getFieldName()))) + .toList(); + } + private void initStoreMap() { if (!stores.isEmpty()) { return; @@ -255,13 +213,15 @@ private void initStoreMap() { } } - private IdentifiableObjectStore getStore(Class klass) { + @SuppressWarnings("unchecked") + private InternalHibernateGenericStore getStore(Class klass) { initStoreMap(); - return stores.get(klass); + return (InternalHibernateGenericStore) stores.get(klass); } private Predicate buildPredicates(CriteriaBuilder builder, Root root, Query query) { Predicate junction = builder.conjunction(); + if (query.isEmpty()) return junction; query.getAliases().forEach(alias -> root.join(alias).alias(alias)); if (!query.getCriterions().isEmpty()) { junction = getJpaJunction(builder, query.getRootJunctionType()); @@ -274,14 +234,10 @@ private Predicate buildPredicates(CriteriaBuilder builder, Root root, Que } private Predicate getJpaJunction(CriteriaBuilder builder, Junction.Type type) { - switch (type) { - case AND: - return builder.conjunction(); - case OR: - return builder.disjunction(); - } - - return builder.conjunction(); + return switch (type) { + case AND -> builder.conjunction(); + case OR -> builder.disjunction(); + }; } private Predicate getPredicate( diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Query.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Query.java index 564eb9dbef5b..0c3e514539d9 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Query.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Query.java @@ -58,6 +58,8 @@ public class Query extends Criteria { private boolean skipSharing; + private boolean dataSharing; + private Integer firstResult = 0; private Integer maxResults = Integer.MAX_VALUE; @@ -150,7 +152,7 @@ public Query add(Criterion... criterions) { } @Override - public Query add(Collection criterions) { + public Query add(Collection criterions) { super.add(criterions); return this; } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/QueryParser.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/QueryParser.java index 5cf2f4d96cb0..69547997e708 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/QueryParser.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/QueryParser.java @@ -28,6 +28,7 @@ package org.hisp.dhis.query; import java.util.List; +import javax.annotation.Nonnull; import org.hisp.dhis.schema.Property; import org.hisp.dhis.schema.Schema; @@ -48,10 +49,10 @@ public interface QueryParser { * @return Query instance based on Schema of klass and filters list * @throws QueryParserException */ - Query parse(Class klass, List filters, Junction.Type rootJunction) + Query parse(Class klass, @Nonnull List filters, Junction.Type rootJunction) throws QueryParserException; - Query parse(Class klass, List filters) throws QueryParserException; + Query parse(Class klass, @Nonnull List filters) throws QueryParserException; Property getProperty(Schema schema, String path) throws QueryParserException; } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/QueryService.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/QueryService.java index d48849d10c6b..816cee399409 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/QueryService.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/QueryService.java @@ -65,23 +65,8 @@ public interface QueryService { * orders. * * @param klass Type of object you want to query - * @param filters List of filters to use as basis for query instance - * @param orders List of orders to use for query - * @param rootJunction Root junction (defaults to AND) + * @param params standard object list query parameters * @return New query instance using provided filters/orders */ - Query getQueryFromUrl( - Class klass, - List filters, - List orders, - Pagination pagination, - Junction.Type rootJunction) - throws QueryParserException; - - Query getQueryFromUrl( - Class klass, List filters, List orders, Pagination pagination) - throws QueryParserException; - - Query getQueryFromUrl(Class klass, List filters, List orders) - throws QueryParserException; + Query getQueryFromUrl(Class klass, GetObjectListParams params) throws QueryParserException; } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Restriction.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Restriction.java index 210ae7c2cd32..5ebf3c95ff3e 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Restriction.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Restriction.java @@ -32,6 +32,7 @@ import lombok.RequiredArgsConstructor; import lombok.Setter; import lombok.experimental.Accessors; +import org.hisp.dhis.query.operators.InOperator; import org.hisp.dhis.query.operators.Operator; import org.hisp.dhis.query.planner.QueryPath; @@ -71,4 +72,11 @@ public Restriction asAttribute() { public String toString() { return "[" + path + ", op: " + operator + "]"; } + + @Override + public boolean isAlwaysFalse() { + if (operator instanceof InOperator in) + return in.getCollectionArgs().isEmpty() || in.getCollectionArgs().get(0).isEmpty(); + return false; + } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Restrictions.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Restrictions.java index 1f3249b0612f..48bb0af8cd4b 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Restrictions.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Restrictions.java @@ -28,6 +28,7 @@ package org.hisp.dhis.query; import java.util.Collection; +import java.util.List; import org.hisp.dhis.query.operators.BetweenOperator; import org.hisp.dhis.query.operators.EmptyOperator; import org.hisp.dhis.query.operators.EqualOperator; @@ -45,6 +46,7 @@ import org.hisp.dhis.query.operators.NotTokenOperator; import org.hisp.dhis.query.operators.NullOperator; import org.hisp.dhis.query.operators.TokenOperator; +import org.hisp.dhis.schema.Schema; /** * @author Morten Olav Hansen @@ -131,5 +133,29 @@ public static Restriction isEmpty(String path) { return new Restriction(path, new EmptyOperator<>()); } + public static Disjunction query(Schema schema, String query) { + return or(schema, eq("id", query), eq("code", query), ilike("name", query, MatchMode.ANYWHERE)); + } + + public static Disjunction or(Schema schema, Criterion... filters) { + return or(schema, List.of(filters)); + } + + public static Disjunction or(Schema schema, List filters) { + Disjunction or = new Disjunction(schema); + or.add(filters); + return or; + } + + public static Conjunction and(Schema schema, Criterion... filters) { + return and(schema, List.of(filters)); + } + + public static Conjunction and(Schema schema, List filters) { + Conjunction and = new Conjunction(schema); + and.add(filters); + return and; + } + private Restrictions() {} } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/operators/TokenOperator.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/operators/TokenOperator.java index 67f47a006d33..c3f4a0cd6edf 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/operators/TokenOperator.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/operators/TokenOperator.java @@ -61,7 +61,9 @@ public Criterion getHibernateCriterion(QueryPath queryPath) { @Override public Predicate getPredicate(CriteriaBuilder builder, Root root, QueryPath queryPath) { String value = caseSensitive ? getValue(String.class) : getValue(String.class).toLowerCase(); - + if (skipUidToken(value, queryPath)) { + return null; + } Predicate defaultSearch = builder.equal( builder.function( @@ -94,4 +96,15 @@ public Predicate getPredicate(CriteriaBuilder builder, Root root, QueryPa public boolean test(Object value) { return TokenUtils.test(value, getValue(String.class), caseSensitive, matchMode); } + + /** + * Return + * + * @param value + * @param query + * @return + */ + private boolean skipUidToken(String value, QueryPath query) { + return "uid".equals(query.getProperty().getFieldName()) && value.length() < 4; + } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/user/DefaultUserService.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/user/DefaultUserService.java index e2eeef2a4af7..6409343033a1 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/user/DefaultUserService.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/user/DefaultUserService.java @@ -70,9 +70,9 @@ import org.hisp.dhis.common.PasswordGenerator; import org.hisp.dhis.common.UID; import org.hisp.dhis.common.UserOrgUnitType; -import org.hisp.dhis.commons.filter.FilterUtils; import org.hisp.dhis.dataset.DataSet; import org.hisp.dhis.email.EmailResponse; +import org.hisp.dhis.feedback.ConflictException; import org.hisp.dhis.feedback.ErrorCode; import org.hisp.dhis.feedback.ErrorReport; import org.hisp.dhis.feedback.ForbiddenException; @@ -89,7 +89,6 @@ import org.hisp.dhis.security.TwoFactoryAuthenticationUtils; import org.hisp.dhis.security.acl.AclService; import org.hisp.dhis.setting.SystemSettingsProvider; -import org.hisp.dhis.system.filter.UserRoleCanIssueFilter; import org.hisp.dhis.system.util.ValidationUtils; import org.hisp.dhis.system.velocity.VelocityManager; import org.hisp.dhis.util.DateUtils; @@ -314,19 +313,35 @@ public List getUsers(UserQueryParams params) { public List getUsers(UserQueryParams params, @Nullable List orders) { handleUserQueryParams(params); - if (isNotValidUserQueryParams(params)) { + try { + validateUserQueryParams(params); + } catch (ConflictException ex) { + log.warn(ex.getMessage()); return Lists.newArrayList(); } return userStore.getUsers(params, orders); } + @Override + @Transactional(readOnly = true) + public List getUserIds(UserQueryParams params, @CheckForNull List orders) + throws ConflictException { + handleUserQueryParams(params); + validateUserQueryParams(params); + + return userStore.getUserIds(params, orders); + } + @Override @Transactional(readOnly = true) public int getUserCount(UserQueryParams params) { handleUserQueryParams(params); - if (isNotValidUserQueryParams(params)) { + try { + validateUserQueryParams(params); + } catch (ConflictException ex) { + log.warn(ex.getMessage()); return 0; } @@ -382,25 +397,21 @@ private void handleUserQueryParams(UserQueryParams params) { } } - private boolean isNotValidUserQueryParams(UserQueryParams params) { + private void validateUserQueryParams(UserQueryParams params) throws ConflictException { if (params.isCanManage() && (params.getUser() == null || !params.getUser().hasManagedGroups())) { - log.warn("Cannot get managed users as user does not have any managed groups"); - return true; + throw new ConflictException( + "Cannot get managed users as user does not have any managed groups"); } - if (params.isAuthSubset() && (params.getUser() == null || !params.getUser().hasAuthorities())) { - log.warn("Cannot get users with authority subset as user does not have any authorities"); - return true; + throw new ConflictException( + "Cannot get users with authority subset as user does not have any authorities"); } - if (params.isDisjointRoles() && (params.getUser() == null || !params.getUser().hasUserRoles())) { - log.warn("Cannot get users with disjoint roles as user does not have any user roles"); - return true; + throw new ConflictException( + "Cannot get users with disjoint roles as user does not have any user roles"); } - - return false; } @Override @@ -545,12 +556,15 @@ public int countDataSetUserRoles(DataSet dataSet) { @Override @Transactional(readOnly = true) - public void canIssueFilter(Collection userRoles) { - User user = getUserByUsername(CurrentUserUtil.getCurrentUsername()); + public List getRolesCurrentUserCanIssue() { + UserDetails user = CurrentUserUtil.getCurrentUserDetails(); boolean canGrantOwnUserRoles = settingsProvider.getCurrentSettings().getCanGrantOwnUserRoles(); - FilterUtils.filter(userRoles, new UserRoleCanIssueFilter(user, canGrantOwnUserRoles)); + return userRoleStore.getAll().stream() + .filter(role -> user.canIssueUserRole(role, canGrantOwnUserRoles)) + .map(role -> UID.of(role.getUid())) + .toList(); } @Override diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/user/hibernate/HibernateUserStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/user/hibernate/HibernateUserStore.java index b1ad62fc4f05..688c3a76c70f 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/user/hibernate/HibernateUserStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/user/hibernate/HibernateUserStore.java @@ -47,12 +47,9 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; -import java.util.stream.Collectors; -import java.util.stream.Stream; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -125,7 +122,7 @@ public void save(@Nonnull User user, boolean clearSharing) { @Override public List getUsers(UserQueryParams params, @Nullable List orders) { - Query userQuery = getUserQuery(params, orders, false); + Query userQuery = getUserQuery(params, orders, QueryMode.OBJECTS); return extractUserQueryUsers(userQuery.list()); } @@ -136,7 +133,7 @@ public List getUsers(UserQueryParams params) { @Override public List getExpiringUsers(UserQueryParams params) { - return extractUserQueryUsers(getUserQuery(params, null, false).list()); + return extractUserQueryUsers(getUserQuery(params, null, QueryMode.OBJECTS).list()); } @Override @@ -155,10 +152,17 @@ public List getExpiringUserAccounts(int inDays) { @Override public int getUserCount(UserQueryParams params) { - Long count = (Long) getUserQuery(params, null, true).uniqueResult(); + Long count = (Long) getUserQuery(params, null, QueryMode.COUNT).uniqueResult(); return count != null ? count.intValue() : 0; } + @Override + public List getUserIds(UserQueryParams params, @CheckForNull List orders) { + return getUserQuery(params, orders, QueryMode.IDS).stream() + .map(id -> UID.of((String) id)) + .toList(); + } + @Nonnull private List extractUserQueryUsers(@Nonnull List result) { if (result.isEmpty()) { @@ -176,30 +180,35 @@ private List extractUserQueryUsers(@Nonnull List result) { return users; } - private Query getUserQuery(UserQueryParams params, List orders, boolean count) { + private enum QueryMode { + OBJECTS, + COUNT, + IDS + } + + private Query getUserQuery(UserQueryParams params, List orders, QueryMode mode) { SqlHelper hlp = new SqlHelper(); List convertedOrder = null; - String hql = null; + String hql; - if (count) { + boolean fetch = mode == QueryMode.OBJECTS; + if (mode == QueryMode.COUNT) { hql = "select count(distinct u) "; + } else if (mode == QueryMode.IDS) { + hql = "select distinct u.uid "; } else { Schema userSchema = schemaService.getSchema(User.class); convertedOrder = QueryUtils.convertOrderStrings(orders, userSchema); - - hql = - Stream.of( - "select distinct u", - JpaQueryUtils.createSelectOrderExpression(convertedOrder, "u")) - .filter(Objects::nonNull) - .collect(Collectors.joining(",")); + String order = JpaQueryUtils.createSelectOrderExpression(convertedOrder, "u"); + hql = "select distinct u"; + if (order != null) hql += "," + order; hql += " "; } hql += "from User u "; - if (params.isPrefetchUserGroups() && !count) { + if (params.isPrefetchUserGroups() && fetch) { hql += "left join fetch u.groups g "; } else { hql += "left join u.groups g "; @@ -308,7 +317,7 @@ private Query getUserQuery(UserQueryParams params, List orders, boole + "and u.restoreExpiry < current_timestamp() "; } - if (!count) { + if (fetch) { String orderExpression = JpaQueryUtils.createOrderExpression(convertedOrder, "u"); hql += "order by " + StringUtils.defaultString(orderExpression, "u.surname, u.firstName"); } @@ -385,7 +394,7 @@ private Query getUserQuery(UserQueryParams params, List orders, boole query.setParameterList("userGroupIds", userGroupIds); } - if (!count) { + if (fetch) { if (params.getFirst() != null) { query.setFirstResult(params.getFirst()); } diff --git a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/dataitem/query/ExpressionDimensionItemQueryTest.java b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/dataitem/query/ExpressionDimensionItemQueryTest.java index 9d1098abed6e..70bdde9e0dcd 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/dataitem/query/ExpressionDimensionItemQueryTest.java +++ b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/dataitem/query/ExpressionDimensionItemQueryTest.java @@ -40,14 +40,11 @@ class ExpressionDimensionItemQueryTest { @Test void testGetStatementContainsOwnerCheck() { - // Given MapSqlParameterSource anyMap = new MapSqlParameterSource(); ExpressionDimensionItemQuery query = new ExpressionDimensionItemQuery(); - // When String statement = query.getStatement(anyMap); - // Then assertTrue(statement.contains("(jsonb_extract_path_text(t.item_sharing, 'owner') = :userUid)")); } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/expression/ExpressionService2Test.java b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/expression/ExpressionServiceTest.java similarity index 98% rename from dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/expression/ExpressionService2Test.java rename to dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/expression/ExpressionServiceTest.java index e1c30e9ca515..b472290e850f 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/expression/ExpressionService2Test.java +++ b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/expression/ExpressionServiceTest.java @@ -60,6 +60,7 @@ import com.google.common.collect.Sets; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -112,7 +113,7 @@ * @author Luciano Fiandesio */ @ExtendWith(MockitoExtension.class) -class ExpressionService2Test extends TestBase { +class ExpressionServiceTest extends TestBase { @Mock private HibernateGenericStore hibernateGenericStore; @Mock private ConstantService constantService; @@ -229,8 +230,6 @@ class ExpressionService2Test extends TestBase { private String expressionN; - private String expressionO; - private String expressionP; private String expressionR; @@ -392,7 +391,6 @@ public void setUp() { + SEPARATOR + cocA.getUid() + "}"; - expressionO = "#{" + opA.getDimensionItem() + "}+sum(#{" + opB.getDimensionItem() + "})"; expressionP = "#{" + deB.getUid() + SEPARATOR + coc.getUid() + "}"; expressionR = "#{" @@ -693,20 +691,17 @@ void testGetBaseExpressionParams() { when(idObjectManager.getNoAcl( eq(OrganisationUnitGroup.class), - (java.util.Collection) - argThat(containsInAnyOrder(groupA.getUid(), groupB.getUid())))) + (Collection) argThat(containsInAnyOrder(groupA.getUid(), groupB.getUid())))) .thenReturn(List.of(groupA, groupB)); when(idObjectManager.getNoAcl( eq(DataSet.class), - (java.util.Collection) - argThat(containsInAnyOrder(dataSetA.getUid(), dataSetB.getUid())))) + (Collection) argThat(containsInAnyOrder(dataSetA.getUid(), dataSetB.getUid())))) .thenReturn(List.of(dataSetA, dataSetB)); when(idObjectManager.getNoAcl( eq(Program.class), - (java.util.Collection) - argThat(containsInAnyOrder(programA.getUid(), programB.getUid())))) + (Collection) argThat(containsInAnyOrder(programA.getUid(), programB.getUid())))) .thenReturn(List.of(programA, programB)); ExpressionParams baseParams = target.getBaseExpressionParams(info); diff --git a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/period/PeriodDataProviderTest.java b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/period/PeriodDataProviderTest.java index edd13c5cb893..6fdc1b699d3f 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/period/PeriodDataProviderTest.java +++ b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/period/PeriodDataProviderTest.java @@ -31,8 +31,8 @@ import static org.hisp.dhis.period.PeriodDataProvider.BEFORE_AND_AFTER_DATA_YEARS_SUPPORTED; import static org.hisp.dhis.period.PeriodDataProvider.DEFAULT_FIRST_YEAR_SUPPORTED; import static org.hisp.dhis.period.PeriodDataProvider.DEFAULT_LATEST_YEAR_SUPPORTED; -import static org.hisp.dhis.period.PeriodDataProvider.DataSource.DATABASE; -import static org.hisp.dhis.period.PeriodDataProvider.DataSource.SYSTEM_DEFINED; +import static org.hisp.dhis.period.PeriodDataProvider.PeriodSource.DATABASE; +import static org.hisp.dhis.period.PeriodDataProvider.PeriodSource.SYSTEM_DEFINED; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; diff --git a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/query/DefaultQueryServiceTest.java b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/query/DefaultQueryServiceTest.java index 299d2fde1de6..bb84e91d776f 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/query/DefaultQueryServiceTest.java +++ b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/query/DefaultQueryServiceTest.java @@ -73,7 +73,7 @@ public void setUp() { QueryPlanner queryPlanner = new DefaultQueryPlanner(schemaService, settingsService); subject = new DefaultQueryService( - queryParser, queryPlanner, criteriaQueryEngine, inMemoryQueryEngine); + queryParser, queryPlanner, schemaService, criteriaQueryEngine, inMemoryQueryEngine); } @Test diff --git a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/query/GetObjectListParamsTest.java b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/query/GetObjectListParamsTest.java new file mode 100644 index 000000000000..69ba2eac6088 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/query/GetObjectListParamsTest.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.query; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import org.junit.jupiter.api.Test; + +/** + * Tests the parameter processing implemented in {@link GetObjectListParams}. + * + * @author Jan Bernitt + */ +class GetObjectListParamsTest { + + @Test + void testSplitFilters_empty() { + assertFilters(null, ""); + assertFilters(null, null); + } + + @Test + void testSplitFilters_2parts() { + assertFilters(List.of("name:empty"), "name:empty"); + assertFilters(List.of("organisationUnits:!empty"), "organisationUnits:!empty"); + assertFilters( + List.of("name:empty", "organisationUnits:!empty"), "name:empty,organisationUnits:!empty"); + } + + @Test + void testSplitFilters_3parts() { + assertFilters(List.of("name:eq:Peter"), "name:eq:Peter"); + assertFilters(List.of("parent.id:eq:a1234t6u8o"), "parent.id:eq:a1234t6u8o"); + assertFilters( + List.of("name:eq:Peter", "parent.id:eq:a1234t6u8o"), + "name:eq:Peter,parent.id:eq:a1234t6u8o"); + } + + @Test + void testSplitFilters_in() { + assertFilters(List.of("name:in:[Peter,Paul,Mary]"), "name:in:[Peter,Paul,Mary]"); + assertFilters( + List.of("age:eq:20", "name:in:[Peter,Paul,Mary]", "points:lt:200"), + "age:eq:20,name:in:[Peter,Paul,Mary],points:lt:200"); + } + + @Test + void testSplitFilters_allOperators() { + List ops = + List.of( + "eq", + "ieq", + "!eq", + "neq", + "ne", + "gt", + "lt", + "gte", + "ge", + "lte", + "le", + "like", + "!like", + "$like", + "!$like", + "like$", + "!like$", + "ilike", + "!ilike", + "startsWith", + "$ilike", + "!$ilike", + "token", + "!token", + "endsWith", + "ilkike$", + "!ilike$", + "in", + "!in", + "null", + "empty", + "!null"); + for (String op : ops) { + String filter = "name:%s:value".formatted(op); + assertFilters(List.of(filter), filter); + assertFilters(List.of("foo:eq:bar", filter), "foo:eq:bar," + filter); + assertFilters(List.of("foo:eq:bar", filter, "x:empty"), "foo:eq:bar," + filter + ",x:empty"); + } + } + + private static void assertFilters(List expected, String actual) { + assertEquals(expected, GetObjectListParams.splitFilters(actual)); + } +} diff --git a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/query/TokenOperatorTest.java b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/query/TokenOperatorTest.java index d01901dfd3e1..9ba40b3223f6 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/query/TokenOperatorTest.java +++ b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/query/TokenOperatorTest.java @@ -28,11 +28,15 @@ package org.hisp.dhis.query; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import org.hisp.dhis.query.operators.MatchMode; import org.hisp.dhis.query.operators.NotTokenOperator; import org.hisp.dhis.query.operators.TokenOperator; +import org.hisp.dhis.query.planner.QueryPath; +import org.hisp.dhis.schema.Property; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; /** @@ -43,6 +47,16 @@ */ class TokenOperatorTest { + @Test + @DisplayName("Uid Token filter must have at least 4 characters. Otherwise return null.") + void testUidLength() { + TokenOperator operator = new TokenOperator("ABC", false, MatchMode.ANYWHERE); + Property property = new Property(); + property.setFieldName("uid"); + QueryPath queryPath = new QueryPath(property, true); + assertNull(operator.getPredicate(null, null, queryPath)); + } + @Test void nullValue() { for (MatchMode mode : MatchMode.values()) { diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/DefaultMetadataExportService.java b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/DefaultMetadataExportService.java index 31f7ffd44176..6a445daafc06 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/DefaultMetadataExportService.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/metadata/DefaultMetadataExportService.java @@ -36,7 +36,6 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.collect.Sets; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; @@ -69,7 +68,6 @@ import org.hisp.dhis.dataset.DataSetElement; import org.hisp.dhis.dataset.Section; import org.hisp.dhis.document.Document; -import org.hisp.dhis.dxf2.common.OrderParams; import org.hisp.dhis.eventchart.EventChart; import org.hisp.dhis.eventreport.EventReport; import org.hisp.dhis.eventvisualization.EventVisualization; @@ -97,6 +95,7 @@ import org.hisp.dhis.programrule.ProgramRuleService; import org.hisp.dhis.programrule.ProgramRuleVariable; import org.hisp.dhis.programrule.ProgramRuleVariableService; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.query.Query; import org.hisp.dhis.query.QueryService; import org.hisp.dhis.report.Report; @@ -177,12 +176,12 @@ public Map, List> parameter for (Map.Entry, Map>> entry : map.entrySet()) { Map> classMap = entry.getValue(); - Schema schema = schemaService.getDynamicSchema(entry.getKey()); - - if (classMap.containsKey("fields")) params.addFields(entry.getKey(), classMap.get("fields")); - - if (classMap.containsKey("filter") && classMap.containsKey("order")) { - OrderParams orderParams = new OrderParams(Sets.newHashSet(classMap.get("order"))); - Query query = - queryService.getQueryFromUrl( - entry.getKey(), classMap.get("filter"), orderParams.getOrders(schema)); - query.setDefaultOrder(); - params.addQuery(query); - } else if (classMap.containsKey("filter")) { - Query query = - queryService.getQueryFromUrl(entry.getKey(), classMap.get("filter"), new ArrayList<>()); - query.setDefaultOrder(); - params.addQuery(query); - } else if (classMap.containsKey("order")) { - OrderParams orderParams = new OrderParams(); - orderParams.setOrder(Sets.newHashSet(classMap.get("order"))); - - Query query = - queryService.getQueryFromUrl( - entry.getKey(), new ArrayList<>(), orderParams.getOrders(schema)); + Class objectType = entry.getKey(); + if (classMap.containsKey("fields")) params.addFields(objectType, classMap.get("fields")); + + List filters = classMap.get("filter"); + List orders = classMap.get("order"); + if (filters != null || orders != null) { + GetObjectListParams queryParams = + new GetObjectListParams().setPaging(false).setFilters(filters).setOrders(orders); + Query query = queryService.getQueryFromUrl(objectType, queryParams); query.setDefaultOrder(); params.addQuery(query); } diff --git a/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/DefaultMetadataExportServiceTest.java b/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/DefaultMetadataExportServiceTest.java index 376ddc884e9d..2901c0842942 100644 --- a/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/DefaultMetadataExportServiceTest.java +++ b/dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/metadata/DefaultMetadataExportServiceTest.java @@ -64,6 +64,7 @@ import org.hisp.dhis.scheduling.JobConfiguration; import org.hisp.dhis.schema.Schema; import org.hisp.dhis.schema.SchemaService; +import org.hisp.dhis.user.SystemUser; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -214,12 +215,13 @@ void deprecatedAnalyticClassesSchemaRemovedTest() { schemas.forEach(s -> s.setPersisted(true)); Query query = Query.from(new Schema(EventReport.class, "eventReport", "eventReports")); - when(queryService.getQueryFromUrl(any(), any(), any())).thenReturn(query); + when(queryService.getQueryFromUrl(any(), any())).thenReturn(query); // return 5 schemas, including the 2 for the deprecated classes EventChart & EventReport when(schemaService.getMetadataSchemas()).thenReturn(schemas); // when + params.setCurrentUserDetails(new SystemUser()); service.getMetadata(params); // then diff --git a/dhis-2/dhis-services/dhis-service-node/pom.xml b/dhis-2/dhis-services/dhis-service-node/pom.xml index 5e75d0c0aa15..e3e72dfbabaa 100644 --- a/dhis-2/dhis-services/dhis-service-node/pom.xml +++ b/dhis-2/dhis-services/dhis-service-node/pom.xml @@ -127,10 +127,5 @@ mockito-junit-jupiter test - - org.hisp.dhis - dhis-service-field-filtering - - diff --git a/dhis-2/dhis-services/dhis-service-tracker/pom.xml b/dhis-2/dhis-services/dhis-service-tracker/pom.xml index 40847583dc15..e4067be79ef4 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/pom.xml +++ b/dhis-2/dhis-services/dhis-service-tracker/pom.xml @@ -33,10 +33,6 @@ org.hisp.dhis dhis-service-acl - - org.hisp.dhis - dhis-service-node - org.hisp.dhis dhis-support-artemis diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/DefaultEventChangeLogService.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/DefaultEventChangeLogService.java index c1dae08647a5..d3cfb0c0b417 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/DefaultEventChangeLogService.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/DefaultEventChangeLogService.java @@ -39,6 +39,7 @@ import java.util.stream.Stream; import javax.annotation.Nonnull; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.tuple.Pair; import org.hisp.dhis.changelog.ChangeLogType; import org.hisp.dhis.common.UID; import org.hisp.dhis.dataelement.DataElement; @@ -69,8 +70,7 @@ public Page getEventChangeLog( // check existence and access eventService.getEvent(event); - return hibernateEventChangeLogStore.getEventChangeLogs( - event, operationParams.getOrder(), pageParams); + return hibernateEventChangeLogStore.getEventChangeLogs(event, operationParams, pageParams); } @Transactional @@ -165,6 +165,11 @@ public Set getOrderableFields() { return hibernateEventChangeLogStore.getOrderableFields(); } + @Override + public Set>> getFilterableFields() { + return hibernateEventChangeLogStore.getFilterableFields(); + } + private void logIfChanged( String propertyName, Function valueExtractor, diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/EventChangeLogOperationParams.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/EventChangeLogOperationParams.java index d162f53b2fce..bbd8516024aa 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/EventChangeLogOperationParams.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/EventChangeLogOperationParams.java @@ -31,6 +31,8 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; +import org.apache.commons.lang3.tuple.Pair; +import org.hisp.dhis.common.QueryFilter; import org.hisp.dhis.common.SortDirection; import org.hisp.dhis.tracker.export.Order; @@ -40,20 +42,30 @@ public class EventChangeLogOperationParams { private Order order; + private Pair filter; public static class EventChangeLogOperationParamsBuilder { - // Do not remove this unused method. This hides the order field from the builder which Lombok - // does not support. The repeated order field and private order method prevent access to order - // via the builder. - // Order should be added via the orderBy builder methods. + // Do not remove these unused methods. They hide the order and filter fields from the builder + // which Lombok + // does not support. + // They should be added via their respective orderBy and filterBy builder methods. private EventChangeLogOperationParamsBuilder order(Order order) { return this; } + private EventChangeLogOperationParamsBuilder filter(Pair filter) { + return this; + } + public EventChangeLogOperationParamsBuilder orderBy(String field, SortDirection direction) { this.order = new Order(field, direction); return this; } + + public EventChangeLogOperationParamsBuilder filterBy(String field, QueryFilter filter) { + this.filter = Pair.of(field, filter); + return this; + } } } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/EventChangeLogService.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/EventChangeLogService.java index 8d408d6ca947..c66b2e6e46c3 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/EventChangeLogService.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/EventChangeLogService.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Set; import javax.annotation.Nonnull; +import org.apache.commons.lang3.tuple.Pair; import org.hisp.dhis.changelog.ChangeLogType; import org.hisp.dhis.common.UID; import org.hisp.dhis.dataelement.DataElement; @@ -85,9 +86,17 @@ void addPropertyChangeLog( /** * Fields the {@link #getEventChangeLog(UID, EventChangeLogOperationParams, PageParams)} can order - * event change logs by. Ordering by fields other than these is considered a programmer error. + * event change logs by. Ordering by fields other than these, is considered a programmer error. * Validation of user provided field names should occur before calling {@link * #getEventChangeLog(UID, EventChangeLogOperationParams, PageParams)}. */ Set getOrderableFields(); + + /** + * Fields the {@link #getEventChangeLog(UID, EventChangeLogOperationParams, PageParams)} can + * filter event change logs by. Filtering by fields other than these, is considered a programmer + * error. Validation of user provided field names should occur before calling {@link + * #getEventChangeLog(UID, EventChangeLogOperationParams, PageParams)}. + */ + Set>> getFilterableFields(); } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/HibernateEventChangeLogStore.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/HibernateEventChangeLogStore.java index 1095cef26698..b06bf4523e9d 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/HibernateEventChangeLogStore.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/HibernateEventChangeLogStore.java @@ -34,11 +34,13 @@ import java.util.Date; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import javax.annotation.Nonnull; -import javax.annotation.Nullable; +import org.apache.commons.lang3.tuple.Pair; import org.hibernate.Session; import org.hisp.dhis.changelog.ChangeLogType; +import org.hisp.dhis.common.QueryFilter; import org.hisp.dhis.common.SortDirection; import org.hisp.dhis.common.UID; import org.hisp.dhis.dataelement.DataElement; @@ -73,6 +75,12 @@ public class HibernateEventChangeLogStore { entry("dataElement", COLUMN_CHANGELOG_DATA_ELEMENT), entry("property", COLUMN_CHANGELOG_PROPERTY)); + private static final Map>, String> FILTERABLE_FIELDS = + Map.ofEntries( + entry(Pair.of("username", String.class), COLUMN_CHANGELOG_USER), + entry(Pair.of("dataElement", UID.class), COLUMN_CHANGELOG_DATA_ELEMENT), + entry(Pair.of("property", String.class), COLUMN_CHANGELOG_PROPERTY)); + private final EntityManager entityManager; public HibernateEventChangeLogStore(EntityManager entityManager) { @@ -84,11 +92,14 @@ public void addEventChangeLog(EventChangeLog eventChangeLog) { } public Page getEventChangeLogs( - @Nonnull UID event, @Nullable Order order, @Nonnull PageParams pageParams) { + @Nonnull UID event, + @Nonnull EventChangeLogOperationParams operationParams, + @Nonnull PageParams pageParams) { + + Pair filter = operationParams.getFilter(); String hql = - String.format( - """ + """ select ecl.event, ecl.dataElement, ecl.eventProperty, @@ -105,15 +116,30 @@ public Page getEventChangeLogs( left join ecl.dataElement d left join ecl.createdBy u where e.uid = :eventUid - order by %s - """ - .formatted(sortExpressions(order))); + """; + + if (filter != null) { + String filterField = + FILTERABLE_FIELDS.entrySet().stream() + .filter(entry -> entry.getKey().getLeft().equals(filter.getKey())) + .findFirst() + .map(Entry::getValue) + .get(); + + hql += String.format(" and %s = :filterValue ", filterField); + } + + hql += String.format("order by %s".formatted(sortExpressions(operationParams.getOrder()))); Query query = entityManager.createQuery(hql); query.setParameter("eventUid", event.getValue()); query.setFirstResult((pageParams.getPage() - 1) * pageParams.getPageSize()); query.setMaxResults(pageParams.getPageSize() + 1); + if (filter != null) { + query.setParameter("filterValue", filter.getValue().getFilter()); + } + List results = query.getResultList(); List eventChangeLogs = results.stream() @@ -189,4 +215,8 @@ private static String sortExpressions(Order order) { public Set getOrderableFields() { return ORDERABLE_FIELDS.keySet(); } + + public Set>> getFilterableFields() { + return FILTERABLE_FIELDS.keySet(); + } } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/JdbcEventStore.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/JdbcEventStore.java index 5f936331d2bc..8de7b2d18e6f 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/JdbcEventStore.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/export/event/JdbcEventStore.java @@ -288,7 +288,8 @@ private List fetchEvents(EventQueryParams queryParams, PageParams pagePar mapSqlParameterSource, resultSet -> { Set notes = new HashSet<>(); - Set dataElementUids = new HashSet<>(); + // data elements per event + Map> dataElementUids = new HashMap<>(); while (resultSet.next()) { if (resultSet.getString(COLUMN_EVENT_UID) == null) { @@ -305,6 +306,7 @@ private List fetchEvents(EventQueryParams queryParams, PageParams pagePar event.setId(resultSet.getLong(COLUMN_EVENT_ID)); event.setUid(eventUid); eventsByUid.put(eventUid, event); + dataElementUids.put(eventUid, new HashSet<>()); TrackedEntity te = new TrackedEntity(); te.setUid(resultSet.getString(COLUMN_TRACKEDENTITY_UID)); @@ -437,11 +439,11 @@ private List fetchEvents(EventQueryParams queryParams, PageParams pagePar // data value per data element. The same data element can be in the result set // multiple times if the event also has notes. String dataElementUid = resultSet.getString("de_uid"); - if (!dataElementUids.contains(dataElementUid)) { + if (!dataElementUids.get(eventUid).contains(dataElementUid)) { EventDataValue eventDataValue = parseEventDataValue(dataElementIdScheme, resultSet); if (eventDataValue != null) { event.getEventDataValues().add(eventDataValue); - dataElementUids.add(dataElementUid); + dataElementUids.get(eventUid).add(dataElementUid); } } } @@ -650,7 +652,6 @@ lateral jsonb_each( ) as eventdatavalue(dataelement_uid, value) on true left join dataelement de on de.uid = eventdatavalue.dataelement_uid -where eventdatavalue.dataelement_uid is not null """); } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/DateValidator.java b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/DateValidator.java index f541434cdc55..b04a0a71f038 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/DateValidator.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/main/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/DateValidator.java @@ -36,7 +36,7 @@ import static org.hisp.dhis.tracker.imports.validation.ValidationCode.E1052; import java.time.LocalDate; -import java.time.ZoneOffset; +import java.time.ZoneId; import java.util.Objects; import org.hisp.dhis.program.Program; import org.hisp.dhis.tracker.imports.bundle.TrackerBundle; @@ -84,13 +84,13 @@ private void validateEnrollmentDatesNotInFuture( final LocalDate now = LocalDate.now(); if (Objects.nonNull(enrollment.getEnrolledAt()) && Boolean.FALSE.equals(program.getSelectEnrollmentDatesInFuture()) - && enrollment.getEnrolledAt().atOffset(ZoneOffset.UTC).toLocalDate().isAfter(now)) { + && enrollment.getEnrolledAt().atZone(ZoneId.systemDefault()).toLocalDate().isAfter(now)) { reporter.addError(enrollment, E1020, enrollment.getEnrolledAt()); } if (Objects.nonNull(enrollment.getOccurredAt()) && Boolean.FALSE.equals(program.getSelectIncidentDatesInFuture()) - && enrollment.getOccurredAt().atOffset(ZoneOffset.UTC).toLocalDate().isAfter(now)) { + && enrollment.getOccurredAt().atZone(ZoneId.systemDefault()).toLocalDate().isAfter(now)) { reporter.addError(enrollment, E1021, enrollment.getOccurredAt()); } } diff --git a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/DateValidatorTest.java b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/DateValidatorTest.java index 8797e7c53603..4c05e068becf 100644 --- a/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/DateValidatorTest.java +++ b/dhis-2/dhis-services/dhis-service-tracker/src/test/java/org/hisp/dhis/tracker/imports/validation/validator/enrollment/DateValidatorTest.java @@ -41,6 +41,8 @@ import java.time.Duration; import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; import org.hisp.dhis.common.CodeGenerator; import org.hisp.dhis.common.UID; import org.hisp.dhis.program.EnrollmentStatus; @@ -101,7 +103,7 @@ void testMandatoryDatesMustBePresent() { @Test void testDatesMustNotBeInTheFuture() { - final Instant dateInTheFuture = now().plus(Duration.ofDays(2)); + final Instant dateInTheFuture = now().plus(Duration.ofDays(1)); Enrollment enrollment = Enrollment.builder() .enrollment(UID.generate()) @@ -119,6 +121,33 @@ void testDatesMustNotBeInTheFuture() { () -> assertHasError(reporter, enrollment, E1021)); } + @Test + void testDatesWithNoTimeZoneMustNotBeInTheFuture() { + ZoneId systemZone = ZoneId.systemDefault(); + LocalDate tomorrow = LocalDate.now(systemZone).plusDays(1); + Instant dateTomorrow = tomorrow.atStartOfDay(systemZone).toInstant(); + + // Create enrollment with dates set to tomorrow + Enrollment enrollment = + Enrollment.builder() + .enrollment(UID.generate()) + .program(MetadataIdentifier.ofUid(CodeGenerator.generateUid())) + .occurredAt(dateTomorrow) + .enrolledAt(dateTomorrow) + .build(); + + when(preheat.getProgram(enrollment.getProgram())).thenReturn(new Program()); + + // Run validation + validator.validate(reporter, bundle, enrollment); + + // Assert that the future dates are detected as errors + assertAll( + () -> assertHasError(reporter, enrollment, E1020), // enrolledAt in the future + () -> assertHasError(reporter, enrollment, E1021) // occurredAt in the future + ); + } + @Test void testDatesShouldBeAllowedOnSameDayIfFutureDatesAreNotAllowed() { final Instant today = now().plus(Duration.ofMinutes(1)); diff --git a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/utils/Assertions.java b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/utils/Assertions.java index 2a25cb922a87..043f4389d685 100644 --- a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/utils/Assertions.java +++ b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/utils/Assertions.java @@ -66,6 +66,19 @@ private Assertions() { * @param actual the actual collection. */ public static void assertContainsOnly(Collection expected, Collection actual) { + assertContainsOnly(expected, actual, "assertContainsOnly found mismatch"); + } + + /** + * Asserts that the given collection contains exactly the given items in any order. + * + * @param the type. + * @param expected the expected items. + * @param actual the actual collection. + * @param heading the assertAll heading + */ + public static void assertContainsOnly( + Collection expected, Collection actual, String heading) { assertNotNull( actual, () -> String.format("Expected collection to contain %s, got null instead", expected)); @@ -73,7 +86,7 @@ public static void assertContainsOnly(Collection expected, Collection List missing = CollectionUtils.difference(expected, actual); List extra = CollectionUtils.difference(actual, expected); assertAll( - "assertContainsOnly found mismatch", + heading, () -> assertTrue( missing.isEmpty(), () -> String.format("Expected %s to be in %s", missing, actual)), @@ -128,6 +141,17 @@ public static void assertIsEmpty(Collection actual) { assertTrue(actual.isEmpty(), actual.toString()); } + /** + * Asserts that the given collection is not null and empty. + * + * @param actual the collection. + * @param message fails with this message + */ + public static void assertIsEmpty(Collection actual, String message) { + assertNotNull(actual, message); + assertTrue(actual.isEmpty(), message); + } + /** * Asserts that the given collection is not null and not empty. * diff --git a/dhis-2/dhis-test-e2e/pom.xml b/dhis-2/dhis-test-e2e/pom.xml index 635ff8083e5a..e9c118efe62a 100644 --- a/dhis-2/dhis-test-e2e/pom.xml +++ b/dhis-2/dhis-test-e2e/pom.xml @@ -31,7 +31,7 @@ 4.2.2 1.18.36 2.29.0 - 4.26.0 + 4.27.0 2.0.16 4.5.14 5.4.1 diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/message/MessageConversationStoreTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/message/MessageConversationStoreTest.java index c741e08ab594..1348f2e5ad44 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/message/MessageConversationStoreTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/message/MessageConversationStoreTest.java @@ -34,6 +34,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import org.hisp.dhis.common.UID; import org.hisp.dhis.test.integration.PostgresIntegrationTestBase; import org.hisp.dhis.user.User; import org.junit.jupiter.api.BeforeAll; @@ -76,7 +77,7 @@ void setUp() { conversationIds = new HashSet<>(); conversationA = messageService.sendPrivateMessage(usersA, "Subject1", "Text", "Meta", null); MessageConversation mc = messageService.getMessageConversation(conversationA); - mc.markRead(userC); + mc.markRead(UID.of(userC.getUid())); messageService.updateMessageConversation(mc); conversationIds.add(mc.getUid()); messageService.sendReply(mc, "Message 1", "Meta", false, null); diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/query/CriteriaQueryEngineTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/query/CriteriaQueryEngineTest.java index 5ea5a8be4a4a..c31bf545a0fd 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/query/CriteriaQueryEngineTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/query/CriteriaQueryEngineTest.java @@ -27,8 +27,6 @@ */ package org.hisp.dhis.query; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -509,9 +507,9 @@ void testIdentifiableSearch7() { @Test void testUnicodeSearch() { addDataElement('U', "Кириллица", ValueType.NUMBER, "2021"); - Query query = - queryService.getQueryFromUrl( - DataElement.class, singletonList("identifiable:token:Кири"), emptyList()); + GetObjectListParams params = + new GetObjectListParams().setPaging(false).setFilters(List.of("identifiable:token:Кири")); + Query query = queryService.getQueryFromUrl(DataElement.class, params); List matches = queryService.query(query); assertEquals(1, matches.size()); assertEquals("Кириллица", matches.get(0).getName()); diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/query/QueryServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/query/QueryServiceTest.java index bca5b662cc5f..8a401834d99c 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/query/QueryServiceTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/query/QueryServiceTest.java @@ -119,9 +119,8 @@ void getAllQuery() { @Test void getAllQueryUrl() throws QueryParserException { - Query query = - queryService.getQueryFromUrl( - DataElement.class, Lists.newArrayList(), Lists.newArrayList(), new Pagination()); + GetObjectListParams params = new GetObjectListParams().setPaging(false); + Query query = queryService.getQueryFromUrl(DataElement.class, params); assertEquals(6, queryService.query(query).size()); } @@ -147,10 +146,8 @@ void getAllQueryUrlWithPaginationReturnsNoDataWhenPageNumberHigherThanMaxNumberO } private List getResultWithPagination(int page, int pageSize) { - Pagination pagination = new Pagination(page, pageSize); - Query query = - queryService.getQueryFromUrl( - DataElement.class, Lists.newArrayList(), Lists.newArrayList(), pagination); + GetObjectListParams params = new GetObjectListParams().setPage(page).setPageSize(pageSize); + Query query = queryService.getQueryFromUrl(DataElement.class, params); return queryService.query(query); } @@ -177,12 +174,9 @@ void getEqQuery() { @Test void getEqQueryUrl() throws QueryParserException { - Query query = - queryService.getQueryFromUrl( - DataElement.class, - Lists.newArrayList("id:eq:deabcdefghA"), - Lists.newArrayList(), - new Pagination()); + GetObjectListParams params = + new GetObjectListParams().setPaging(false).setFilters(List.of("id:eq:deabcdefghA")); + Query query = queryService.getQueryFromUrl(DataElement.class, params); List objects = queryService.query(query); assertEquals(1, objects.size()); assertEquals("deabcdefghA", objects.get(0).getUid()); @@ -233,12 +227,9 @@ void getIlikeQueryNoMatchExtraCharAtStart() { @Test void getIeqQueryUrlMatch() throws QueryParserException { - Query query = - queryService.getQueryFromUrl( - DataElement.class, - Lists.newArrayList("name:ieq:dataelementa"), - Lists.newArrayList(), - new Pagination()); + GetObjectListParams params = + new GetObjectListParams().setPaging(false).setFilters(List.of("name:ieq:dataelementa")); + Query query = queryService.getQueryFromUrl(DataElement.class, params); List objects = queryService.query(query); assertEquals(1, objects.size()); assertEquals("DataElementA", objects.get(0).getName()); @@ -246,12 +237,9 @@ void getIeqQueryUrlMatch() throws QueryParserException { @Test void getIeqQueryUrlMatchMixedCase() throws QueryParserException { - Query query = - queryService.getQueryFromUrl( - DataElement.class, - Lists.newArrayList("name:ieq:dAtAeLeMeNta"), - Lists.newArrayList(), - new Pagination()); + GetObjectListParams params = + new GetObjectListParams().setPaging(false).setFilters(List.of("name:ieq:dAtAeLeMeNta")); + Query query = queryService.getQueryFromUrl(DataElement.class, params); List objects = queryService.query(query); assertEquals(1, objects.size()); assertEquals("DataElementA", objects.get(0).getName()); @@ -259,12 +247,9 @@ void getIeqQueryUrlMatchMixedCase() throws QueryParserException { @Test void getIeqQueryUrlNoMatchExtraCharAtStart() throws QueryParserException { - Query query = - queryService.getQueryFromUrl( - DataElement.class, - Lists.newArrayList("name:ieq:ddataelementa"), - Lists.newArrayList(), - new Pagination()); + GetObjectListParams params = + new GetObjectListParams().setPaging(false).setFilters(List.of("name:ieq:ddataelementa")); + Query query = queryService.getQueryFromUrl(DataElement.class, params); List objects = queryService.query(query); assertEquals(0, objects.size()); } @@ -285,12 +270,9 @@ void getNeQuery() { @Test void getNeQueryUrl() throws QueryParserException { - Query query = - queryService.getQueryFromUrl( - DataElement.class, - Lists.newArrayList("id:ne:deabcdefghA"), - Lists.newArrayList(), - new Pagination()); + GetObjectListParams params = + new GetObjectListParams().setPaging(false).setFilters(List.of("id:ne:deabcdefghA")); + Query query = queryService.getQueryFromUrl(DataElement.class, params); List objects = queryService.query(query); assertEquals(5, objects.size()); assertFalse(collectionContainsUid(objects, "deabcdefghA")); @@ -312,12 +294,9 @@ void getLikeQuery() { @Test void getLikeQueryUrl() throws QueryParserException { - Query query = - queryService.getQueryFromUrl( - DataElement.class, - Lists.newArrayList("name:like:F"), - Lists.newArrayList(), - new Pagination()); + GetObjectListParams params = + new GetObjectListParams().setPaging(false).setFilters(List.of("name:like:F")); + Query query = queryService.getQueryFromUrl(DataElement.class, params); List objects = queryService.query(query); assertEquals(1, objects.size()); assertEquals("deabcdefghF", objects.get(0).getUid()); @@ -336,12 +315,9 @@ void getGtQuery() { @Test void getGtQueryUrl() throws QueryParserException { - Query query = - queryService.getQueryFromUrl( - DataElement.class, - Lists.newArrayList("created:gt:2003"), - Lists.newArrayList(), - new Pagination()); + GetObjectListParams params = + new GetObjectListParams().setPaging(false).setFilters(List.of("created:gt:2003")); + Query query = queryService.getQueryFromUrl(DataElement.class, params); List objects = queryService.query(query); assertEquals(3, objects.size()); assertTrue(collectionContainsUid(objects, "deabcdefghD")); @@ -361,12 +337,9 @@ void getLtQuery() { @Test void getLtQueryUrl() throws QueryParserException { - Query query = - queryService.getQueryFromUrl( - DataElement.class, - Lists.newArrayList("created:lt:2003"), - Lists.newArrayList(), - new Pagination()); + GetObjectListParams params = + new GetObjectListParams().setPaging(false).setFilters(List.of("created:lt:2003")); + Query query = queryService.getQueryFromUrl(DataElement.class, params); List objects = queryService.query(query); assertEquals(2, objects.size()); assertTrue(collectionContainsUid(objects, "deabcdefghA")); @@ -387,12 +360,9 @@ void getGeQuery() { @Test void getGeQueryUrl() throws QueryParserException { - Query query = - queryService.getQueryFromUrl( - DataElement.class, - Lists.newArrayList("created:ge:2003"), - Lists.newArrayList(), - new Pagination()); + GetObjectListParams params = + new GetObjectListParams().setPaging(false).setFilters(List.of("created:ge:2003")); + Query query = queryService.getQueryFromUrl(DataElement.class, params); List objects = queryService.query(query); assertEquals(4, objects.size()); assertTrue(collectionContainsUid(objects, "deabcdefghC")); @@ -414,12 +384,9 @@ void getLeQuery() { @Test void getLeQueryUrl() throws QueryParserException { - Query query = - queryService.getQueryFromUrl( - DataElement.class, - Lists.newArrayList("created:le:2003"), - Lists.newArrayList(), - new Pagination()); + GetObjectListParams params = + new GetObjectListParams().setPaging(false).setFilters(List.of("created:le:2003")); + Query query = queryService.getQueryFromUrl(DataElement.class, params); List objects = queryService.query(query); assertEquals(3, objects.size()); assertTrue(collectionContainsUid(objects, "deabcdefghA")); @@ -587,12 +554,9 @@ void testIsNotNull() { @Test void testIsNotNullUrl() throws QueryParserException { - Query query = - queryService.getQueryFromUrl( - DataElement.class, - Lists.newArrayList("categoryCombo:!null"), - Lists.newArrayList(), - new Pagination()); + GetObjectListParams params = + new GetObjectListParams().setPaging(false).setFilters(List.of("categoryCombo:!null")); + Query query = queryService.getQueryFromUrl(DataElement.class, params); List objects = queryService.query(query); assertEquals(6, objects.size()); assertTrue(collectionContainsUid(objects, "deabcdefghA")); diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/event/OrderEventChangeLogTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/event/OrderAndFilterEventChangeLogTest.java similarity index 85% rename from dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/event/OrderEventChangeLogTest.java rename to dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/event/OrderAndFilterEventChangeLogTest.java index 74d9bb5c3238..dc14a6afde5b 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/event/OrderEventChangeLogTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/tracker/export/event/OrderAndFilterEventChangeLogTest.java @@ -27,6 +27,7 @@ */ package org.hisp.dhis.tracker.export.event; +import static org.hisp.dhis.test.utils.Assertions.assertContainsOnly; import static org.hisp.dhis.tracker.Assertions.assertNoErrors; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -35,8 +36,12 @@ import java.io.IOException; import java.time.Instant; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.hisp.dhis.common.IdentifiableObjectManager; +import org.hisp.dhis.common.QueryFilter; +import org.hisp.dhis.common.QueryOperator; import org.hisp.dhis.common.SortDirection; import org.hisp.dhis.common.UID; import org.hisp.dhis.feedback.ForbiddenException; @@ -59,7 +64,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.springframework.beans.factory.annotation.Autowired; -class OrderEventChangeLogTest extends TrackerTest { +class OrderAndFilterEventChangeLogTest extends TrackerTest { @Autowired private EventChangeLogService eventChangeLogService; @@ -260,6 +265,67 @@ void shouldSortChangeLogsWhenOrderingByPropertyDesc() () -> assertPropertyCreate("geometry", "(-11.419700, 8.103900)", changeLogs.get(4))); } + @Test + void shouldFilterChangeLogsWhenFilteringByUser() throws ForbiddenException, NotFoundException { + EventChangeLogOperationParams params = + EventChangeLogOperationParams.builder() + .filterBy("username", new QueryFilter(QueryOperator.EQ, importUser.getUsername())) + .build(); + + Page changeLogs = + eventChangeLogService.getEventChangeLog(UID.of("QRYjLTiJTrA"), params, defaultPageParams); + + Set changeLogUsers = + changeLogs.getItems().stream() + .map(cl -> cl.getCreatedBy().getUsername()) + .collect(Collectors.toSet()); + assertContainsOnly(List.of(importUser.getUsername()), changeLogUsers); + } + + @Test + void shouldFilterChangeLogsWhenFilteringByDataElement() + throws ForbiddenException, NotFoundException { + Event event = getEvent("kWjSezkXHVp"); + String dataElement = getFirstDataElement(event); + EventChangeLogOperationParams params = + EventChangeLogOperationParams.builder() + .filterBy("dataElement", new QueryFilter(QueryOperator.EQ, dataElement)) + .build(); + + Page changeLogs = + eventChangeLogService.getEventChangeLog(UID.of(event.getUid()), params, defaultPageParams); + + Set changeLogDataElements = + changeLogs.getItems().stream() + .map(cl -> cl.getDataElement().getUid()) + .collect(Collectors.toSet()); + assertContainsOnly(List.of(dataElement), changeLogDataElements); + } + + private Stream provideEventProperties() { + return Stream.of( + Arguments.of("occurredAt"), Arguments.of("scheduledAt"), Arguments.of("geometry")); + } + + @ParameterizedTest + @MethodSource("provideEventProperties") + void shouldFilterChangeLogsWhenFilteringByProperties(String filterValue) + throws ForbiddenException, NotFoundException { + EventChangeLogOperationParams params = + EventChangeLogOperationParams.builder() + .filterBy("property", new QueryFilter(QueryOperator.EQ, filterValue)) + .build(); + + Page changeLogs = + eventChangeLogService.getEventChangeLog(UID.of("QRYjLTiJTrA"), params, defaultPageParams); + + Set changeLogOccurredAtProperties = + changeLogs.getItems().stream() + .map(EventChangeLog::getEventProperty) + .collect(Collectors.toSet()); + assertContainsOnly(List.of(filterValue), changeLogOccurredAtProperties); + } + private void updateDataValue(String event, String dataElementUid, String newValue) { trackerObjects.getEvents().stream() .filter(e -> e.getEvent().getValue().equalsIgnoreCase(event)) diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyControllerTest.java index b061b5aed2ee..0621b0724844 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyControllerTest.java @@ -44,7 +44,7 @@ import org.springframework.transaction.annotation.Transactional; /** - * Tests the generic operations offered by the {@link AbstractFullReadOnlyController} using specific + * Tests the generic operations offered by the {@code AbstractFullReadOnlyController} using specific * endpoints. * * @author David Mackessy diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/CrudControllerIntegrationTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/CrudControllerIntegrationTest.java index d4677f098c61..00ddbca01603 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/CrudControllerIntegrationTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/CrudControllerIntegrationTest.java @@ -37,6 +37,7 @@ import org.hisp.dhis.dataset.DataSet; import org.hisp.dhis.http.HttpStatus; import org.hisp.dhis.jsontree.JsonArray; +import org.hisp.dhis.jsontree.JsonMixed; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.setting.SystemSettingsService; import org.hisp.dhis.test.webapi.PostgresControllerIntegrationTestBase; @@ -197,6 +198,30 @@ void testSearchTokenWithFallback() throws Exception { .isEmpty()); } + @Test + @DisplayName("Should not apply token filter for UID if value has length < 4") + void testIdentifiableTokenFilterLength() { + assertStatus( + HttpStatus.CREATED, + POST( + "/organisationUnits/", + "{'name':'My Unit 1', 'shortName':'OU1', 'openingDate': '2020-01-01'}")); + String ou2 = + assertStatus( + HttpStatus.CREATED, + POST( + "/organisationUnits/", + "{'name':'My Unit 2', 'shortName':'OU2', 'openingDate': '2020-01-01'}")); + + JsonMixed response = + GET("/organisationUnits?filter=identifiable:token:" + ou2.substring(0, 3)).content(); + assertEquals(0, response.getArray("organisationUnits").size()); + + response = GET("/organisationUnits?filter=identifiable:token:" + ou2.substring(0, 4)).content(); + assertEquals(1, response.getArray("organisationUnits").size()); + assertEquals(ou2, response.getArray("organisationUnits").getObject(0).getString("id").string()); + } + private void setUpTranslation() { injectSecurityContextUser(getAdminUser()); diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/OrganisationUnitControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/OrganisationUnitControllerTest.java index 81e601a53f36..84bf552d609d 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/OrganisationUnitControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/OrganisationUnitControllerTest.java @@ -190,6 +190,11 @@ void testGetParents() { GET("/organisationUnits/{id}/parents?filter=displayName:ilike:L0", ou21).content(), "L0"); } + @Test + void testGetParents_Root() { + assertListOfOrganisationUnits(GET("/organisationUnits/{id}/parents", ou0).content()); + } + @Test void testGetQuery() { assertListOfOrganisationUnits(GET("/organisationUnits?query=L21").content(), "L21"); diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/IdSchemeExportControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/IdSchemeExportControllerTest.java index 16f9076a330e..fc17683ff3bc 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/IdSchemeExportControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/IdSchemeExportControllerTest.java @@ -29,6 +29,7 @@ import static org.hisp.dhis.test.utils.Assertions.assertContains; import static org.hisp.dhis.test.utils.Assertions.assertContainsOnly; +import static org.hisp.dhis.test.utils.Assertions.assertIsEmpty; import static org.hisp.dhis.test.utils.Assertions.assertNotEmpty; import static org.hisp.dhis.test.webapi.Assertions.assertWebMessage; import static org.junit.jupiter.api.Assertions.assertAll; @@ -49,6 +50,7 @@ import org.hisp.dhis.attribute.Attribute; import org.hisp.dhis.common.IdentifiableObject; import org.hisp.dhis.common.IdentifiableObjectManager; +import org.hisp.dhis.common.collection.CollectionUtils; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundle; import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundleMode; @@ -56,8 +58,10 @@ import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundleService; import org.hisp.dhis.dxf2.metadata.objectbundle.ObjectBundleValidationService; import org.hisp.dhis.dxf2.metadata.objectbundle.feedback.ObjectBundleValidationReport; +import org.hisp.dhis.eventdatavalue.EventDataValue; import org.hisp.dhis.http.HttpStatus; import org.hisp.dhis.importexport.ImportStrategy; +import org.hisp.dhis.jsontree.JsonList; import org.hisp.dhis.jsontree.JsonObject; import org.hisp.dhis.program.Event; import org.hisp.dhis.render.RenderFormat; @@ -75,6 +79,7 @@ import org.hisp.dhis.webapi.controller.tracker.JsonEvent; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.params.ParameterizedTest; @@ -149,6 +154,7 @@ void setUpUser() { @MethodSource(value = "shouldExportMetadataUsingGivenIdSchemeProvider") void shouldExportMetadataUsingGivenIdScheme(TrackerIdSchemeParam idSchemeParam) { Event event = get(Event.class, "QRYjLTiJTrA"); + assertNotEmpty(event.getEventDataValues(), "test expects an event with data values"); // maps JSON fields to idScheme request parameters Map idSchemeRequestParams = @@ -266,6 +272,48 @@ void shouldExportMetadataUsingGivenIdScheme(TrackerIdSchemeParam idSchemeParam) assertMetadataIdScheme(metadata, actual, idSchemeParam, "event"); } + @Test + void shouldExportEventUsingNonUIDDataElementIdSchemeEvenIfItHasNoDataValues() { + Event event = get(Event.class, "jxgFyJEMUPf"); + assertIsEmpty(event.getEventDataValues(), "test expects an event with no data values"); + + JsonEvent actual = + GET("/tracker/events/{id}?fields=event,dataValues&dataElementIdScheme=NAME", event.getUid()) + .content(HttpStatus.OK) + .as(JsonEvent.class); + + assertEquals("jxgFyJEMUPf", actual.getEvent()); + } + + @Test + void shouldExportEventsUsingNonUIDDataElementIdScheme() { + Event event1 = get(Event.class, "QRYjLTiJTrA"); + Event event2 = get(Event.class, "kWjSezkXHVp"); + assertNotEmpty( + CollectionUtils.intersection( + event1.getEventDataValues().stream() + .map(EventDataValue::getDataElement) + .collect(Collectors.toSet()), + event2.getEventDataValues().stream() + .map(EventDataValue::getDataElement) + .collect(Collectors.toSet())), + "test expects both events to have at least one data value for the same data element"); + + JsonList jsonEvents = + GET("/tracker/events?events=QRYjLTiJTrA,kWjSezkXHVp&fields=event,dataValues&dataElementIdScheme=NAME") + .content(HttpStatus.OK) + .getList("events", JsonEvent.class); + + Map events = + jsonEvents.stream().collect(Collectors.toMap(JsonEvent::getEvent, Function.identity())); + assertContainsOnly(List.of(event1.getUid(), event2.getUid()), events.keySet()); + + TrackerIdSchemeParam idSchemeParam = TrackerIdSchemeParam.NAME; + assertAll( + () -> assertDataValues(events.get("QRYjLTiJTrA"), event1, idSchemeParam), + () -> assertDataValues(events.get("kWjSezkXHVp"), event2, idSchemeParam)); + } + @ParameterizedTest @ValueSource(strings = {"/{id}?", "?events={id}&paging=true&", "?events={id}&paging=false&"}) void shouldReportMetadataWhichDoesNotHaveAnIdentifierForGivenIdScheme(String urlPortion) { @@ -334,6 +382,35 @@ private static void assertIdScheme( field, idSchemeParam)); } + private void assertDataValues( + JsonEvent actual, Event expected, TrackerIdSchemeParam idSchemeParam) { + String field = "dataValues"; + List expectedDataElement = + expected.getEventDataValues().stream() + .map(dv -> idSchemeParam.getIdentifier(get(DataElement.class, dv.getDataElement()))) + .toList(); + assertNotEmpty( + expectedDataElement, + String.format( + "metadata corresponding to field \"%s\" has no value in test data for" + + " idScheme '%s'", + field, idSchemeParam)); + assertTrue( + actual.has(field), + () -> + String.format( + "field \"%s\" is not in response %s for idScheme '%s'", + field, actual, idSchemeParam)); + List actualDataElement = + actual + .getList(field, JsonObject.class) + .toList(el -> el.getString("dataElement").string("")); + assertContainsOnly( + expectedDataElement, + actualDataElement, + "mismatch in data elements of event " + expected.getUid()); + } + private T get(Class type, String uid) { T t = manager.get(type, uid); assertNotNull( diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/event/EventsExportChangeLogsControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/event/EventsExportChangeLogsControllerTest.java index e78b9e5135d8..6a87881c6a73 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/event/EventsExportChangeLogsControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/tracker/export/event/EventsExportChangeLogsControllerTest.java @@ -223,6 +223,24 @@ void shouldGetEventChangeLogInAscOrder() { "scheduledAt", "2023-01-10 00:00:00.000", eventPropertyChangeLogs)); } + @Test + void shouldGetEventChangeLogsWhenFilteringByProperty() { + JsonList changeLogs = + GET("/tracker/events/{id}/changeLogs?filter=property:eq:occurredAt", event.getUid()) + .content(HttpStatus.OK) + .getList("changeLogs", JsonEventChangeLog.class); + List eventPropertyChangeLogs = + changeLogs.stream() + .filter(log -> log.getChange().getEventProperty().getProperty() != null) + .toList(); + + assertAll( + () -> assertHasSize(1, eventPropertyChangeLogs), + () -> + assertPropertyCreateExists( + "occurredAt", "2023-01-10 00:00:00.000", eventPropertyChangeLogs)); + } + @Test void shouldGetChangeLogPagerWithNextElementWhenMultipleElementsImportedAndFirstPageRequested() { JsonPage changeLogs = diff --git a/dhis-2/dhis-web-api/pom.xml b/dhis-2/dhis-web-api/pom.xml index d2544d095b72..508037ba2f08 100644 --- a/dhis-2/dhis-web-api/pom.xml +++ b/dhis-2/dhis-web-api/pom.xml @@ -291,10 +291,6 @@ org.locationtech.jts jts-core - - org.cache2k - cache2k-api - com.lowagie itext diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractCrudController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractCrudController.java index 6729114fd393..307f00565f57 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractCrudController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractCrudController.java @@ -83,6 +83,7 @@ import org.hisp.dhis.jsonpatch.BulkPatchParameters; import org.hisp.dhis.jsonpatch.JsonPatchManager; import org.hisp.dhis.jsonpatch.validator.BulkPatchValidatorFactory; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.render.RenderService; import org.hisp.dhis.schema.MetadataMergeService; import org.hisp.dhis.schema.validation.SchemaValidator; @@ -117,8 +118,9 @@ @Stable @ApiVersion({DhisApiVersion.DEFAULT, DhisApiVersion.ALL}) @OpenApi.Document(group = OpenApi.Document.GROUP_MANAGE) -public abstract class AbstractCrudController - extends AbstractFullReadOnlyController { +public abstract class AbstractCrudController< + T extends IdentifiableObject, P extends GetObjectListParams> + extends AbstractFullReadOnlyController { @Autowired protected SchemaValidator schemaValidator; @Autowired protected RenderService renderService; @@ -178,9 +180,7 @@ public WebMessage patchObject( IOException, JsonPatchException, ConflictException { - WebOptions options = new WebOptions(rpParameters); - - final T persistedObject = getEntity(pvUid, options); + final T persistedObject = getEntity(pvUid); if (!aclService.canUpdate(currentUser, persistedObject)) { throw new ForbiddenException("You don't have the proper permissions to update this object."); @@ -454,13 +454,10 @@ public WebMessage putJsonObject( @ResponseBody public WebMessage replaceTranslations( @OpenApi.Param(UID.class) @PathVariable("uid") String pvUid, - @RequestParam Map rpParameters, @CurrentUser UserDetails currentUser, HttpServletRequest request) throws NotFoundException, ForbiddenException, IOException { - WebOptions options = new WebOptions(rpParameters); - - BaseIdentifiableObject persistedObject = (BaseIdentifiableObject) getEntity(pvUid, options); + BaseIdentifiableObject persistedObject = (BaseIdentifiableObject) getEntity(pvUid); if (!aclService.canUpdate(currentUser, persistedObject)) { throw new ForbiddenException("You don't have the proper permissions to update this object."); diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyController.java index 670851025140..2034dcf490dd 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyController.java @@ -27,7 +27,6 @@ */ package org.hisp.dhis.webapi.controller; -import static java.util.stream.Collectors.toList; import static org.springframework.http.CacheControl.noCache; import com.fasterxml.jackson.databind.SequenceWriter; @@ -37,17 +36,16 @@ import com.fasterxml.jackson.dataformat.csv.CsvSchema.Builder; import com.fasterxml.jackson.dataformat.csv.CsvWriteException; import com.google.common.base.Joiner; -import com.google.common.collect.Lists; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.StringWriter; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import javax.annotation.CheckForNull; @@ -64,20 +62,19 @@ import org.hisp.dhis.common.Pager; import org.hisp.dhis.common.PrimaryKeyObject; import org.hisp.dhis.common.UID; -import org.hisp.dhis.dxf2.common.OrderParams; import org.hisp.dhis.feedback.BadRequestException; import org.hisp.dhis.feedback.ConflictException; import org.hisp.dhis.feedback.ForbiddenException; import org.hisp.dhis.feedback.NotFoundException; -import org.hisp.dhis.fieldfilter.Defaults; import org.hisp.dhis.fieldfilter.FieldFilterService; import org.hisp.dhis.fieldfiltering.FieldFilterParams; -import org.hisp.dhis.fieldfiltering.FieldPreset; -import org.hisp.dhis.query.Order; -import org.hisp.dhis.query.Pagination; +import org.hisp.dhis.query.Criterion; +import org.hisp.dhis.query.GetObjectListParams; +import org.hisp.dhis.query.GetObjectParams; import org.hisp.dhis.query.Query; import org.hisp.dhis.query.QueryParserException; import org.hisp.dhis.query.QueryService; +import org.hisp.dhis.query.Restrictions; import org.hisp.dhis.schema.Property; import org.hisp.dhis.schema.PropertyType; import org.hisp.dhis.schema.Schema; @@ -90,10 +87,7 @@ import org.hisp.dhis.webapi.service.ContextService; import org.hisp.dhis.webapi.service.LinkService; import org.hisp.dhis.webapi.utils.ContextUtils; -import org.hisp.dhis.webapi.utils.PaginationUtils; import org.hisp.dhis.webapi.webdomain.StreamingJsonRoot; -import org.hisp.dhis.webapi.webdomain.WebMetadata; -import org.hisp.dhis.webapi.webdomain.WebOptions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -111,11 +105,9 @@ @Maturity.Stable @ApiVersion({DhisApiVersion.DEFAULT, DhisApiVersion.ALL}) @OpenApi.Document(group = OpenApi.Document.GROUP_QUERY) -public abstract class AbstractFullReadOnlyController +public abstract class AbstractFullReadOnlyController< + T extends IdentifiableObject, P extends GetObjectListParams> extends AbstractGistReadOnlyController { - protected static final String DEFAULTS = "INCLUDE"; - - protected static final WebOptions NO_WEB_OPTIONS = new WebOptions(new HashMap<>()); @Autowired protected IdentifiableObjectManager manager; @@ -145,21 +137,22 @@ public abstract class AbstractFullReadOnlyController entityList, WebOptions options, Map parameters) {} + protected void postProcessResponseEntities(List entityList, P params) {} /** * Override to process a single entity after it has been retrieved from storage and before it is * returned to the view. Entity is null-safe. */ - protected void postProcessResponseEntity( - T entity, WebOptions options, Map parameters) {} + protected void postProcessResponseEntity(T entity, GetObjectParams params) {} /** - * Allows to append new filters to the incoming ones. Recommended only on very specific cases - * where forcing a new filter, programmatically, make sense. + * Allows to append further filters to the incoming ones. Recommended only on very specific cases + * where forcing a new filter, programmatically, make sense. This is usually used to ensure that + * some filters are always present. */ - protected void forceFiltering(final WebOptions webOptions, final List filters) {} + protected void addProgrammaticModifiers(P params) {} + + protected void addProgrammaticFilters(Consumer add) {} // -------------------------------------------------------------------------- // GET Full @@ -174,88 +167,93 @@ protected static class ObjectListResponse { List entries; } - @OpenApi.Param(name = "fields", value = String[].class) - @OpenApi.Param(name = "filter", value = String[].class) - @OpenApi.Params(WebOptions.class) @OpenApi.Response(ObjectListResponse.class) @GetMapping public @ResponseBody ResponseEntity> getObjectList( - @RequestParam Map rpParameters, - OrderParams orderParams, + P params, HttpServletResponse response, @CurrentUser UserDetails currentUser) + throws ForbiddenException, BadRequestException, ConflictException { + return getObjectListInternal(params, response, currentUser, getAdditionalFilters(params)); + } + + protected final ResponseEntity> getObjectListWith( + P params, HttpServletResponse response, - @CurrentUser UserDetails currentUser) - throws ForbiddenException, BadRequestException { - return getObjectList( - rpParameters, orderParams, response, currentUser, !rpParameters.containsKey("query"), null); + UserDetails currentUser, + List additionalFilters) + throws ForbiddenException, BadRequestException, ConflictException { + List filters = getAdditionalFilters(params); + filters.addAll(additionalFilters); + return getObjectListInternal(params, response, currentUser, filters); } - protected final ResponseEntity> getObjectList( - @RequestParam Map rpParameters, - OrderParams orderParams, + protected final ResponseEntity> getObjectListInternal( + P params, HttpServletResponse response, - UserDetails userDetails, - boolean countTotal, - @CheckForNull List objects) + UserDetails currentUser, + List additionalFilters) throws ForbiddenException, BadRequestException { - List orders = orderParams.getOrders(getSchema()); - List fields = Lists.newArrayList(contextService.getParameterValues("fields")); - List filters = Lists.newArrayList(contextService.getParameterValues("filter")); - - if (fields.isEmpty()) { - fields.addAll(FieldPreset.defaultPreset().getFields()); - } - - WebOptions options = new WebOptions(rpParameters); - WebMetadata metadata = new WebMetadata(); - if (!aclService.canRead(userDetails, getEntityClass())) { + if (!aclService.canRead(currentUser, getEntityClass())) { throw new ForbiddenException( "You don't have the proper permissions to read objects of this type."); } - forceFiltering(options, filters); - - List entities = getEntityList(metadata, options, filters, orders, objects); - - Pager pager = metadata.getPager(); - - if (options.hasPaging() && pager == null) { - long totalCount; + addProgrammaticModifiers(params); - if (!countTotal) { - totalCount = entities.size(); + boolean isAlwaysEmpty = additionalFilters.stream().anyMatch(Criterion::isAlwaysFalse); + List entities = isAlwaysEmpty ? List.of() : getEntityList(params, additionalFilters); + postProcessResponseEntities(entities, params); - long skip = (long) (options.getPage() - 1) * options.getPageSize(); - entities = entities.stream().skip(skip).limit(options.getPageSize()).collect(toList()); - } else { - totalCount = countTotal(options, filters, orders); - } + List fields = params.getFieldsJsonList(); + handleLinksAndAccess(entities, fields, false); - pager = new Pager(options.getPage(), totalCount, options.getPageSize()); + Pager pager = null; + if (params.isPaging()) { + long totalCount = isAlwaysEmpty ? 0 : countGetObjectList(params, additionalFilters); + pager = new Pager(params.getPage(), totalCount, params.getPageSize()); + linkService.generatePagerLinks(pager, getEntityClass()); } - postProcessResponseEntities(entities, options, rpParameters); - - handleLinksAndAccess(entities, fields, false); - linkService.generatePagerLinks(pager, getEntityClass()); - cachePrivate(response); - return ResponseEntity.ok( new StreamingJsonRoot<>( pager, getSchema().getCollectionName(), FieldFilterParams.of(entities, fields), - Defaults.valueOf(options.get("defaults", DEFAULTS)).isExclude())); + params.getDefaults().isExclude())); + } + + /** + * A way to incorporate additional filters to the {@link #getObjectList(GetObjectListParams, + * HttpServletResponse, UserDetails)} endpoint that require running a separate query resulting in + * matching ID list which then is used as filter in the standard query process. + * + * @param params options used + * @return the ID of matches, nor null when no such filter is present/used + */ + @CheckForNull + protected List getPreQueryMatches(P params) throws ConflictException { + return null; // no special filters used + } + + @Nonnull + protected List getAdditionalFilters(P params) throws ConflictException { + List filters = new ArrayList<>(); + if (params.getQuery() != null && !params.getQuery().isEmpty()) + filters.add(Restrictions.query(getSchema(), params.getQuery())); + List matches = getPreQueryMatches(params); + // Note: null = no special filters, empty = no matches for special filters + if (matches != null) filters.add(createIdInFilter(matches)); + return filters; + } + + protected final Criterion createIdInFilter(@Nonnull List matches) { + return Restrictions.in("id", UID.toValueList(matches)); } - @OpenApi.Param(name = "fields", value = String[].class) - @OpenApi.Param(name = "filter", value = String[].class) - @OpenApi.Params(WebOptions.class) @GetMapping(produces = {"text/csv", "application/text"}) public ResponseEntity getObjectListCsv( - @RequestParam Map rpParameters, - OrderParams orderParams, + P params, @CurrentUser UserDetails currentUser, @RequestParam(defaultValue = ",") char separator, @RequestParam(defaultValue = ";") String arraySeparator, @@ -266,16 +264,6 @@ public ResponseEntity getObjectListCsv( ConflictException, ForbiddenException, BadRequestException { - List orders = orderParams.getOrders(getSchema()); - List fields = Lists.newArrayList(contextService.getParameterValues("fields")); - List filters = Lists.newArrayList(contextService.getParameterValues("filter")); - - WebOptions options = new WebOptions(rpParameters); - WebMetadata metadata = new WebMetadata(); - - if (fields.isEmpty() || fields.contains("*") || fields.contains(":all")) { - fields.addAll(FieldPreset.defaultPreset().getFields()); - } // only support metadata if (!getSchema().isMetadata()) { @@ -288,8 +276,8 @@ public ResponseEntity getObjectListCsv( "You don't have the proper permissions to read objects of this type."); } - List entities = getEntityList(metadata, options, filters, orders, null); - + List entities = getEntityList(params, List.of()); + List fields = params.getFieldsCsvList(); try { String csv = applyCsvSteps(fields, entities, separator, arraySeparator, skipHeader); return ResponseEntity.ok(csv); @@ -400,15 +388,11 @@ private static Object getAttributeValue(Object obj, String attrId) { return null; } - @OpenApi.Param(name = "fields", value = String[].class) - @OpenApi.Param(name = "filter", value = String[].class) - @OpenApi.Params(WebOptions.class) @OpenApi.Response(OpenApi.EntityType.class) @GetMapping("/{uid:[a-zA-Z0-9]{11}}") - @SuppressWarnings("unchecked") public @ResponseBody ResponseEntity getObject( @OpenApi.Param(UID.class) @PathVariable("uid") String pvUid, - @RequestParam Map rpParameters, + GetObjectParams params, @CurrentUser UserDetails currentUser, HttpServletRequest request, HttpServletResponse response) @@ -419,48 +403,35 @@ private static Object getAttributeValue(Object obj, String attrId) { "You don't have the proper permissions to read objects of this type."); } - List fields = Lists.newArrayList(contextService.getParameterValues("fields")); - List filters = Lists.newArrayList(contextService.getParameterValues("filter")); - forceFiltering(new WebOptions(rpParameters), filters); - - if (fields.isEmpty()) { - fields.add("*"); - } - cachePrivate(response); - WebOptions options = new WebOptions(rpParameters); - T entity = getEntity(pvUid, options); + T entity = getEntity(pvUid); - Query query = - queryService.getQueryFromUrl( - getEntityClass(), - filters, - new ArrayList<>(), - getPaginationData(options), - options.getRootJunction()); + GetObjectListParams listParams = params.toListParams(); + addProgrammaticFilters(listParams::addFilter); // temporary workaround + Query query = queryService.getQueryFromUrl(getEntityClass(), listParams); query.setCurrentUserDetails(currentUser); query.setObjects(List.of(entity)); - query.setDefaults(Defaults.valueOf(options.get("defaults", DEFAULTS))); + query.setDefaults(params.getDefaults()); + @SuppressWarnings("unchecked") List entities = (List) queryService.query(query); + List fields = params.getFieldsObject(); handleLinksAndAccess(entities, fields, true); - entities.forEach(e -> postProcessResponseEntity(e, options, rpParameters)); + entities.forEach(e -> postProcessResponseEntity(e, params)); return ResponseEntity.ok( new StreamingJsonRoot<>( null, null, FieldFilterParams.of(entities, fields), query.getDefaults().isExclude())); } - @OpenApi.Param(name = "fields", value = String[].class) - @OpenApi.Params(WebOptions.class) @GetMapping("/{uid:[a-zA-Z0-9]{11}}/{property}") public @ResponseBody ResponseEntity getObjectProperty( @OpenApi.Param(UID.class) @PathVariable("uid") String pvUid, @OpenApi.Param(PropertyNames.class) @PathVariable("property") String pvProperty, - @RequestParam Map rpParameters, + @RequestParam(required = false) List fields, @CurrentUser UserDetails currentUser, HttpServletResponse response) throws ForbiddenException, NotFoundException { @@ -470,54 +441,37 @@ private static Object getAttributeValue(Object obj, String attrId) { "You don't have the proper permissions to read objects of this type."); } - List fields = Lists.newArrayList(contextService.getParameterValues("fields")); - - if (fields.isEmpty()) { - fields.add(":all"); + if (fields == null || fields.isEmpty()) { + fields = List.of(":all"); } String fieldFilter = "[" + Joiner.on(',').join(fields) + "]"; cachePrivate(response); - ObjectNode objectNode = - getObjectInternal( - pvUid, - rpParameters, - Lists.newArrayList(), - Lists.newArrayList(pvProperty + fieldFilter), - currentUser); + GetObjectParams params = new GetObjectParams(); + params.addField(pvProperty + fieldFilter); + ObjectNode objectNode = getObjectInternal(pvUid, params, currentUser); return ResponseEntity.ok(objectNode); } @SuppressWarnings("unchecked") - private ObjectNode getObjectInternal( - String uid, - Map parameters, - List filters, - List fields, - UserDetails currentUser) + private ObjectNode getObjectInternal(String uid, GetObjectParams params, UserDetails currentUser) throws NotFoundException { - WebOptions options = new WebOptions(parameters); - T entity = getEntity(uid, options); + T entity = getEntity(uid); - Query query = - queryService.getQueryFromUrl( - getEntityClass(), - filters, - new ArrayList<>(), - getPaginationData(options), - options.getRootJunction()); + Query query = queryService.getQueryFromUrl(getEntityClass(), params.toListParams()); query.setCurrentUserDetails(currentUser); query.setObjects(List.of(entity)); - query.setDefaults(Defaults.valueOf(options.get("defaults", DEFAULTS))); + query.setDefaults(params.getDefaults()); List entities = (List) queryService.query(query); + List fields = params.getFieldsObject(); handleLinksAndAccess(entities, fields, true); - entities.forEach(e -> postProcessResponseEntity(entity, options, parameters)); + entities.forEach(e -> postProcessResponseEntity(entity, params)); FieldFilterParams filterParams = FieldFilterParams.of(entities, fields); List objectNodes = fieldFilterService.toObjectNodes(filterParams); @@ -525,55 +479,39 @@ private ObjectNode getObjectInternal( return objectNodes.isEmpty() ? fieldFilterService.createObjectNode() : objectNodes.get(0); } - @SuppressWarnings("unchecked") - protected List getEntityList( - WebMetadata metadata, - WebOptions options, - List filters, - List orders, - List objects) + private List getEntityList(P params, List additionalFilters) throws BadRequestException { Query query = BadRequestException.on( QueryParserException.class, - () -> - queryService.getQueryFromUrl( - getEntityClass(), - filters, - orders, - getPaginationData(options), - options.getRootJunction())); + () -> queryService.getQueryFromUrl(getEntityClass(), params)); + query.add(additionalFilters); + query.setDefaultOrder(); - query.setDefaults(Defaults.valueOf(options.get("defaults", DEFAULTS))); - query.setObjects(objects); - - // Note: objects being null means no query had been running whereas empty means a query did run - // with no result - if (objects == null && options.getOptions().containsKey("query")) { - return getEntityListPostProcess( - options, - Lists.newArrayList(manager.filter(getEntityClass(), options.getOptions().get("query")))); - } - return getEntityListPostProcess(options, (List) queryService.query(query)); + query.setDefaults(params.getDefaults()); + + modifyGetObjectList(params, query); + + @SuppressWarnings("unchecked") + List res = (List) queryService.query(query); + getEntityListPostProcess(params, res); + return res; } - protected List getEntityListPostProcess(WebOptions options, List entities) { - return entities; + protected void modifyGetObjectList(P params, Query query) { + // by default: nothing special to do } - private long countTotal(WebOptions options, List filters, List orders) + protected void getEntityListPostProcess(P params, List entities) {} + + private long countGetObjectList(P params, List additionalFilters) throws BadRequestException { Query query = BadRequestException.on( QueryParserException.class, - () -> - queryService.getQueryFromUrl( - getEntityClass(), - filters, - orders, - new Pagination(), - options.getRootJunction())); - + () -> queryService.getQueryFromUrl(getEntityClass(), params)); + query.add(additionalFilters); + modifyGetObjectList(params, query); return queryService.count(query); } @@ -607,33 +545,17 @@ private boolean fieldsContains(String match, List fields) { // Reflection helpers // -------------------------------------------------------------------------- - private String entityName; - private String entitySimpleName; - protected final String getEntityName() { - if (entityName == null) { - entityName = getEntityClass().getName(); - } - - return entityName; - } - protected final String getEntitySimpleName() { if (entitySimpleName == null) { entitySimpleName = getEntityClass().getSimpleName(); } - return entitySimpleName; } @Nonnull - protected final T getEntity(String uid) throws NotFoundException { - return getEntity(uid, NO_WEB_OPTIONS); - } - - @Nonnull - protected T getEntity(String uid, WebOptions options) throws NotFoundException { + protected T getEntity(String uid) throws NotFoundException { return getEntity(uid, getEntityClass()) .orElseThrow(() -> new NotFoundException(getEntityClass(), uid)); } @@ -646,12 +568,4 @@ protected final java.util.Optional getEntity( protected final Schema getSchema(Class klass) { return schemaService.getDynamicSchema(klass); } - - // -------------------------------------------------------------------------- - // Helpers - // -------------------------------------------------------------------------- - - protected final Pagination getPaginationData(WebOptions options) { - return PaginationUtils.getPaginationData(options); - } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AggregateDataExchangeController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AggregateDataExchangeController.java index 1fa1db292cb9..f8715c580ecb 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AggregateDataExchangeController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AggregateDataExchangeController.java @@ -40,6 +40,7 @@ import org.hisp.dhis.dxf2.webmessage.WebMessage; import org.hisp.dhis.dxf2.webmessage.WebMessageUtils; import org.hisp.dhis.feedback.ForbiddenException; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.scheduling.JobProgress; import org.hisp.dhis.user.CurrentUser; import org.hisp.dhis.user.UserDetails; @@ -63,7 +64,8 @@ @RequiredArgsConstructor @RequestMapping("/api/aggregateDataExchanges") @ApiVersion({DhisApiVersion.DEFAULT, DhisApiVersion.ALL}) -public class AggregateDataExchangeController extends AbstractCrudController { +public class AggregateDataExchangeController + extends AbstractCrudController { private final AggregateDataExchangeService service; @PostMapping("/exchange") diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AnalyticsTableHookController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AnalyticsTableHookController.java index 590c333adb0f..e9118625c2bc 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AnalyticsTableHookController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AnalyticsTableHookController.java @@ -29,6 +29,7 @@ import org.hisp.dhis.analytics.AnalyticsTableHook; import org.hisp.dhis.common.OpenApi; +import org.hisp.dhis.query.GetObjectListParams; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -38,4 +39,5 @@ @Controller @RequestMapping("/api/analyticsTableHooks") @OpenApi.Document(classifiers = {"team:analytics", "purpose:metadata"}) -public class AnalyticsTableHookController extends AbstractCrudController {} +public class AnalyticsTableHookController + extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AttributeController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AttributeController.java index a4f1c97bb99c..7ff36cae4f46 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AttributeController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AttributeController.java @@ -29,6 +29,7 @@ import org.hisp.dhis.attribute.Attribute; import org.hisp.dhis.common.OpenApi; +import org.hisp.dhis.query.GetObjectListParams; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -38,4 +39,4 @@ @Controller @RequestMapping("/api/attributes") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class AttributeController extends AbstractCrudController {} +public class AttributeController extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ConstantController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ConstantController.java index 1716a6cf8860..a9b998b0de78 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ConstantController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ConstantController.java @@ -29,6 +29,7 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.constant.Constant; +import org.hisp.dhis.query.GetObjectListParams; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -38,4 +39,4 @@ @Controller @RequestMapping("/api/constants") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class ConstantController extends AbstractCrudController {} +public class ConstantController extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DashboardController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DashboardController.java index 1ba5f501fb1e..171ce8005c7f 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DashboardController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DashboardController.java @@ -41,6 +41,7 @@ import org.hisp.dhis.dxf2.metadata.MetadataExportParams; import org.hisp.dhis.dxf2.webmessage.WebMessageException; import org.hisp.dhis.feedback.ConflictException; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.sharing.CascadeSharingParameters; import org.hisp.dhis.sharing.CascadeSharingReport; import org.hisp.dhis.sharing.CascadeSharingService; @@ -62,7 +63,7 @@ @Controller @RequestMapping("/api/dashboards") @OpenApi.Document(classifiers = {"team:analytics", "purpose:metadata"}) -public class DashboardController extends AbstractCrudController { +public class DashboardController extends AbstractCrudController { @Autowired private DashboardService dashboardService; @Autowired private CascadeSharingService cascadeSharingService; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DashboardItemController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DashboardItemController.java index bcc6b804b5d8..05b3f284d2a9 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DashboardItemController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DashboardItemController.java @@ -38,6 +38,7 @@ import org.hisp.dhis.dashboard.DashboardService; import org.hisp.dhis.dxf2.webmessage.WebMessageException; import org.hisp.dhis.feedback.ForbiddenException; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.user.CurrentUser; import org.hisp.dhis.user.UserDetails; import org.springframework.beans.factory.annotation.Autowired; @@ -56,7 +57,8 @@ classifiers = {"team:analytics", "purpose:metadata"}) @Controller @RequestMapping("/api/dashboardItems") -public class DashboardItemController extends AbstractCrudController { +public class DashboardItemController + extends AbstractCrudController { // TODO this controller class is only needed for the pre 2.30 old dashboard // app and should be removed diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataApprovalLevelController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataApprovalLevelController.java index 3d641289e591..6ae3b51202ec 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataApprovalLevelController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataApprovalLevelController.java @@ -30,6 +30,7 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.dataapproval.DataApprovalLevel; import org.hisp.dhis.dataapproval.DataApprovalLevelService; +import org.hisp.dhis.query.GetObjectListParams; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -37,7 +38,8 @@ @Controller @RequestMapping("/api/dataApprovalLevels") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class DataApprovalLevelController extends AbstractCrudController { +public class DataApprovalLevelController + extends AbstractCrudController { @Autowired private DataApprovalLevelService dataApprovalLevelService; @Override diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataApprovalWorkflowController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataApprovalWorkflowController.java index c4091071c426..aae62bf58db4 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataApprovalWorkflowController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataApprovalWorkflowController.java @@ -29,10 +29,12 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.dataapproval.DataApprovalWorkflow; +import org.hisp.dhis.query.GetObjectListParams; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/api/dataApprovalWorkflows") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class DataApprovalWorkflowController extends AbstractCrudController {} +public class DataApprovalWorkflowController + extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataEntryFormController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataEntryFormController.java index c9ccf70ae0be..b26978c3b39f 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataEntryFormController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataEntryFormController.java @@ -29,6 +29,7 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.dataentryform.DataEntryForm; +import org.hisp.dhis.query.GetObjectListParams; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -38,4 +39,5 @@ @Controller @RequestMapping("/api/dataEntryForms") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class DataEntryFormController extends AbstractCrudController {} +public class DataEntryFormController + extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataSetController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataSetController.java index ac2fa2a76bdc..c80843aababd 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataSetController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DataSetController.java @@ -89,6 +89,7 @@ import org.hisp.dhis.period.Period; import org.hisp.dhis.period.PeriodService; import org.hisp.dhis.period.PeriodType; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.query.Query; import org.hisp.dhis.webapi.mvc.annotation.ApiVersion; import org.hisp.dhis.webapi.utils.FormUtils; @@ -116,7 +117,7 @@ @Controller @RequestMapping("/api/dataSets") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class DataSetController extends AbstractCrudController { +public class DataSetController extends AbstractCrudController { public static final String DSD_TRANSFORM = "/templates/metadata2dsd.xsl"; // ------------------------------------------------------------------------- diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DocumentController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DocumentController.java index 5a880b352e3c..6ecf16d9575a 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DocumentController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/DocumentController.java @@ -45,6 +45,7 @@ import org.hisp.dhis.external.location.LocationManager; import org.hisp.dhis.fileresource.FileResource; import org.hisp.dhis.fileresource.FileResourceService; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.utils.ContextUtils; import org.hisp.dhis.webapi.utils.HeaderUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -62,7 +63,7 @@ @Slf4j @RequestMapping("/api/documents") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class DocumentController extends AbstractCrudController { +public class DocumentController extends AbstractCrudController { @Autowired private DocumentService documentService; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EnrollmentQueryAnalyticsController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EnrollmentQueryAnalyticsController.java index f6984bcce96b..1cc9503abf01 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EnrollmentQueryAnalyticsController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EnrollmentQueryAnalyticsController.java @@ -32,8 +32,8 @@ import static org.hisp.dhis.common.RequestTypeAware.EndpointAction.QUERY; import static org.hisp.dhis.common.RequestTypeAware.EndpointItem.ENROLLMENT; import static org.hisp.dhis.common.cache.CacheStrategy.RESPECT_SYSTEM_SETTING; -import static org.hisp.dhis.period.PeriodDataProvider.DataSource.DATABASE; -import static org.hisp.dhis.period.PeriodDataProvider.DataSource.SYSTEM_DEFINED; +import static org.hisp.dhis.period.PeriodDataProvider.PeriodSource.DATABASE; +import static org.hisp.dhis.period.PeriodDataProvider.PeriodSource.SYSTEM_DEFINED; import static org.hisp.dhis.security.Authorities.F_PERFORM_ANALYTICS_EXPLAIN; import static org.hisp.dhis.system.grid.GridUtils.toCsv; import static org.hisp.dhis.system.grid.GridUtils.toHtml; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EventFilterController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EventFilterController.java index 615ccc4fd921..51f0133cd419 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EventFilterController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EventFilterController.java @@ -33,6 +33,7 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.programstagefilter.EventFilter; import org.hisp.dhis.programstagefilter.EventFilterService; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.mvc.annotation.ApiVersion; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -44,7 +45,8 @@ @RequestMapping("/api/eventFilters") @ApiVersion(include = {DhisApiVersion.ALL, DhisApiVersion.DEFAULT}) @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) -public class EventFilterController extends AbstractCrudController { +public class EventFilterController + extends AbstractCrudController { private final EventFilterService eventFilterService; public EventFilterController(EventFilterService eventFilterService) { diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EventHookController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EventHookController.java index 76f2cdddae86..b20dd4a239cd 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EventHookController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/EventHookController.java @@ -31,6 +31,7 @@ import org.hisp.dhis.common.DhisApiVersion; import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.eventhook.EventHook; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.mvc.annotation.ApiVersion; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -43,4 +44,4 @@ @RequestMapping("/api/eventHooks") @ApiVersion({DhisApiVersion.DEFAULT, DhisApiVersion.ALL}) @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) -public class EventHookController extends AbstractCrudController {} +public class EventHookController extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ExpressionDimensionItemController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ExpressionDimensionItemController.java index 1bbc1e2a4abe..c06a0278fc88 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ExpressionDimensionItemController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ExpressionDimensionItemController.java @@ -31,6 +31,7 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.expressiondimensionitem.ExpressionDimensionItem; import org.hisp.dhis.feedback.ConflictException; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.mvc.annotation.ApiVersion; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -41,7 +42,7 @@ @ApiVersion({DhisApiVersion.DEFAULT, DhisApiVersion.ALL}) @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) public class ExpressionDimensionItemController - extends AbstractCrudController { + extends AbstractCrudController { @Override protected void preCreateEntity(ExpressionDimensionItem expressionDimensionItem) throws ConflictException { diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/FileResourceController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/FileResourceController.java index 860ce5e735af..c1112a806e2b 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/FileResourceController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/FileResourceController.java @@ -37,7 +37,6 @@ import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; -import java.util.Map; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hisp.dhis.common.DhisApiVersion; @@ -56,6 +55,8 @@ import org.hisp.dhis.fileresource.FileResourceOwner; import org.hisp.dhis.fileresource.FileResourceService; import org.hisp.dhis.fileresource.ImageFileDimension; +import org.hisp.dhis.query.GetObjectListParams; +import org.hisp.dhis.query.GetObjectParams; import org.hisp.dhis.user.CurrentUser; import org.hisp.dhis.user.User; import org.hisp.dhis.user.UserDetails; @@ -82,7 +83,8 @@ @ApiVersion({DhisApiVersion.DEFAULT, DhisApiVersion.ALL}) @AllArgsConstructor @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class FileResourceController extends AbstractFullReadOnlyController { +public class FileResourceController + extends AbstractFullReadOnlyController { private final FileResourceService fileResourceService; private final FileResourceUtils fileResourceUtils; @@ -95,12 +97,12 @@ public class FileResourceController extends AbstractFullReadOnlyController getObject( @PathVariable String uid, - Map rpParameters, + GetObjectParams params, @CurrentUser UserDetails currentUser, HttpServletRequest request, HttpServletResponse response) throws ForbiddenException, NotFoundException { - return super.getObject(uid, rpParameters, currentUser, request, response); + return super.getObject(uid, params, currentUser, request, response); } @GetMapping(value = "/{uid}") diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/IdentifiableObjectController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/IdentifiableObjectController.java index b6b4bc23c5b6..d40906308d0c 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/IdentifiableObjectController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/IdentifiableObjectController.java @@ -37,9 +37,9 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.dxf2.webmessage.WebMessage; import org.hisp.dhis.feedback.NotFoundException; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.user.CurrentUser; import org.hisp.dhis.user.UserDetails; -import org.hisp.dhis.webapi.webdomain.WebOptions; import org.springframework.stereotype.Controller; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.PathVariable; @@ -51,12 +51,13 @@ @Controller @RequestMapping("/api/identifiableObjects") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class IdentifiableObjectController extends AbstractCrudController { +public class IdentifiableObjectController + extends AbstractCrudController { @Nonnull @Override @SuppressWarnings("unchecked") - public IdentifiableObject getEntity(String uid, WebOptions options) throws NotFoundException { + public IdentifiableObject getEntity(String uid) throws NotFoundException { Optional object = (Optional) manager.find(uid); if (object.isEmpty()) { throw new NotFoundException(format("No identifiable object with id `%s` exists", uid)); diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/InterpretationController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/InterpretationController.java index 2cbe281f86ed..dd8a9fb7c87e 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/InterpretationController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/InterpretationController.java @@ -31,13 +31,9 @@ import static org.hisp.dhis.dxf2.webmessage.WebMessageUtils.created; import static org.hisp.dhis.dxf2.webmessage.WebMessageUtils.notFound; -import com.google.common.collect.Lists; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.util.ArrayList; -import java.util.Collection; import java.util.Iterator; -import java.util.List; import org.hisp.dhis.common.AnalyticalObject; import org.hisp.dhis.common.IdentifiableObjectManager; import org.hisp.dhis.common.OpenApi; @@ -48,29 +44,20 @@ import org.hisp.dhis.eventreport.EventReport; import org.hisp.dhis.eventvisualization.EventVisualization; import org.hisp.dhis.feedback.ForbiddenException; -import org.hisp.dhis.fieldfilter.Defaults; import org.hisp.dhis.interpretation.Interpretation; import org.hisp.dhis.interpretation.InterpretationComment; import org.hisp.dhis.interpretation.InterpretationService; -import org.hisp.dhis.interpretation.MentionUtils; import org.hisp.dhis.mapping.Map; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.period.Period; import org.hisp.dhis.period.PeriodType; -import org.hisp.dhis.query.Disjunction; -import org.hisp.dhis.query.Order; -import org.hisp.dhis.query.Query; -import org.hisp.dhis.query.QueryParserException; -import org.hisp.dhis.query.Restrictions; -import org.hisp.dhis.schema.Schema; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.schema.descriptors.InterpretationSchemaDescriptor; import org.hisp.dhis.user.CurrentUser; import org.hisp.dhis.user.CurrentUserUtil; import org.hisp.dhis.user.User; import org.hisp.dhis.user.UserDetails; import org.hisp.dhis.visualization.Visualization; -import org.hisp.dhis.webapi.webdomain.WebMetadata; -import org.hisp.dhis.webapi.webdomain.WebOptions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; @@ -90,51 +77,12 @@ @Controller @RequestMapping("/api/interpretations") @OpenApi.Document(classifiers = {"team:analytics", "purpose:metadata"}) -public class InterpretationController extends AbstractCrudController { +public class InterpretationController + extends AbstractCrudController { @Autowired private InterpretationService interpretationService; @Autowired private IdentifiableObjectManager idObjectManager; - @Override - @SuppressWarnings("unchecked") - protected List getEntityList( - WebMetadata metadata, - WebOptions options, - List filters, - List orders, - List objects) - throws QueryParserException { - // If custom filter (mentions:in:[username]) in filters -> Remove from - // filters - List mentionsFromCustomFilters = MentionUtils.removeCustomFilters(filters); - - Query query = - queryService.getQueryFromUrl( - getEntityClass(), - filters, - orders, - getPaginationData(options), - options.getRootJunction()); - query.setDefaultOrder(); - query.setDefaults(Defaults.valueOf(options.get("defaults", DEFAULTS))); - // If custom filter (mentions:in:[username]) in filters -> Add as - // disjunction including interpretation mentions and comments mentions - for (Disjunction disjunction : - (Collection) - getDisjunctionsFromCustomMentions(mentionsFromCustomFilters, query.getSchema())) { - query.add(disjunction); - } - - List entityList; - if (objects == null && options.getOptions().containsKey("query")) { - entityList = - Lists.newArrayList(manager.filter(getEntityClass(), options.getOptions().get("query"))); - } else { - entityList = (List) queryService.query(query); - } - return entityList; - } - // ------------------------------------------------------------------------- // Interpretation create // ------------------------------------------------------------------------- @@ -500,22 +448,4 @@ public WebMessage unlike(@PathVariable("uid") String uid) { } return conflict("Could not remove like, user had not previously liked interpretation"); } - - // ------------------------------------------------------------------------- - // Supportive methods - // ------------------------------------------------------------------------- - - private Collection getDisjunctionsFromCustomMentions( - List mentions, Schema schema) { - Collection disjunctions = new ArrayList(); - for (String m : mentions) { - Disjunction disjunction = new Disjunction(schema); - String[] split = m.substring(1, m.length() - 1).split(","); - List items = Lists.newArrayList(split); - disjunction.add(Restrictions.in("mentions.username", items)); - disjunction.add(Restrictions.in("comments.mentions.username", items)); - disjunctions.add(disjunction); - } - return disjunctions; - } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/LegendSetController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/LegendSetController.java index 2bbe7453ffe9..a24d2f6c9ccf 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/LegendSetController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/LegendSetController.java @@ -40,6 +40,7 @@ import org.hisp.dhis.feedback.ForbiddenException; import org.hisp.dhis.feedback.NotFoundException; import org.hisp.dhis.legend.LegendSet; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.security.RequiresAuthority; import org.hisp.dhis.user.CurrentUser; import org.hisp.dhis.user.UserDetails; @@ -54,7 +55,7 @@ @Controller @RequestMapping("/api/legendSets") @OpenApi.Document(classifiers = {"team:analytics", "purpose:metadata"}) -public class LegendSetController extends AbstractCrudController { +public class LegendSetController extends AbstractCrudController { @Override @RequiresAuthority(anyOf = {F_LEGEND_SET_PUBLIC_ADD, F_LEGEND_SET_PRIVATE_ADD}) public WebMessage postJsonObject(HttpServletRequest request) diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MessageConversationController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MessageConversationController.java index 66e78789eace..e4a5ad88d55f 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MessageConversationController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MessageConversationController.java @@ -31,6 +31,7 @@ import static org.hisp.dhis.dxf2.webmessage.WebMessageUtils.created; import static org.hisp.dhis.dxf2.webmessage.WebMessageUtils.notFound; import static org.hisp.dhis.security.Authorities.F_METADATA_IMPORT; +import static org.hisp.dhis.user.CurrentUserUtil.getCurrentUserDetails; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.collect.Lists; @@ -38,18 +39,16 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.List; -import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; +import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; import org.hisp.dhis.common.OpenApi; -import org.hisp.dhis.common.Pager; +import org.hisp.dhis.common.UID; import org.hisp.dhis.configuration.ConfigurationService; import org.hisp.dhis.dxf2.webmessage.WebMessage; import org.hisp.dhis.dxf2.webmessage.WebMessageException; @@ -57,7 +56,6 @@ import org.hisp.dhis.feedback.ConflictException; import org.hisp.dhis.feedback.ForbiddenException; import org.hisp.dhis.feedback.NotFoundException; -import org.hisp.dhis.fieldfilter.Defaults; import org.hisp.dhis.fileresource.FileResource; import org.hisp.dhis.fileresource.FileResourceDomain; import org.hisp.dhis.fileresource.FileResourceService; @@ -72,15 +70,13 @@ import org.hisp.dhis.node.types.SimpleNode; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.organisationunit.OrganisationUnitService; +import org.hisp.dhis.query.GetObjectListParams; +import org.hisp.dhis.query.GetObjectParams; import org.hisp.dhis.query.Junction; -import org.hisp.dhis.query.Order; -import org.hisp.dhis.query.Pagination; import org.hisp.dhis.query.Query; -import org.hisp.dhis.query.QueryParserException; import org.hisp.dhis.schema.descriptors.MessageConversationSchemaDescriptor; import org.hisp.dhis.security.RequiresAuthority; import org.hisp.dhis.user.CurrentUser; -import org.hisp.dhis.user.CurrentUserUtil; import org.hisp.dhis.user.User; import org.hisp.dhis.user.UserDetails; import org.hisp.dhis.user.UserGroup; @@ -88,8 +84,6 @@ import org.hisp.dhis.webapi.utils.ContextUtils; import org.hisp.dhis.webapi.utils.FileResourceUtils; import org.hisp.dhis.webapi.webdomain.MessageConversation; -import org.hisp.dhis.webapi.webdomain.WebMetadata; -import org.hisp.dhis.webapi.webdomain.WebOptions; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -113,7 +107,10 @@ @RequiredArgsConstructor @OpenApi.Document(classifiers = {"team:platform", "purpose:support"}) public class MessageConversationController - extends AbstractCrudController { + extends AbstractCrudController< + org.hisp.dhis.message.MessageConversation, + MessageConversationController.GetMessageConversationObjectListParams> { + private final MessageService messageService; private final OrganisationUnitService organisationUnitService; private final UserGroupService userGroupService; @@ -122,30 +119,29 @@ public class MessageConversationController private final FileResourceService fileResourceService; private final DhisConfigurationProvider dhisConfig; + @Data + @EqualsAndHashCode(callSuper = true) + public static final class GetMessageConversationObjectListParams extends GetObjectListParams { + String queryString; + String queryOperator; + } + @Override protected void postProcessResponseEntity( - org.hisp.dhis.message.MessageConversation entity, - WebOptions options, - Map parameters) { + org.hisp.dhis.message.MessageConversation entity, GetObjectParams params) { - User currentUser = userService.getUserByUsername(CurrentUserUtil.getCurrentUsername()); - if (!messageService.hasAccessToManageFeedbackMessages(UserDetails.fromUser(currentUser))) { + if (!messageService.hasAccessToManageFeedbackMessages(getCurrentUserDetails())) { entity.setMessages( entity.getMessages().stream().filter(message -> !message.isInternal()).toList()); } - - boolean markRead = Boolean.parseBoolean(parameters.get("markRead")); - - if (markRead) { - entity.markRead(currentUser); - manager.update(entity); - } } @Override + @OpenApi.Param(name = "markRead", value = boolean.class) + @GetMapping("/{uid:[a-zA-Z0-9]{11}}") public ResponseEntity getObject( @PathVariable String uid, - Map rpParameters, + GetObjectParams params, @CurrentUser UserDetails currentUser, HttpServletRequest request, HttpServletResponse response) @@ -167,80 +163,40 @@ public ResponseEntity getObject( throw new ForbiddenException("Not authorized to access this conversation."); } - return super.getObject(uid, rpParameters, currentUser, request, response); - } - - @Override - @SuppressWarnings("unchecked") - protected List getEntityList( - WebMetadata metadata, - WebOptions options, - List filters, - List orders, - List objects) - throws QueryParserException { - List messageConversations; - - if (objects == null && options.getOptions().containsKey("query")) { - messageConversations = - Lists.newArrayList(manager.filter(getEntityClass(), options.getOptions().get("query"))); - } else { - messageConversations = new ArrayList<>(messageService.getMessageConversations()); - } - - Query query = - queryService.getQueryFromUrl( - getEntityClass(), filters, orders, new Pagination(), options.getRootJunction()); - query.setDefaultOrder(); - query.setDefaults(Defaults.valueOf(options.get("defaults", DEFAULTS))); - query.setObjects(messageConversations); - - messageConversations = - (List) queryService.query(query); - - if (options.get("queryString") != null) { - String queryOperator = "token"; - if (options.get("queryOperator") != null) { - queryOperator = options.get("queryOperator"); - } + boolean markRead = "true".equals(request.getParameter("markRead")); - List queryFilter = - Arrays.asList( - "subject:" + queryOperator + ":" + options.get("queryString"), - "messages.text:" + queryOperator + ":" + options.get("queryString"), - "messages.sender.displayName:" + queryOperator + ":" + options.get("queryString")); - Query subQuery = - queryService.getQueryFromUrl( - getEntityClass(), - queryFilter, - Collections.emptyList(), - new Pagination(), - Junction.Type.OR); - subQuery.setObjects(messageConversations); - messageConversations = - (List) queryService.query(subQuery); + if (markRead) { + messageConversation.markRead(UID.of(currentUser.getUid())); + manager.update(messageConversation); } - int count = messageConversations.size(); - - Query paginatedQuery = - queryService.getQueryFromUrl( - getEntityClass(), - Collections.emptyList(), - Collections.emptyList(), - getPaginationData(options), - options.getRootJunction()); - paginatedQuery.setObjects(messageConversations); - - messageConversations = - (List) queryService.query(paginatedQuery); - - if (options.hasPaging()) { - Pager pager = new Pager(options.getPage(), count, options.getPageSize()); - metadata.setPager(pager); - } + return super.getObject(uid, params, currentUser, request, response); + } - return messageConversations; + @Override + protected List getPreQueryMatches(GetMessageConversationObjectListParams params) { + String query = params.getQueryString(); + if (query == null) return null; + + String op = params.getQueryOperator(); + if (op == null) op = "token"; + + List filters = + List.of( + "subject:" + op + ":" + query, + "messages.text:" + op + ":" + query, + "messages.sender.displayName:" + op + ":" + query); + + GetObjectListParams subQueryParams = + new GetObjectListParams() + .setPaging(false) + .setRootJunction(Junction.Type.OR) + .setFilters(filters); + Query subQuery = queryService.getQueryFromUrl(getEntityClass(), subQueryParams); + // Note: in theory these filters could be added to the main query + // but the OR concerns both DB and in-memory properties + // which would break if added to the main query ATM + return queryService.query(subQuery).stream().map(UID::of).toList(); } // -------------------------------------------------------------------------- @@ -1061,7 +1017,10 @@ private RootNode modifyMessageConversationRead( for (org.hisp.dhis.message.MessageConversation conversation : messageConversations) { - boolean success = (readValue ? conversation.markRead(user) : conversation.markUnread(user)); + boolean success = + (readValue + ? conversation.markRead(UID.of(user.getUid())) + : conversation.markUnread(user)); if (success) { messageService.updateMessageConversation(conversation); marked.addChild(new SimpleNode("uid", conversation.getUid())); diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PredictorController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PredictorController.java index 82b58f462564..095692195e9a 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PredictorController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PredictorController.java @@ -47,6 +47,7 @@ import org.hisp.dhis.predictor.PredictionSummary; import org.hisp.dhis.predictor.Predictor; import org.hisp.dhis.predictor.PredictorService; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.security.RequiresAuthority; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; @@ -66,7 +67,7 @@ @Slf4j @RequestMapping("/api/predictors") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class PredictorController extends AbstractCrudController { +public class PredictorController extends AbstractCrudController { @Autowired private PredictorService predictorService; @Autowired private PredictionService predictionService; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PredictorGroupController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PredictorGroupController.java index 2a3a7e1cd23d..10d2739b2a3d 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PredictorGroupController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PredictorGroupController.java @@ -39,6 +39,7 @@ import org.hisp.dhis.predictor.PredictionService; import org.hisp.dhis.predictor.PredictionSummary; import org.hisp.dhis.predictor.PredictorGroup; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.scheduling.JobProgress; import org.hisp.dhis.security.RequiresAuthority; import org.springframework.stereotype.Controller; @@ -55,7 +56,8 @@ @RequestMapping("/api/predictorGroups") @RequiredArgsConstructor @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class PredictorGroupController extends AbstractCrudController { +public class PredictorGroupController + extends AbstractCrudController { private final PredictionService predictionService; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ProgramStageWorkingListController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ProgramStageWorkingListController.java index 1128a591dbd6..6734fdfba2ee 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ProgramStageWorkingListController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ProgramStageWorkingListController.java @@ -30,6 +30,7 @@ import org.hisp.dhis.common.DhisApiVersion; import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.programstageworkinglist.ProgramStageWorkingList; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.mvc.annotation.ApiVersion; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -39,4 +40,4 @@ @ApiVersion(include = {DhisApiVersion.ALL, DhisApiVersion.DEFAULT}) @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) public class ProgramStageWorkingListController - extends AbstractCrudController {} + extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PushAnalysisController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PushAnalysisController.java index 180ef58cc42f..725f4a462f4b 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PushAnalysisController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/PushAnalysisController.java @@ -41,6 +41,7 @@ import org.hisp.dhis.feedback.NotFoundException; import org.hisp.dhis.pushanalysis.PushAnalysis; import org.hisp.dhis.pushanalysis.PushAnalysisService; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.scheduling.JobConfiguration; import org.hisp.dhis.scheduling.JobConfigurationService; import org.hisp.dhis.scheduling.JobSchedulerService; @@ -67,7 +68,8 @@ @ApiVersion({DhisApiVersion.DEFAULT, DhisApiVersion.ALL}) @RequiredArgsConstructor @OpenApi.Document(classifiers = {"team:platform", "purpose:support"}) -public class PushAnalysisController extends AbstractCrudController { +public class PushAnalysisController + extends AbstractCrudController { private final PushAnalysisService pushAnalysisService; private final ContextUtils contextUtils; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ReportController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ReportController.java index 96a8afe769ed..b44644735a43 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ReportController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/ReportController.java @@ -46,6 +46,7 @@ import org.hisp.dhis.period.MonthlyPeriodType; import org.hisp.dhis.period.Period; import org.hisp.dhis.period.PeriodType; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.report.Report; import org.hisp.dhis.report.ReportService; import org.hisp.dhis.report.ReportType; @@ -72,7 +73,7 @@ @Controller @RequestMapping("/api/reports") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class ReportController extends AbstractCrudController { +public class ReportController extends AbstractCrudController { @Autowired public ReportService reportService; @Autowired private OrganisationUnitService organisationUnitService; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/RouteController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/RouteController.java index a02412bf474d..fdda5c0d2d13 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/RouteController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/RouteController.java @@ -39,6 +39,7 @@ import org.hisp.dhis.feedback.ConflictException; import org.hisp.dhis.feedback.ForbiddenException; import org.hisp.dhis.feedback.NotFoundException; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.route.Route; import org.hisp.dhis.route.RouteService; import org.hisp.dhis.schema.descriptors.RouteSchemaDescriptor; @@ -62,7 +63,7 @@ @RequestMapping("/api/routes") @ApiVersion({DhisApiVersion.DEFAULT, DhisApiVersion.ALL}) @OpenApi.Document(classifiers = {"team:extensibility", "purpose:metadata"}) -public class RouteController extends AbstractCrudController { +public class RouteController extends AbstractCrudController { private final RouteService routeService; @RequestMapping( diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/SectionController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/SectionController.java index 211f5a6b8b96..2877e3058f3b 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/SectionController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/SectionController.java @@ -29,6 +29,7 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.dataset.Section; +import org.hisp.dhis.query.GetObjectListParams; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -38,4 +39,4 @@ @Controller @RequestMapping("/api/sections") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class SectionController extends AbstractCrudController
{} +public class SectionController extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/SqlViewController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/SqlViewController.java index 119fe8053fe2..418c48c1e1d9 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/SqlViewController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/SqlViewController.java @@ -46,6 +46,7 @@ import org.hisp.dhis.external.conf.DhisConfigurationProvider; import org.hisp.dhis.feedback.ConflictException; import org.hisp.dhis.feedback.NotFoundException; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.scheduling.JobConfiguration; import org.hisp.dhis.scheduling.JobConfigurationService; import org.hisp.dhis.scheduling.JobType; @@ -73,7 +74,7 @@ @RequestMapping("/api/sqlViews") @RequiredArgsConstructor @OpenApi.Document(classifiers = {"team:platform", "purpose:support"}) -public class SqlViewController extends AbstractCrudController { +public class SqlViewController extends AbstractCrudController { private final SqlViewService sqlViewService; private final JobConfigurationService jobConfigurationService; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/TrackedEntityFilterController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/TrackedEntityFilterController.java index 9d26fbeb5184..b8760b71e09c 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/TrackedEntityFilterController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/TrackedEntityFilterController.java @@ -31,6 +31,7 @@ import org.hisp.dhis.common.DhisApiVersion; import org.hisp.dhis.common.IllegalQueryException; import org.hisp.dhis.common.OpenApi; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.trackedentityfilter.TrackedEntityFilter; import org.hisp.dhis.trackedentityfilter.TrackedEntityFilterService; import org.hisp.dhis.webapi.mvc.annotation.ApiVersion; @@ -44,7 +45,8 @@ @RequestMapping("/api/trackedEntityInstanceFilters") @ApiVersion(include = {DhisApiVersion.ALL, DhisApiVersion.DEFAULT}) @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) -public class TrackedEntityFilterController extends AbstractCrudController { +public class TrackedEntityFilterController + extends AbstractCrudController { private final TrackedEntityFilterService teiFilterService; public TrackedEntityFilterController(TrackedEntityFilterService teiFilterService) { diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/VisualizationController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/VisualizationController.java index a163845380a3..18592c1d6fe7 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/VisualizationController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/VisualizationController.java @@ -37,7 +37,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Set; import org.apache.commons.collections4.CollectionUtils; import org.hisp.dhis.common.BaseDimensionalItemObject; @@ -53,17 +52,19 @@ import org.hisp.dhis.legend.LegendSetService; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.period.Period; +import org.hisp.dhis.query.GetObjectListParams; +import org.hisp.dhis.query.GetObjectParams; import org.hisp.dhis.user.CurrentUserUtil; import org.hisp.dhis.user.User; import org.hisp.dhis.visualization.Visualization; -import org.hisp.dhis.webapi.webdomain.WebOptions; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/api/visualizations") @OpenApi.Document(classifiers = {"team:analytics", "purpose:metadata"}) -public class VisualizationController extends AbstractCrudController { +public class VisualizationController + extends AbstractCrudController { private final LegendSetService legendSetService; private final DimensionService dimensionService; @@ -141,7 +142,7 @@ private void maybeLoadLegendSetInto(Visualization visualization) { @Override public void postProcessResponseEntities( - List entityList, WebOptions options, Map parameters) { + List entityList, GetObjectListParams params) { if (CollectionUtils.isEmpty(entityList)) return; User currentUser = userService.getUserByUsername(CurrentUserUtil.getCurrentUsername()); @@ -169,8 +170,7 @@ public void postProcessResponseEntities( } @Override - public void postProcessResponseEntity( - Visualization visualization, WebOptions options, Map parameters) { + public void postProcessResponseEntity(Visualization visualization, GetObjectParams params) { if (visualization != null) { visualization.populateAnalyticalProperties(); diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryComboController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryComboController.java index 8d811020490d..d10ebbd19881 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryComboController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryComboController.java @@ -38,6 +38,7 @@ import org.hisp.dhis.feedback.ConflictException; import org.hisp.dhis.feedback.ErrorCode; import org.hisp.dhis.feedback.NotFoundException; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; @@ -53,7 +54,8 @@ @Controller @RequestMapping("/api/categoryCombos") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class CategoryComboController extends AbstractCrudController { +public class CategoryComboController + extends AbstractCrudController { @Autowired private CategoryService categoryService; @Autowired private DataValueService dataValueService; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryController.java index d1cfec59d1ea..7fb6dc1595bd 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryController.java @@ -29,6 +29,7 @@ import org.hisp.dhis.category.Category; import org.hisp.dhis.common.OpenApi; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -39,4 +40,4 @@ @Controller @RequestMapping("/api/categories") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class CategoryController extends AbstractCrudController {} +public class CategoryController extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionComboController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionComboController.java index ddfab84e9a08..e243d3a836a0 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionComboController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionComboController.java @@ -29,6 +29,7 @@ import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.common.OpenApi; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -39,4 +40,5 @@ @Controller @RequestMapping("/api/categoryOptionCombos") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class CategoryOptionComboController extends AbstractCrudController {} +public class CategoryOptionComboController + extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionController.java index cbe2b8fa9b36..f828daa7bc37 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionController.java @@ -50,6 +50,7 @@ import org.hisp.dhis.feedback.MergeReport; import org.hisp.dhis.merge.MergeParams; import org.hisp.dhis.merge.MergeService; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.security.RequiresAuthority; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.http.HttpStatus; @@ -70,7 +71,8 @@ @RequestMapping("/api/categoryOptions") @RequiredArgsConstructor @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class CategoryOptionController extends AbstractCrudController { +public class CategoryOptionController + extends AbstractCrudController { private final CategoryService categoryService; private final MergeService categoryOptionMergeService; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionGroupController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionGroupController.java index f1b548320189..e96f98af42f8 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionGroupController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionGroupController.java @@ -29,6 +29,7 @@ import org.hisp.dhis.category.CategoryOptionGroup; import org.hisp.dhis.common.OpenApi; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -39,4 +40,5 @@ @Controller @RequestMapping("/api/categoryOptionGroups") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class CategoryOptionGroupController extends AbstractCrudController {} +public class CategoryOptionGroupController + extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionGroupSetController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionGroupSetController.java index 9c6ff8e3a40b..6ca10099199c 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionGroupSetController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/category/CategoryOptionGroupSetController.java @@ -29,6 +29,7 @@ import org.hisp.dhis.category.CategoryOptionGroupSet; import org.hisp.dhis.common.OpenApi; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -40,4 +41,4 @@ @RequestMapping("/api/categoryOptionGroupSets") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) public class CategoryOptionGroupSetController - extends AbstractCrudController {} + extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dataelement/DataElementController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dataelement/DataElementController.java index bc6b4422970d..4e161536dc8e 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dataelement/DataElementController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dataelement/DataElementController.java @@ -43,6 +43,7 @@ import org.hisp.dhis.feedback.MergeReport; import org.hisp.dhis.merge.MergeParams; import org.hisp.dhis.merge.MergeService; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.security.RequiresAuthority; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.http.HttpStatus; @@ -61,7 +62,8 @@ @RequestMapping("/api/dataElements") @RequiredArgsConstructor @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class DataElementController extends AbstractCrudController { +public class DataElementController + extends AbstractCrudController { private final DataElementService dataElementService; private final MergeService dataElementMergeService; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dataelement/DataElementGroupController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dataelement/DataElementGroupController.java index 0be59f38e91d..0914e655202d 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dataelement/DataElementGroupController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dataelement/DataElementGroupController.java @@ -45,6 +45,7 @@ import org.hisp.dhis.dxf2.metadata.MetadataExportParams; import org.hisp.dhis.dxf2.webmessage.WebMessageException; import org.hisp.dhis.feedback.NotFoundException; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.hisp.dhis.webapi.webdomain.WebMetadata; import org.hisp.dhis.webapi.webdomain.WebOptions; @@ -63,7 +64,8 @@ @Controller @RequestMapping("/api/dataElementGroups") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class DataElementGroupController extends AbstractCrudController { +public class DataElementGroupController + extends AbstractCrudController { @Autowired private CategoryService dataElementCategoryService; @Autowired private DataElementService dataElementService; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dataelement/DataElementGroupSetController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dataelement/DataElementGroupSetController.java index a337845383b4..467671d2898d 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dataelement/DataElementGroupSetController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dataelement/DataElementGroupSetController.java @@ -29,6 +29,7 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.dataelement.DataElementGroupSet; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -39,4 +40,5 @@ @Controller @RequestMapping("/api/dataElementGroupSets") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class DataElementGroupSetController extends AbstractCrudController {} +public class DataElementGroupSetController + extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dataelement/DataElementOperandController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dataelement/DataElementOperandController.java index 6d6f049637cc..61a5b860dbef 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dataelement/DataElementOperandController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dataelement/DataElementOperandController.java @@ -32,50 +32,39 @@ import com.google.common.collect.Lists; import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; +import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; -import org.cache2k.Cache; -import org.cache2k.Cache2kBuilder; import org.hisp.dhis.category.CategoryService; import org.hisp.dhis.common.DhisApiVersion; import org.hisp.dhis.common.IdentifiableObjectManager; import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.common.Pager; +import org.hisp.dhis.common.UID; import org.hisp.dhis.common.collection.CollectionUtils; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.dataelement.DataElementGroup; import org.hisp.dhis.dataelement.DataElementOperand; import org.hisp.dhis.dataset.DataSet; -import org.hisp.dhis.dxf2.common.OrderParams; import org.hisp.dhis.fieldfilter.FieldFilterParams; import org.hisp.dhis.fieldfilter.FieldFilterService; -import org.hisp.dhis.fieldfiltering.FieldPreset; import org.hisp.dhis.node.NodeUtils; import org.hisp.dhis.node.types.RootNode; -import org.hisp.dhis.query.Order; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.query.Query; import org.hisp.dhis.query.QueryParserException; import org.hisp.dhis.query.QueryService; -import org.hisp.dhis.schema.Schema; -import org.hisp.dhis.schema.SchemaService; -import org.hisp.dhis.user.CurrentUser; -import org.hisp.dhis.user.User; import org.hisp.dhis.webapi.mvc.annotation.ApiVersion; -import org.hisp.dhis.webapi.service.ContextService; import org.hisp.dhis.webapi.service.LinkService; -import org.hisp.dhis.webapi.utils.PaginationUtils; -import org.hisp.dhis.webapi.webdomain.WebMetadata; -import org.hisp.dhis.webapi.webdomain.WebOptions; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; /** * @author Morten Olav Hansen */ +@OpenApi.EntityType(DataElementOperand.class) @OpenApi.Document( entity = DataElementOperand.class, classifiers = {"team:platform", "purpose:metadata"}) @@ -92,45 +81,33 @@ public class DataElementOperandController { private final LinkService linkService; - private final ContextService contextService; - - private final SchemaService schemaService; - private final CategoryService dataElementCategoryService; - private final Cache paginationCountCache = - new Cache2kBuilder() {}.expireAfterWrite(1, TimeUnit.MINUTES).build(); - - @GetMapping - @SuppressWarnings("unchecked") - public @ResponseBody RootNode getObjectList( - @RequestParam Map rpParameters, - OrderParams orderParams, - @CurrentUser User currentUser) - throws QueryParserException { - Schema schema = schemaService.getDynamicSchema(DataElementOperand.class); - - List fields = Lists.newArrayList(contextService.getParameterValues("fields")); - List filters = Lists.newArrayList(contextService.getParameterValues("filter")); - List orders = orderParams.getOrders(schema); + @Data + @EqualsAndHashCode(callSuper = true) + public static final class GetDataElementOperandObjectListParams extends GetObjectListParams { + boolean persisted; + boolean totals; - if (fields.isEmpty()) { - fields.addAll(FieldPreset.ALL.getFields()); - } + @OpenApi.Property({UID.class, DataSet.class}) + String dataSet; + } - WebOptions options = new WebOptions(rpParameters); - WebMetadata metadata = new WebMetadata(); + private List getUnfilteredDataElementOperands( + GetDataElementOperandObjectListParams params) { List dataElementOperands = List.of(); - if (options.isTrue("persisted")) { + if (params.isPersisted()) { dataElementOperands = Lists.newArrayList(manager.getAll(DataElementOperand.class)); } else { - boolean totals = options.isTrue("totals"); + boolean totals = params.isTotals(); - String deg = CollectionUtils.popStartsWith(filters, "dataElement.dataElementGroups.id:eq:"); + String deg = + CollectionUtils.popStartsWith( + params.getFilters(), "dataElement.dataElementGroups.id:eq:"); deg = deg != null ? deg.substring("dataElement.dataElementGroups.id:eq:".length()) : null; - String ds = options.get("dataSet"); + String ds = params.getDataSet(); if (deg != null) { DataElementGroup dataElementGroup = manager.get(DataElementGroup.class, deg); @@ -146,71 +123,52 @@ public class DataElementOperandController { dataElementOperands = dataElementCategoryService.getOperands(dataElements, totals); } } + return dataElementOperands; + } + + @GetMapping + @SuppressWarnings("unchecked") + public @ResponseBody RootNode getObjectList(GetDataElementOperandObjectListParams params) + throws QueryParserException { + List allItems = getUnfilteredDataElementOperands(params); // This is needed for two reasons: // 1) We are doing in-memory paging; // 2) We have to count all items respecting the filtering and the // initial universe of elements. In this case, the variable // "dataElementOperands". - Query queryForCount = queryService.getQueryFromUrl(DataElementOperand.class, filters, orders); - queryForCount.setObjects(dataElementOperands); - - List totalOfItems = - (List) queryService.query(queryForCount); - - Query query = - queryService.getQueryFromUrl( - DataElementOperand.class, - filters, - orders, - PaginationUtils.getPaginationData(options), - options.getRootJunction()); - query.setDefaultOrder(); - query.setObjects(dataElementOperands); - - dataElementOperands = (List) queryService.query(query); - Pager pager = metadata.getPager(); - - if (options.hasPaging() && pager == null) { - final long countTotal = isNotEmpty(totalOfItems) ? totalOfItems.size() : 0; - - // fetch the count for the current query from a short-lived cache - - long cachedCountTotal = - !isFilterNullOrBlank(options.get("filter")) - ? paginationCountCache.computeIfAbsent( - calculatePaginationCountKey(currentUser, options), () -> countTotal) - : countTotal; - - pager = new Pager(options.getPage(), cachedCountTotal, options.getPageSize()); - + Pager pager = null; + if (params.isPaging()) { + params.setPaging(false); + Query queryForCount = queryService.getQueryFromUrl(DataElementOperand.class, params); + queryForCount.setObjects(allItems); + + List totalOfItems = queryService.query(queryForCount); + int countTotal = isNotEmpty(totalOfItems) ? totalOfItems.size() : 0; + pager = new Pager(params.getPage(), countTotal, params.getPageSize()); linkService.generatePagerLinks(pager, DataElementOperand.class); + params.setPaging(true); } + Query query = queryService.getQueryFromUrl(DataElementOperand.class, params); + query.setDefaultOrder(); + query.setObjects(allItems); + + List pageItems = (List) queryService.query(query); + RootNode rootNode = NodeUtils.createMetadata(); if (pager != null) { rootNode.addChild(NodeUtils.createPager(pager)); } + List fields = params.getFields(); + if (fields == null || fields.isEmpty()) fields = List.of("*"); + rootNode.addChild( fieldFilterService.toCollectionNode( - DataElementOperand.class, new FieldFilterParams(dataElementOperands, fields))); + DataElementOperand.class, new FieldFilterParams(pageItems, fields))); return rootNode; } - - private String calculatePaginationCountKey(User currentUser, WebOptions options) { - return currentUser.getUsername() - + "." - + "DataElementOperand" - + "." - + options.get("filter") - + "." - + options.getRootJunction().name(); - } - - private boolean isFilterNullOrBlank(String filter) { - return filter == null || filter.isBlank(); - } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dimension/DimensionController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dimension/DimensionController.java index 55a792e96f9b..a90fc377956d 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dimension/DimensionController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dimension/DimensionController.java @@ -27,19 +27,16 @@ */ package org.hisp.dhis.webapi.controller.dimension; -import static com.google.common.collect.Lists.newArrayList; import static java.lang.String.format; import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.hisp.dhis.common.CodeGenerator.isValidUid; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.google.common.collect.Lists; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.Map; import java.util.Set; import javax.annotation.Nonnull; import lombok.RequiredArgsConstructor; @@ -55,18 +52,16 @@ import org.hisp.dhis.common.Pager; import org.hisp.dhis.commons.jackson.domain.JsonRoot; import org.hisp.dhis.dataset.DataSet; -import org.hisp.dhis.dxf2.common.OrderParams; import org.hisp.dhis.feedback.NotFoundException; import org.hisp.dhis.fieldfilter.FieldFilterParams; import org.hisp.dhis.fieldfiltering.FieldPath; -import org.hisp.dhis.fieldfiltering.FieldPreset; import org.hisp.dhis.hibernate.InternalHibernateGenericStore; import org.hisp.dhis.node.AbstractNode; import org.hisp.dhis.node.Node; import org.hisp.dhis.node.NodeUtils; import org.hisp.dhis.node.types.CollectionNode; import org.hisp.dhis.node.types.RootNode; -import org.hisp.dhis.query.Order; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.query.Query; import org.hisp.dhis.query.QueryParserException; import org.hisp.dhis.user.CurrentUser; @@ -77,8 +72,6 @@ import org.hisp.dhis.webapi.utils.PaginationUtils.PagedEntities; import org.hisp.dhis.webapi.webdomain.StreamingJsonRoot; import org.hisp.dhis.webapi.webdomain.WebMetadata; -import org.hisp.dhis.webapi.webdomain.WebOptions; -import org.hisp.dhis.webapi.webdomain.WebRequestData; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @@ -95,7 +88,8 @@ @Controller @RequestMapping("/api/dimensions") @RequiredArgsConstructor -public class DimensionController extends AbstractCrudController { +public class DimensionController + extends AbstractCrudController { // ------------------------------------------------------------------------- // Dependencies @@ -116,7 +110,7 @@ public class DimensionController extends AbstractCrudController> getObjectList( - @RequestParam Map rpParameters, - OrderParams orderParams, + GetObjectListParams params, HttpServletResponse response, @CurrentUser UserDetails currentUser) { - WebRequestData requestData = applyRequestSetup(rpParameters, orderParams); - PagedEntities pagedEntities = getPagedEntities(requestData); + addProgrammaticModifiers(params); + PagedEntities pagedEntities = getPagedEntities(params); linkService.generatePagerLinks(pagedEntities.pager(), RESOURCE_PATH); return ResponseEntity.ok( @@ -152,14 +140,13 @@ protected DimensionalObject getEntity(String uid, WebOptions options) throws Not pagedEntities.pager(), getSchema().getCollectionName(), org.hisp.dhis.fieldfiltering.FieldFilterParams.of( - pagedEntities.entities(), requestData.fields()))); + pagedEntities.entities(), params.getFieldsJsonList()))); } @Override @GetMapping(produces = {"text/csv", "application/text"}) public ResponseEntity getObjectListCsv( - @RequestParam Map rpParameters, - OrderParams orderParams, + GetObjectListParams params, @CurrentUser UserDetails currentUser, @RequestParam(defaultValue = ",") char separator, @RequestParam(defaultValue = ";") String arraySeparator, @@ -167,11 +154,15 @@ public ResponseEntity getObjectListCsv( HttpServletResponse response) throws IOException { - WebRequestData requestData = applyRequestSetup(rpParameters, orderParams); - PagedEntities pagedEntities = getPagedEntities(requestData); + addProgrammaticModifiers(params); + PagedEntities pagedEntities = getPagedEntities(params); String csv = applyCsvSteps( - requestData.fields(), pagedEntities.entities(), separator, arraySeparator, skipHeader); + params.getFieldsCsvList(), + pagedEntities.entities(), + separator, + arraySeparator, + skipHeader); return ResponseEntity.ok(csv); } @@ -182,47 +173,42 @@ public ResponseEntity getObjectListCsv( @OpenApi.Property(name = "pager", value = Pager.class), @OpenApi.Property(name = "items", value = BaseDimensionalItemObject[].class) }) - @SuppressWarnings("unchecked") @GetMapping("/{uid}/items") - public @ResponseBody RootNode getItems( - @PathVariable String uid, - @RequestParam Map parameters, - OrderParams orderParams) + public @ResponseBody RootNode getItems(@PathVariable String uid, GetObjectListParams params) throws QueryParserException { - List fields = newArrayList(contextService.getParameterValues("fields")); - List filters = newArrayList(contextService.getParameterValues("filter")); - List orders = orderParams.getOrders(getSchema(DimensionalItemObject.class)); - WebOptions options = new WebOptions(parameters); - - if (fields.isEmpty()) { - fields.addAll(FieldPreset.defaultPreset().getFields()); - } // This is the base list used in this flow. It contains only items // allowed to the current user. List readableItems = dimensionService.getCanReadDimensionItems(uid); + // The query engine is just used as a tool to do in-memory filtering // This is needed for two reasons: // 1) We are doing in-memory paging; // 2) We have to count all items respecting the filtering. - Query queryForCount = - queryService.getQueryFromUrl(DimensionalItemObject.class, filters, orders); - queryForCount.setObjects(readableItems); - - List forCountItems = - (List) queryService.query(queryForCount); + boolean paging = params.isPaging(); + int totalOfItems = 0; + if (paging) { + params.setPaging(false); + Query queryForCount = queryService.getQueryFromUrl(DimensionalItemObject.class, params); + queryForCount.setObjects(readableItems); + + List totalItems = queryService.query(queryForCount); + totalOfItems = isNotEmpty(totalItems) ? totalItems.size() : 0; + params.setPaging(true); + } - Query query = - queryService.getQueryFromUrl( - DimensionalItemObject.class, filters, orders, getPaginationData(options)); + Query query = queryService.getQueryFromUrl(DimensionalItemObject.class, params); query.setObjects(readableItems); query.setDefaultOrder(); + @SuppressWarnings("unchecked") List paginatedItems = (List) queryService.query(query); + if (!paging) totalOfItems = paginatedItems.size(); RootNode rootNode = NodeUtils.createMetadata(); + List fields = params.getFieldsJsonList(); CollectionNode collectionNode = rootNode.addChild( oldFieldFilterService.toCollectionNode( @@ -234,8 +220,7 @@ public ResponseEntity getObjectListCsv( } // Adding pagination elements to the root node. - final int totalOfItems = isNotEmpty(forCountItems) ? forCountItems.size() : 0; - dimensionItemPageHandler.addPaginationToNodeIfEnabled(rootNode, options, uid, totalOfItems); + dimensionItemPageHandler.addPaginationToNodeIfEnabled(rootNode, params, uid, totalOfItems); return rootNode; } @@ -324,43 +309,14 @@ public ResponseEntity getDimensionsForDataSet( return ResponseEntity.ok(new JsonRoot("dimensions", objectNodes)); } - private PagedEntities getPagedEntities(WebRequestData requestData) { - List entities = dimensionService.getAllDimensions(); - WebMetadata metadata = new WebMetadata(); - - Query filteredQuery = - queryService.getQueryFromUrl( - DimensionalObject.class, requestData.filters(), requestData.orders()); - filteredQuery.setObjects(entities); + private PagedEntities getPagedEntities(GetObjectListParams params) { + Query filteredQuery = queryService.getQueryFromUrl(DimensionalObject.class, params); + filteredQuery.setObjects(dimensionService.getAllDimensions()); - List filteredEntities = + filteredQuery.setSkipPaging(true); // paging is done post + @SuppressWarnings("unchecked") + List filteredNotPaged = (List) queryService.query(filteredQuery); - return PaginationUtils.addPagingIfEnabled(metadata, requestData.options(), filteredEntities); - } - - /** - * This method performs some generic steps. It can be moved into the {@link - * org.hisp.dhis.webapi.controller.AbstractFullReadOnlyController} so other Controllers can use it - * to avoid duplication. - * - * @param rpParameters request parameters - * @return {@link WebRequestData} record purely for data packaging purposes, containing {@link - * WebOptions}, {@link List} of fields and {@link List} of filters - */ - protected WebRequestData applyRequestSetup( - Map rpParameters, OrderParams orderParams) { - - List fields = Lists.newArrayList(contextService.getParameterValues("fields")); - List filters = Lists.newArrayList(contextService.getParameterValues("filter")); - List orders = orderParams.getOrders(getSchema(DimensionalObject.class)); - - if (fields.isEmpty()) { - fields.addAll(FieldPreset.defaultPreset().getFields()); - } - - WebOptions options = new WebOptions(rpParameters); - forceFiltering(options, filters); - - return new WebRequestData(options, fields, filters, orders); + return PaginationUtils.addPagingIfEnabled(params, filteredNotPaged); } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dimension/DimensionItemPageHandler.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dimension/DimensionItemPageHandler.java index ecf4be1f0a53..7cdd65e9a680 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dimension/DimensionItemPageHandler.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/dimension/DimensionItemPageHandler.java @@ -36,6 +36,7 @@ import org.hisp.dhis.common.Pager; import org.hisp.dhis.dxf2.common.OrderParams; import org.hisp.dhis.node.types.RootNode; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.service.LinkService; import org.hisp.dhis.webapi.webdomain.WebOptions; import org.springframework.stereotype.Component; @@ -61,7 +62,7 @@ public class DimensionItemPageHandler { * pagination flag must be set to true. See {@link WebOptions#hasPaging(boolean)}. * * @param rootNode the root node where the pagination node will be appended to. - * @param webOptions the WebOptions settings. + * @param params for the paging used * @param dimensionUid the uid of the dimension queried in the API url. See {@link * DimensionController#getItems(String, Map, OrderParams)}. * @param totalOfItems the total of items. This is represented as page total. See {@link @@ -69,14 +70,14 @@ public class DimensionItemPageHandler { */ void addPaginationToNodeIfEnabled( final RootNode rootNode, - final WebOptions webOptions, + final GetObjectListParams params, final String dimensionUid, final int totalOfItems) { - final boolean isPaginationEnabled = webOptions.hasPaging(false); + final boolean isPaginationEnabled = params.isPaging(); if (isPaginationEnabled) { final String apiRelativeUrl = format(RESOURCE_PATH + "/%s/items", dimensionUid); - final Pager pager = new Pager(webOptions.getPage(), totalOfItems, webOptions.getPageSize()); + final Pager pager = new Pager(params.getPage(), totalOfItems, params.getPageSize()); linkService.generatePagerLinks(pager, apiRelativeUrl); rootNode.addChild(createPager(pager)); diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/EventChartController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/EventChartController.java index 3aa5ec0246d1..15070a26b94d 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/EventChartController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/EventChartController.java @@ -37,9 +37,8 @@ import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Date; -import java.util.List; -import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import org.hisp.dhis.common.DimensionService; import org.hisp.dhis.common.IllegalQueryException; import org.hisp.dhis.common.OpenApi; @@ -53,6 +52,8 @@ import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.organisationunit.OrganisationUnitService; import org.hisp.dhis.period.Period; +import org.hisp.dhis.query.GetObjectListParams; +import org.hisp.dhis.query.GetObjectParams; import org.hisp.dhis.system.util.CodecUtils; import org.hisp.dhis.user.CurrentUserUtil; import org.hisp.dhis.user.User; @@ -60,7 +61,6 @@ import org.hisp.dhis.visualization.PlotData; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.hisp.dhis.webapi.utils.ContextUtils; -import org.hisp.dhis.webapi.webdomain.WebOptions; import org.jfree.chart.ChartUtilities; import org.jfree.chart.JFreeChart; import org.springframework.beans.factory.annotation.Autowired; @@ -80,7 +80,7 @@ @Deprecated @Controller @RequestMapping("/api/eventCharts") -public class EventChartController extends AbstractCrudController { +public class EventChartController extends AbstractCrudController { @Autowired private EventChartService eventChartService; @Autowired private ChartService chartService; @@ -164,14 +164,18 @@ public void getChart( * @deprecated This is a temporary workaround to keep EventChart backward compatible with the new * EventVisualization entity. Only legacy and chart related types can be returned by this * endpoint. Also, multi-program charts cannot be generated, so they are filtered out. - * @param filters */ @Deprecated @Override - protected void forceFiltering(final WebOptions webOptions, final List filters) { - filters.add("type:!eq:PIVOT_TABLE"); - filters.add("type:!eq:LINE_LIST"); - filters.add("legacy:eq:true"); + protected void addProgrammaticModifiers(GetObjectListParams params) { + addProgrammaticFilters(params::addFilter); + } + + @Override + protected void addProgrammaticFilters(Consumer add) { + add.accept("type:!eq:PIVOT_TABLE"); + add.accept("type:!eq:LINE_LIST"); + add.accept("legacy:eq:true"); } @Override @@ -186,8 +190,7 @@ protected void preUpdateEntity(final EventChart eventChart, final EventChart new } @Override - protected void postProcessResponseEntity( - EventChart eventChart, WebOptions options, Map parameters) { + protected void postProcessResponseEntity(EventChart eventChart, GetObjectParams params) { eventChart.populateAnalyticalProperties(); User currentUser = userService.getUserByUsername(CurrentUserUtil.getCurrentUsername()); diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/EventReportController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/EventReportController.java index d6879917b166..b265b1e17e13 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/EventReportController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/EventReportController.java @@ -36,9 +36,8 @@ import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; -import java.util.List; -import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import org.hisp.dhis.common.DimensionService; import org.hisp.dhis.common.IllegalQueryException; import org.hisp.dhis.common.OpenApi; @@ -48,10 +47,11 @@ import org.hisp.dhis.i18n.I18nManager; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.period.Period; +import org.hisp.dhis.query.GetObjectListParams; +import org.hisp.dhis.query.GetObjectParams; import org.hisp.dhis.user.CurrentUserUtil; import org.hisp.dhis.user.User; import org.hisp.dhis.webapi.controller.AbstractCrudController; -import org.hisp.dhis.webapi.webdomain.WebOptions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -66,7 +66,8 @@ @Deprecated @Controller @RequestMapping("/api/eventReports") -public class EventReportController extends AbstractCrudController { +public class EventReportController + extends AbstractCrudController { @Autowired private DimensionService dimensionService; @Autowired private I18nManager i18nManager; @@ -103,13 +104,17 @@ protected EventReport deserializeXmlEntity(HttpServletRequest request) throws IO * @deprecated This is a temporary workaround to keep EventReport backward compatible with the new * EventVisualization entity. Only legacy and report related types can be returned by this * endpoint. - * @param filters */ @Deprecated @Override - protected void forceFiltering(final WebOptions webOptions, final List filters) { - filters.add("type:in:[PIVOT_TABLE,LINE_LIST]"); - filters.add("legacy:eq:true"); + protected void addProgrammaticModifiers(GetObjectListParams params) { + addProgrammaticFilters(params::addFilter); + } + + @Override + protected void addProgrammaticFilters(Consumer add) { + add.accept("type:in:[PIVOT_TABLE,LINE_LIST]"); + add.accept("legacy:eq:true"); } @Override @@ -124,8 +129,7 @@ protected void preUpdateEntity(final EventReport eventReport, final EventReport } @Override - protected void postProcessResponseEntity( - EventReport report, WebOptions options, Map parameters) { + protected void postProcessResponseEntity(EventReport report, GetObjectParams params) { report.populateAnalyticalProperties(); User currentUser = userService.getUserByUsername(CurrentUserUtil.getCurrentUsername()); diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/EventVisualizationController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/EventVisualizationController.java index d2e175403294..39a73a9c89c5 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/EventVisualizationController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/EventVisualizationController.java @@ -44,7 +44,6 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Function; @@ -65,6 +64,8 @@ import org.hisp.dhis.period.Period; import org.hisp.dhis.program.Program; import org.hisp.dhis.program.ProgramService; +import org.hisp.dhis.query.GetObjectListParams; +import org.hisp.dhis.query.GetObjectParams; import org.hisp.dhis.user.CurrentUserUtil; import org.hisp.dhis.user.User; import org.hisp.dhis.visualization.ChartService; @@ -72,7 +73,6 @@ import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.hisp.dhis.webapi.mvc.annotation.ApiVersion; import org.hisp.dhis.webapi.utils.ContextUtils; -import org.hisp.dhis.webapi.webdomain.WebOptions; import org.jfree.chart.ChartUtilities; import org.jfree.chart.JFreeChart; import org.springframework.stereotype.Controller; @@ -91,7 +91,8 @@ @ApiVersion({DEFAULT, ALL}) @AllArgsConstructor @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) -public class EventVisualizationController extends AbstractCrudController { +public class EventVisualizationController + extends AbstractCrudController { private final DimensionService dimensionService; private final LegendSetService legendSetService; @@ -161,7 +162,7 @@ protected EventVisualization deserializeXmlEntity(HttpServletRequest request) th @Override protected void postProcessResponseEntity( - EventVisualization eventVisualization, WebOptions options, Map parameters) { + EventVisualization eventVisualization, GetObjectParams params) { eventVisualization.populateAnalyticalProperties(); User currentUser = userService.getUserByUsername(CurrentUserUtil.getCurrentUsername()); diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramController.java index 83feef749591..b1b3a4434d59 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramController.java @@ -30,12 +30,12 @@ import static org.hisp.dhis.dxf2.webmessage.WebMessageUtils.created; import static org.hisp.dhis.dxf2.webmessage.WebMessageUtils.notFound; -import com.google.common.collect.Lists; import java.util.Collection; -import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.RequiredArgsConstructor; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.SetValuedMap; @@ -47,15 +47,11 @@ import org.hisp.dhis.feedback.ConflictException; import org.hisp.dhis.feedback.ForbiddenException; import org.hisp.dhis.feedback.NotFoundException; -import org.hisp.dhis.fieldfilter.Defaults; import org.hisp.dhis.program.Program; import org.hisp.dhis.program.ProgramService; -import org.hisp.dhis.query.Order; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.query.Query; -import org.hisp.dhis.query.QueryParserException; import org.hisp.dhis.webapi.controller.AbstractCrudController; -import org.hisp.dhis.webapi.webdomain.WebMetadata; -import org.hisp.dhis.webapi.webdomain.WebOptions; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -75,47 +71,25 @@ @RequestMapping("/api/programs") @RequiredArgsConstructor @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) -public class ProgramController extends AbstractCrudController { +public class ProgramController + extends AbstractCrudController { + + @Data + @EqualsAndHashCode(callSuper = true) + public static class GetProgramObjectListParams extends GetObjectListParams { + boolean userFilter; + } + private final ProgramService programService; private final CopyService copyService; @Override - @SuppressWarnings("unchecked") - protected List getEntityList( - WebMetadata metadata, - WebOptions options, - List filters, - List orders, - List objects) - throws QueryParserException { - boolean userFilter = Boolean.parseBoolean(options.getOptions().get("userFilter")); - - List entityList; - Query query = - queryService.getQueryFromUrl( - getEntityClass(), - filters, - orders, - getPaginationData(options), - options.getRootJunction()); - query.setDefaultOrder(); - query.setDefaults(Defaults.valueOf(options.get("defaults", DEFAULTS))); - - if (objects == null && options.getOptions().containsKey("query")) { - entityList = - Lists.newArrayList(manager.filter(getEntityClass(), options.getOptions().get("query"))); - } else { - entityList = (List) queryService.query(query); - } - - if (userFilter) { - List programs = programService.getCurrentUserPrograms(); - entityList.retainAll(programs); - metadata.setPager(null); + protected void modifyGetObjectList(GetProgramObjectListParams params, Query query) { + if (params.isUserFilter()) { + query.setSkipSharing(true); + query.setDataSharing(true); } - - return entityList; } @GetMapping("/{uid}/metadata") diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramDataElementController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramDataElementController.java index 11e9a508aab9..04dc42092f5b 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramDataElementController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramDataElementController.java @@ -27,33 +27,27 @@ */ package org.hisp.dhis.webapi.controller.event; -import com.google.common.collect.Lists; +import static org.apache.commons.collections4.CollectionUtils.isNotEmpty; + import java.util.List; -import java.util.Map; +import lombok.RequiredArgsConstructor; import org.hisp.dhis.common.DhisApiVersion; import org.hisp.dhis.common.DimensionalItemObject; import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.common.Pager; -import org.hisp.dhis.common.PagerUtils; -import org.hisp.dhis.dxf2.common.OrderParams; +import org.hisp.dhis.common.UID; import org.hisp.dhis.fieldfilter.FieldFilterParams; import org.hisp.dhis.fieldfilter.FieldFilterService; -import org.hisp.dhis.fieldfiltering.FieldPreset; import org.hisp.dhis.node.NodeUtils; import org.hisp.dhis.node.types.RootNode; +import org.hisp.dhis.program.Program; import org.hisp.dhis.program.ProgramDataElementDimensionItem; import org.hisp.dhis.program.ProgramService; -import org.hisp.dhis.query.Order; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.query.Query; import org.hisp.dhis.query.QueryParserException; import org.hisp.dhis.query.QueryService; -import org.hisp.dhis.schema.Schema; -import org.hisp.dhis.schema.SchemaService; import org.hisp.dhis.webapi.mvc.annotation.ApiVersion; -import org.hisp.dhis.webapi.service.ContextService; -import org.hisp.dhis.webapi.utils.PaginationUtils; -import org.hisp.dhis.webapi.webdomain.WebMetadata; -import org.hisp.dhis.webapi.webdomain.WebOptions; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -63,78 +57,48 @@ /** * @author Lars Helge Overland */ +@OpenApi.EntityType(ProgramDataElementDimensionItem.class) @OpenApi.Document( entity = DimensionalItemObject.class, classifiers = {"team:tracker", "purpose:metadata"}) @Controller @RequestMapping("/api/programDataElements") +@RequiredArgsConstructor @ApiVersion({DhisApiVersion.DEFAULT, DhisApiVersion.ALL}) public class ProgramDataElementController { - private final QueryService queryService; + private final QueryService queryService; private final FieldFilterService fieldFilterService; - - private final ContextService contextService; - - private final SchemaService schemaService; - private final ProgramService programService; - public ProgramDataElementController( - QueryService queryService, - FieldFilterService fieldFilterService, - ContextService contextService, - SchemaService schemaService, - ProgramService programService) { - this.queryService = queryService; - this.fieldFilterService = fieldFilterService; - this.contextService = contextService; - this.schemaService = schemaService; - this.programService = programService; - } - @GetMapping - @SuppressWarnings("unchecked") public @ResponseBody RootNode getObjectList( - @RequestParam Map rpParameters, OrderParams orderParams) + @OpenApi.Param({UID.class, Program.class}) @RequestParam String program, + GetObjectListParams params) throws QueryParserException { - Schema schema = schemaService.getDynamicSchema(ProgramDataElementDimensionItem.class); - List fields = Lists.newArrayList(contextService.getParameterValues("fields")); - List filters = Lists.newArrayList(contextService.getParameterValues("filter")); - List orders = orderParams.getOrders(schema); - - if (fields.isEmpty()) { - fields.addAll(FieldPreset.ALL.getFields()); + List allItems = + programService.getGeneratedProgramDataElements(program); + + Pager pager = null; + if (params.isPaging()) { + params.setPaging(false); + Query queryForCount = + queryService.getQueryFromUrl(ProgramDataElementDimensionItem.class, params); + queryForCount.setObjects(allItems); + List totalOfItems = queryService.query(queryForCount); + int countTotal = isNotEmpty(totalOfItems) ? totalOfItems.size() : 0; + pager = new Pager(params.getPage(), countTotal, params.getPageSize()); + params.setPaging(true); } - WebOptions options = new WebOptions(rpParameters); - WebMetadata metadata = new WebMetadata(); - - List programDataElements; - Query query = - queryService.getQueryFromUrl( - ProgramDataElementDimensionItem.class, - filters, - orders, - PaginationUtils.getPaginationData(options), - options.getRootJunction()); + Query query = queryService.getQueryFromUrl(ProgramDataElementDimensionItem.class, params); query.setDefaultOrder(); + query.setObjects(allItems); - if (options.contains("program")) { - String programUid = options.get("program"); - programDataElements = programService.getGeneratedProgramDataElements(programUid); - query.setObjects(programDataElements); - } - - programDataElements = (List) queryService.query(query); - - Pager pager = metadata.getPager(); - - if (options.hasPaging() && pager == null) { - pager = new Pager(options.getPage(), programDataElements.size(), options.getPageSize()); - programDataElements = PagerUtils.pageCollection(programDataElements, pager); - } + @SuppressWarnings("unchecked") + List pageItems = + (List) queryService.query(query); RootNode rootNode = NodeUtils.createMetadata(); @@ -142,10 +106,11 @@ public ProgramDataElementController( rootNode.addChild(NodeUtils.createPager(pager)); } + List fields = params.getFields(); + if (fields == null || fields.isEmpty()) fields = List.of("*"); rootNode.addChild( fieldFilterService.toCollectionNode( - ProgramDataElementDimensionItem.class, - new FieldFilterParams(programDataElements, fields))); + ProgramDataElementDimensionItem.class, new FieldFilterParams(pageItems, fields))); return rootNode; } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramIndicatorController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramIndicatorController.java index 9eae6a24b4af..8e3a63f021e1 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramIndicatorController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramIndicatorController.java @@ -41,6 +41,7 @@ import org.hisp.dhis.i18n.I18nManager; import org.hisp.dhis.program.ProgramIndicator; import org.hisp.dhis.program.ProgramIndicatorService; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; @@ -56,7 +57,8 @@ @RequestMapping("/api/programIndicators") @RequiredArgsConstructor @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) -public class ProgramIndicatorController extends AbstractCrudController { +public class ProgramIndicatorController + extends AbstractCrudController { private final ProgramIndicatorService programIndicatorService; private final I18nManager i18nManager; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramIndicatorGroupController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramIndicatorGroupController.java index 102e6808dbe4..1eaf2f9e22b9 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramIndicatorGroupController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramIndicatorGroupController.java @@ -29,6 +29,7 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.program.ProgramIndicatorGroup; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -40,4 +41,4 @@ @RequestMapping("/api/programIndicatorGroups") @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) public class ProgramIndicatorGroupController - extends AbstractCrudController {} + extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramRuleActionController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramRuleActionController.java index ee7eef49ca64..86bc2957e99e 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramRuleActionController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramRuleActionController.java @@ -40,6 +40,7 @@ import org.hisp.dhis.i18n.I18nManager; import org.hisp.dhis.program.ProgramIndicator; import org.hisp.dhis.programrule.ProgramRuleAction; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.rules.models.RuleValidationResult; import org.hisp.dhis.tracker.imports.programrule.engine.ProgramRuleEngine; import org.hisp.dhis.webapi.controller.AbstractCrudController; @@ -58,7 +59,8 @@ @RequiredArgsConstructor @RequestMapping("/api/programRuleActions") @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) -public class ProgramRuleActionController extends AbstractCrudController { +public class ProgramRuleActionController + extends AbstractCrudController { private final I18nManager i18nManager; private final ProgramRuleEngine programRuleEngine; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramRuleController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramRuleController.java index 8f36fefa5855..dc1d27783311 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramRuleController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramRuleController.java @@ -40,6 +40,7 @@ import org.hisp.dhis.i18n.I18nManager; import org.hisp.dhis.program.ProgramIndicator; import org.hisp.dhis.programrule.ProgramRule; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.rules.models.RuleValidationResult; import org.hisp.dhis.tracker.imports.programrule.engine.ProgramRuleEngine; import org.hisp.dhis.webapi.controller.AbstractCrudController; @@ -58,7 +59,8 @@ @AllArgsConstructor @RequestMapping("/api/programRules") @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) -public class ProgramRuleController extends AbstractCrudController { +public class ProgramRuleController + extends AbstractCrudController { private final I18nManager i18nManager; private final ProgramRuleEngine programRuleEngine; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramRuleVariableController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramRuleVariableController.java index 9a99747240e4..4a2cad0dfc17 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramRuleVariableController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramRuleVariableController.java @@ -29,6 +29,7 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.programrule.ProgramRuleVariable; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -39,4 +40,5 @@ @Controller @RequestMapping("/api/programRuleVariables") @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) -public class ProgramRuleVariableController extends AbstractCrudController {} +public class ProgramRuleVariableController + extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramSectionController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramSectionController.java index 9471bbb92f7e..5df0b4da246b 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramSectionController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramSectionController.java @@ -29,6 +29,7 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.program.ProgramSection; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -39,4 +40,5 @@ @Controller @RequestMapping("/api/programSections") @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) -public class ProgramSectionController extends AbstractCrudController {} +public class ProgramSectionController + extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramStageController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramStageController.java index ef3c848fbc87..471b99d59459 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramStageController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramStageController.java @@ -29,6 +29,7 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.program.ProgramStage; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -39,4 +40,5 @@ @Controller @RequestMapping("/api/programStages") @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) -public class ProgramStageController extends AbstractCrudController {} +public class ProgramStageController + extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramStageSectionController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramStageSectionController.java index c4367dab271c..e479fbc039fa 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramStageSectionController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/ProgramStageSectionController.java @@ -29,6 +29,7 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.program.ProgramStageSection; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -39,4 +40,5 @@ @Controller @RequestMapping("/api/programStageSections") @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) -public class ProgramStageSectionController extends AbstractCrudController {} +public class ProgramStageSectionController + extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/RelationshipTypeController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/RelationshipTypeController.java index 1afac30e4630..1f0c28996f23 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/RelationshipTypeController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/RelationshipTypeController.java @@ -28,6 +28,7 @@ package org.hisp.dhis.webapi.controller.event; import org.hisp.dhis.common.OpenApi; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.relationship.RelationshipType; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.stereotype.Controller; @@ -39,4 +40,5 @@ @Controller @RequestMapping("/api/relationshipTypes") @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) -public class RelationshipTypeController extends AbstractCrudController {} +public class RelationshipTypeController + extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/TrackedEntityAttributeController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/TrackedEntityAttributeController.java index b2a6b63e2df5..fd0d5213e8ab 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/TrackedEntityAttributeController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/TrackedEntityAttributeController.java @@ -37,10 +37,13 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import lombok.Data; +import lombok.EqualsAndHashCode; import org.hisp.dhis.common.DhisApiVersion; import org.hisp.dhis.common.IdentifiableObject; import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.dxf2.webmessage.WebMessageException; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.reservedvalue.ReserveValueException; import org.hisp.dhis.reservedvalue.ReservedValue; import org.hisp.dhis.reservedvalue.ReservedValueService; @@ -53,7 +56,6 @@ import org.hisp.dhis.webapi.mvc.annotation.ApiVersion; import org.hisp.dhis.webapi.service.ContextService; import org.hisp.dhis.webapi.utils.ContextUtils; -import org.hisp.dhis.webapi.webdomain.WebOptions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.util.StringUtils; @@ -70,7 +72,9 @@ @RequestMapping("/api/trackedEntityAttributes") @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) public class TrackedEntityAttributeController - extends AbstractCrudController { + extends AbstractCrudController< + TrackedEntityAttribute, + TrackedEntityAttributeController.GetTrackedEntityAttributeObjectListParams> { @Autowired private TrackedEntityAttributeService trackedEntityAttributeService; @@ -80,6 +84,12 @@ public class TrackedEntityAttributeController @Autowired private ContextService context; + @Data + @EqualsAndHashCode(callSuper = true) + public static final class GetTrackedEntityAttributeObjectListParams extends GetObjectListParams { + boolean indexableOnly; + } + @GetMapping( value = "/{id}/generateAndReserve", produces = {ContextUtils.CONTENT_TYPE_JSON, ContextUtils.CONTENT_TYPE_JAVASCRIPT}) @@ -167,7 +177,7 @@ private Map getRequiredValues( requiredValues.removeAll(result.keySet()); - if (requiredValues.size() > 0) { + if (!requiredValues.isEmpty()) { throw new WebMessageException( conflict( "Missing required values: " @@ -178,12 +188,11 @@ private Map getRequiredValues( } @Override - protected void forceFiltering(final WebOptions webOptions, final List filters) { - if (webOptions == null || !webOptions.isTrue("indexableOnly")) { - return; - } + protected void addProgrammaticModifiers(GetTrackedEntityAttributeObjectListParams params) { + if (!params.isIndexableOnly()) return; - if (filters.stream().anyMatch(f -> f.startsWith("id:"))) { + List filters = params.getFilters(); + if (filters != null && filters.stream().anyMatch(f -> f.startsWith("id:"))) { throw new IllegalArgumentException( "indexableOnly parameter cannot be set if a separate filter for id is specified"); } @@ -191,13 +200,13 @@ protected void forceFiltering(final WebOptions webOptions, final List fi Set indexableTeas = trackedEntityAttributeService.getAllTrigramIndexableTrackedEntityAttributes(); - StringBuilder sb = new StringBuilder("id:in:"); - sb.append( - indexableTeas.stream() - .map(IdentifiableObject::getUid) - .collect(Collectors.joining(",", "[", "]"))); + String filter = + "id:in:" + + indexableTeas.stream() + .map(IdentifiableObject::getUid) + .collect(Collectors.joining(",", "[", "]")); - filters.add(sb.toString()); + params.addFilter(filter); } private TrackedEntityAttribute getTrackedEntityAttribute(String id) throws WebMessageException { diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/TrackedEntityTypeController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/TrackedEntityTypeController.java index f536571a9c6a..269b56d57c89 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/TrackedEntityTypeController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/event/TrackedEntityTypeController.java @@ -28,6 +28,7 @@ package org.hisp.dhis.webapi.controller.event; import org.hisp.dhis.common.OpenApi; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.trackedentity.TrackedEntityType; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.stereotype.Controller; @@ -39,4 +40,5 @@ @Controller @RequestMapping("/api/trackedEntityTypes") @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) -public class TrackedEntityTypeController extends AbstractCrudController {} +public class TrackedEntityTypeController + extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorController.java index e9bd0f940f9c..6c4bacd80d01 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorController.java @@ -47,6 +47,7 @@ import org.hisp.dhis.indicator.Indicator; import org.hisp.dhis.merge.MergeParams; import org.hisp.dhis.merge.MergeService; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.security.RequiresAuthority; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.http.HttpStatus; @@ -64,7 +65,7 @@ @Slf4j @RequiredArgsConstructor @RequestMapping("/api/indicators") -public class IndicatorController extends AbstractCrudController { +public class IndicatorController extends AbstractCrudController { private final ExpressionService expressionService; private final ExpressionResolverCollection resolvers; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorGroupController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorGroupController.java index e04377a1ca40..4b82e2c4ef31 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorGroupController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorGroupController.java @@ -28,6 +28,7 @@ package org.hisp.dhis.webapi.controller.indicator; import org.hisp.dhis.indicator.IndicatorGroup; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -37,4 +38,5 @@ */ @Controller @RequestMapping("/api/indicatorGroups") -public class IndicatorGroupController extends AbstractCrudController {} +public class IndicatorGroupController + extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorGroupSetController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorGroupSetController.java index c2e09ddcc168..e8427918d517 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorGroupSetController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorGroupSetController.java @@ -28,6 +28,7 @@ package org.hisp.dhis.webapi.controller.indicator; import org.hisp.dhis.indicator.IndicatorGroupSet; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -37,4 +38,5 @@ */ @Controller @RequestMapping("/api/indicatorGroupSets") -public class IndicatorGroupSetController extends AbstractCrudController {} +public class IndicatorGroupSetController + extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorTypeController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorTypeController.java index 681799e6055b..ac1f15f029f7 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorTypeController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/indicator/IndicatorTypeController.java @@ -39,6 +39,7 @@ import org.hisp.dhis.indicator.IndicatorType; import org.hisp.dhis.merge.MergeParams; import org.hisp.dhis.merge.MergeService; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.security.RequiresAuthority; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.http.HttpStatus; @@ -56,7 +57,8 @@ @RequestMapping("/api/indicatorTypes") @RequiredArgsConstructor @Slf4j -public class IndicatorTypeController extends AbstractCrudController { +public class IndicatorTypeController + extends AbstractCrudController { private final MergeService indicatorTypeMergeService; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/mapping/ExternalMapLayerController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/mapping/ExternalMapLayerController.java index 3c82df495da2..ae191cb5ed8a 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/mapping/ExternalMapLayerController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/mapping/ExternalMapLayerController.java @@ -29,6 +29,7 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.mapping.ExternalMapLayer; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -39,4 +40,5 @@ @Controller @RequestMapping("/api/externalMapLayers") @OpenApi.Document(classifiers = {"team:analytics", "purpose:metadata"}) -public class ExternalMapLayerController extends AbstractCrudController {} +public class ExternalMapLayerController + extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/mapping/MapController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/mapping/MapController.java index 3e50e3776988..dea82258b4c8 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/mapping/MapController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/mapping/MapController.java @@ -64,6 +64,8 @@ import org.hisp.dhis.period.Period; import org.hisp.dhis.program.ProgramService; import org.hisp.dhis.program.ProgramStageService; +import org.hisp.dhis.query.GetObjectListParams; +import org.hisp.dhis.query.GetObjectParams; import org.hisp.dhis.schema.MetadataMergeParams; import org.hisp.dhis.trackedentity.TrackedEntityType; import org.hisp.dhis.user.CurrentUser; @@ -73,7 +75,6 @@ import org.hisp.dhis.user.UserService; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.hisp.dhis.webapi.utils.ContextUtils; -import org.hisp.dhis.webapi.webdomain.WebOptions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; @@ -92,7 +93,7 @@ @Controller @RequestMapping("/api/maps") @OpenApi.Document(classifiers = {"team:analytics", "purpose:metadata"}) -public class MapController extends AbstractCrudController { +public class MapController extends AbstractCrudController { private static final int MAP_MIN_WIDTH = 140; private static final int MAP_MIN_HEIGHT = 25; @@ -208,8 +209,7 @@ public void getMapData( // -------------------------------------------------------------------------- @Override - public void postProcessResponseEntity( - Map map, WebOptions options, java.util.Map parameters) { + public void postProcessResponseEntity(Map map, GetObjectParams params) { I18nFormat format = i18nManager.getI18nFormat(); User currentUser = userService.getUserByUsername(CurrentUserUtil.getCurrentUsername()); diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/mapping/MapViewController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/mapping/MapViewController.java index 94ff7fd06991..c20b09ea4403 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/mapping/MapViewController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/mapping/MapViewController.java @@ -29,27 +29,20 @@ import static org.hisp.dhis.dxf2.webmessage.WebMessageUtils.notFound; -import com.google.common.collect.Lists; import jakarta.servlet.http.HttpServletResponse; import java.awt.image.BufferedImage; -import java.util.List; import javax.imageio.ImageIO; import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.common.cache.CacheStrategy; import org.hisp.dhis.dxf2.webmessage.WebMessageException; -import org.hisp.dhis.fieldfilter.Defaults; import org.hisp.dhis.mapgeneration.MapGenerationService; import org.hisp.dhis.mapping.MapView; import org.hisp.dhis.mapping.MappingService; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.organisationunit.OrganisationUnitService; -import org.hisp.dhis.query.Order; -import org.hisp.dhis.query.Query; -import org.hisp.dhis.query.QueryParserException; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.hisp.dhis.webapi.utils.ContextUtils; -import org.hisp.dhis.webapi.webdomain.WebMetadata; -import org.hisp.dhis.webapi.webdomain.WebOptions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; @@ -64,7 +57,7 @@ @Controller @RequestMapping("/api/mapViews") @OpenApi.Document(classifiers = {"team:analytics", "purpose:metadata"}) -public class MapViewController extends AbstractCrudController { +public class MapViewController extends AbstractCrudController { @Autowired private MappingService mappingService; @Autowired private OrganisationUnitService organisationUnitService; @@ -109,40 +102,6 @@ public void getMapView( renderMapViewPng(mapView, response); } - // -------------------------------------------------------------------------- - // Hooks - // -------------------------------------------------------------------------- - - @Override - @SuppressWarnings("unchecked") - protected List getEntityList( - WebMetadata metadata, - WebOptions options, - List filters, - List orders, - List objects) - throws QueryParserException { - List entityList; - Query query = - queryService.getQueryFromUrl( - getEntityClass(), - filters, - orders, - getPaginationData(options), - options.getRootJunction()); - query.setDefaultOrder(); - query.setDefaults(Defaults.valueOf(options.get("defaults", DEFAULTS))); - - if (objects == null && options.getOptions().containsKey("query")) { - entityList = - Lists.newArrayList(manager.filter(getEntityClass(), options.getOptions().get("query"))); - } else { - entityList = (List) queryService.query(query); - } - - return entityList; - } - // -------------------------------------------------------------------------- // Supportive methods // -------------------------------------------------------------------------- diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/message/ProgramMessageController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/message/ProgramMessageController.java index 8cb40bcda07b..6a93abdd7364 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/message/ProgramMessageController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/message/ProgramMessageController.java @@ -45,6 +45,7 @@ import org.hisp.dhis.program.message.ProgramMessageOperationParams; import org.hisp.dhis.program.message.ProgramMessageService; import org.hisp.dhis.program.message.ProgramMessageStatus; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.security.RequiresAuthority; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.hisp.dhis.webapi.mvc.annotation.ApiVersion; @@ -60,7 +61,8 @@ @RequestMapping("/api/messages") @ApiVersion({DhisApiVersion.DEFAULT, DhisApiVersion.ALL}) @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) -public class ProgramMessageController extends AbstractCrudController { +public class ProgramMessageController + extends AbstractCrudController { @Autowired private ProgramMessageService programMessageService; @Autowired protected ProgramMessageRequestParamMapper requestParamMapper; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/notification/DataSetNotificationTemplateController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/notification/DataSetNotificationTemplateController.java index 95664fd68445..d16e81422259 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/notification/DataSetNotificationTemplateController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/notification/DataSetNotificationTemplateController.java @@ -30,6 +30,7 @@ import org.hisp.dhis.common.DhisApiVersion; import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.dataset.notifications.DataSetNotificationTemplate; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.hisp.dhis.webapi.mvc.annotation.ApiVersion; import org.springframework.stereotype.Controller; @@ -41,4 +42,4 @@ @ApiVersion(include = {DhisApiVersion.DEFAULT, DhisApiVersion.ALL}) @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) public class DataSetNotificationTemplateController - extends AbstractCrudController {} + extends AbstractCrudController {} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/notification/ProgramNotificationTemplateController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/notification/ProgramNotificationTemplateController.java index c2d1bddc058d..ffb35a318c54 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/notification/ProgramNotificationTemplateController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/notification/ProgramNotificationTemplateController.java @@ -38,6 +38,7 @@ import org.hisp.dhis.program.notification.ProgramNotificationTemplate; import org.hisp.dhis.program.notification.ProgramNotificationTemplateOperationParams; import org.hisp.dhis.program.notification.ProgramNotificationTemplateService; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.schema.descriptors.ProgramNotificationTemplateSchemaDescriptor; import org.hisp.dhis.security.RequiresAuthority; import org.hisp.dhis.webapi.controller.AbstractCrudController; @@ -56,7 +57,7 @@ @ApiVersion(include = {DhisApiVersion.DEFAULT, DhisApiVersion.ALL}) @OpenApi.Document(classifiers = {"team:tracker", "purpose:metadata"}) public class ProgramNotificationTemplateController - extends AbstractCrudController { + extends AbstractCrudController { private final ProgramNotificationTemplateService programNotificationTemplateService; private final ProgramNotificationTemplateRequestParamsMapper requestParamsMapper; diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/option/OptionController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/option/OptionController.java index ee7ad49ddfb5..2a291fbac7ab 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/option/OptionController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/option/OptionController.java @@ -29,6 +29,7 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.option.Option; +import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.webapi.controller.AbstractCrudController; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -39,4 +40,4 @@ @Controller @RequestMapping("/api/options") @OpenApi.Document(classifiers = {"team:platform", "purpose:metadata"}) -public class OptionController extends AbstractCrudController