From 5d0b855f188ddbea4ca98ad3195da3bee0b80572 Mon Sep 17 00:00:00 2001 From: Jan Bernitt Date: Mon, 27 Apr 2026 18:37:44 +0200 Subject: [PATCH 01/12] feat: URL params to records via JSON --- .../java/org/hisp/dhis/common/UrlParams.java | 37 ++++++ ...rams.java => MinMaxDataElementParams.java} | 95 +++------------ .../dhis/minmax/MinMaxDataElementService.java | 4 +- .../dhis/minmax/MinMaxDataElementStore.java | 4 +- .../DefaultMinMaxDataElementService.java | 4 +- .../HibernateMinMaxDataElementStore.java | 20 +-- .../minmax/MinMaxDataElementStoreTest.java | 12 +- .../hisp/dhis/test/webapi/MvcTestConfig.java | 14 ++- .../MinMaxDataElementController.java | 21 ++-- .../mvc/UrlParamsMethodArgumentResolver.java | 114 ++++++++++++++++++ .../webapi/security/config/WebMvcConfig.java | 14 ++- 11 files changed, 215 insertions(+), 124 deletions(-) create mode 100644 dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/UrlParams.java rename dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/{MinMaxDataElementQueryParams.java => MinMaxDataElementParams.java} (50%) create mode 100644 dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/UrlParams.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/UrlParams.java new file mode 100644 index 000000000000..717fa98802b4 --- /dev/null +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/UrlParams.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2004-2026, 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: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. 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. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.common; + +/** + * Marker interface to be implemented by {@link Record} classes to be mapped via json-tree. + * + * @author Jan Bernitt + */ +public interface UrlParams {} diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementQueryParams.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementParams.java similarity index 50% rename from dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementQueryParams.java rename to dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementParams.java index c4e2b4522791..b2807af7c011 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementQueryParams.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementParams.java @@ -29,92 +29,35 @@ */ package org.hisp.dhis.minmax; -import com.google.common.base.MoreObjects; +import static org.hisp.dhis.jsontree.Validation.YesNo.NO; + import java.util.List; -import org.apache.commons.lang3.BooleanUtils; import org.hisp.dhis.common.OpenApi; -import org.hisp.dhis.common.Pager; -import org.hisp.dhis.common.PagerUtils; +import org.hisp.dhis.common.UrlParams; +import org.hisp.dhis.jsontree.Validation; /** * @author Viet Nguyen */ -public class MinMaxDataElementQueryParams { - public static final MinMaxDataElementQueryParams EMPTY = new MinMaxDataElementQueryParams(); - - private List filters; - - private Boolean skipPaging; - - private Boolean paging; - - private int page = 1; - - private int pageSize = Pager.DEFAULT_PAGE_SIZE; - - private int total; - - public MinMaxDataElementQueryParams() {} +public record MinMaxDataElementParams( + List fields, + List filters, + Boolean skipPaging, + @Validation(required = NO) boolean paging, + @Validation(required = NO, minimum = 1) int page, + @Validation(required = NO, minimum = 1, maximum = 1000) int pageSize) + implements UrlParams { - public boolean isSkipPaging() { - return PagerUtils.isSkipPaging(skipPaging, paging); - } - - public void setSkipPaging(Boolean skipPaging) { - this.skipPaging = skipPaging; - } - - public boolean isPaging() { - return BooleanUtils.toBoolean(paging); - } - - public void setPaging(Boolean paging) { - this.paging = paging; - } + public static final MinMaxDataElementParams DEFAULT = + new MinMaxDataElementParams(List.of(), List.of(), null, true, 1, 50); - public int getPage() { - return page; - } - - public void setPage(int page) { - this.page = page; - } - - public int getPageSize() { - return pageSize; - } - - public void setPageSize(int pageSize) { - this.pageSize = pageSize; - } - - public int getTotal() { - return total; - } - - public void setTotal(int total) { - this.total = total; + public MinMaxDataElementParams(List filters) { + this(List.of(), filters, null, true, 1, 50); } @OpenApi.Ignore - public Pager getPager() { - return PagerUtils.isSkipPaging(skipPaging, paging) ? null : new Pager(page, total, pageSize); - } - - public List getFilters() { - return this.filters; - } - - public void setFilters(List filters) { - this.filters = filters; - } - - @Override - public String toString() { - return MoreObjects.toStringHelper(this) - .add("page", page) - .add("pageSize", pageSize) - .add("total", total) - .toString(); + public boolean isPaged() { + if (skipPaging != null) return !skipPaging; + return paging; } } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementService.java index 9b677599f695..3defe91339d9 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementService.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementService.java @@ -51,9 +51,9 @@ List getMinMaxDataElements( OrganisationUnit source, Collection dataElements); // TODO replace with use of QueryService once it does no longer require IdentifiableObject - List getMinMaxDataElements(MinMaxDataElementQueryParams query); + List getMinMaxDataElements(MinMaxDataElementParams query); - int countMinMaxDataElements(MinMaxDataElementQueryParams query); + int countMinMaxDataElements(MinMaxDataElementParams query); void removeMinMaxDataElements(OrganisationUnit organisationUnit); diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementStore.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementStore.java index a54af848637d..e82200c01748 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementStore.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementStore.java @@ -49,9 +49,9 @@ MinMaxDataElement get( List get(OrganisationUnit source, Collection dataElements); - List query(MinMaxDataElementQueryParams query); + List query(MinMaxDataElementParams query); - int countMinMaxDataElements(MinMaxDataElementQueryParams query); + int countMinMaxDataElements(MinMaxDataElementParams query); void delete(OrganisationUnit organisationUnit); diff --git a/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/DefaultMinMaxDataElementService.java b/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/DefaultMinMaxDataElementService.java index b5d68699dded..5446cf0160ca 100644 --- a/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/DefaultMinMaxDataElementService.java +++ b/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/DefaultMinMaxDataElementService.java @@ -69,12 +69,12 @@ public List getMinMaxDataElements( } @Override - public List getMinMaxDataElements(MinMaxDataElementQueryParams query) { + public List getMinMaxDataElements(MinMaxDataElementParams query) { return minMaxDataElementStore.query(query); } @Override - public int countMinMaxDataElements(MinMaxDataElementQueryParams query) { + public int countMinMaxDataElements(MinMaxDataElementParams query) { return minMaxDataElementStore.countMinMaxDataElements(query); } diff --git a/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/hibernate/HibernateMinMaxDataElementStore.java b/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/hibernate/HibernateMinMaxDataElementStore.java index 19c77a57f060..197d45ac4e9e 100644 --- a/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/hibernate/HibernateMinMaxDataElementStore.java +++ b/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/hibernate/HibernateMinMaxDataElementStore.java @@ -47,13 +47,12 @@ import javax.annotation.Nonnull; import org.hibernate.Session; import org.hisp.dhis.category.CategoryOptionCombo; -import org.hisp.dhis.common.Pager; import org.hisp.dhis.common.UID; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.hibernate.HibernateGenericStore; import org.hisp.dhis.hibernate.JpaQueryParameters; import org.hisp.dhis.minmax.MinMaxDataElement; -import org.hisp.dhis.minmax.MinMaxDataElementQueryParams; +import org.hisp.dhis.minmax.MinMaxDataElementParams; import org.hisp.dhis.minmax.MinMaxDataElementStore; import org.hisp.dhis.minmax.MinMaxValue; import org.hisp.dhis.minmax.MinMaxValueKey; @@ -134,29 +133,30 @@ public List get( } @Override - public List query(MinMaxDataElementQueryParams query) { + public List query(MinMaxDataElementParams query) { CriteriaBuilder builder = getCriteriaBuilder(); JpaQueryParameters parameters = newJpaParameters(); - parameters.addPredicate(root -> parseFilter(builder, root, query.getFilters())); + parameters.addPredicate(root -> parseFilter(builder, root, query.filters())); - if (!query.isSkipPaging()) { - Pager pager = query.getPager(); - parameters.setFirstResult(pager.getOffset()); - parameters.setMaxResults(pager.getPageSize()); + if (query.isPaged()) { + int pageSize = query.pageSize(); + int offset = (query.page() - 1) * pageSize; + parameters.setFirstResult(offset); + parameters.setMaxResults(pageSize); } return getList(builder, parameters); } @Override - public int countMinMaxDataElements(MinMaxDataElementQueryParams query) { + public int countMinMaxDataElements(MinMaxDataElementParams query) { CriteriaBuilder builder = getCriteriaBuilder(); return getCount( builder, newJpaParameters() - .addPredicate(root -> parseFilter(builder, root, query.getFilters())) + .addPredicate(root -> parseFilter(builder, root, query.filters())) .setUseDistinct(true)) .intValue(); } diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/minmax/MinMaxDataElementStoreTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/minmax/MinMaxDataElementStoreTest.java index 56fcd1f6028e..141b00fec587 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/minmax/MinMaxDataElementStoreTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/minmax/MinMaxDataElementStoreTest.java @@ -162,25 +162,21 @@ void testQuery() { minMaxDataElementStore.save(valueB); minMaxDataElementStore.save(valueC); minMaxDataElementStore.save(valueD); - MinMaxDataElementQueryParams params = new MinMaxDataElementQueryParams(); List filters = Lists.newArrayList(); filters.add("dataElement.id:eq:" + deA.getUid()); - params.setFilters(filters); - List result = minMaxDataElementStore.query(params); + List result = + minMaxDataElementStore.query(new MinMaxDataElementParams(filters)); assertNotNull(result); assertEquals(1, result.size()); - params = new MinMaxDataElementQueryParams(); filters.clear(); filters.add("min:eq:0"); - params.setFilters(filters); - result = minMaxDataElementStore.query(params); + result = minMaxDataElementStore.query(new MinMaxDataElementParams(filters)); assertNotNull(result); assertEquals(4, result.size()); filters.clear(); filters.add("dataElement.id:in:[" + deA.getUid() + "," + deB.getUid() + "]"); - params.setFilters(filters); - result = minMaxDataElementStore.query(params); + result = minMaxDataElementStore.query(new MinMaxDataElementParams(filters)); assertNotNull(result); assertEquals(2, result.size()); } diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/test/webapi/MvcTestConfig.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/test/webapi/MvcTestConfig.java index d4ad3c21802c..ad3529382ed2 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/test/webapi/MvcTestConfig.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/test/webapi/MvcTestConfig.java @@ -48,6 +48,7 @@ import org.hisp.dhis.webapi.mvc.CurrentSystemSettingsHandlerMethodArgumentResolver; import org.hisp.dhis.webapi.mvc.CurrentUserHandlerMethodArgumentResolver; import org.hisp.dhis.webapi.mvc.CustomRequestMappingHandlerMapping; +import org.hisp.dhis.webapi.mvc.UrlParamsMethodArgumentResolver; import org.hisp.dhis.webapi.mvc.interceptor.AuthorityInterceptor; import org.hisp.dhis.webapi.mvc.interceptor.SystemSettingsInterceptor; import org.hisp.dhis.webapi.mvc.interceptor.UserContextInterceptor; @@ -102,12 +103,12 @@ public class MvcTestConfig implements WebMvcConfigurer { @Autowired private NodeService nodeService; - @Autowired - private CurrentUserHandlerMethodArgumentResolver currentUserHandlerMethodArgumentResolver; + @Autowired private CurrentUserHandlerMethodArgumentResolver currentUserArgResolver; @Autowired - private CurrentSystemSettingsHandlerMethodArgumentResolver - currentSystemSettingsHandlerMethodArgumentResolver; + private CurrentSystemSettingsHandlerMethodArgumentResolver currentSystemSettingsArgResolver; + + @Autowired private UrlParamsMethodArgumentResolver urlParamsArgResolver; @Autowired private FieldsConverter fieldsConverter; @@ -252,8 +253,9 @@ public void addFormatters(FormatterRegistry registry) { @Override public void addArgumentResolvers(List resolvers) { - resolvers.add(currentUserHandlerMethodArgumentResolver); - resolvers.add(currentSystemSettingsHandlerMethodArgumentResolver); + resolvers.add(currentUserArgResolver); + resolvers.add(currentSystemSettingsArgResolver); + resolvers.add(urlParamsArgResolver); } @Bean diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MinMaxDataElementController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MinMaxDataElementController.java index 70ecaf0bc51c..17842dfb42e9 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MinMaxDataElementController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MinMaxDataElementController.java @@ -33,7 +33,6 @@ import static org.hisp.dhis.security.Authorities.F_MINMAX_DATAELEMENT_ADD; import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE; -import com.google.common.collect.Lists; import jakarta.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.InputStream; @@ -43,6 +42,7 @@ import lombok.AllArgsConstructor; import org.hisp.dhis.common.Maturity; import org.hisp.dhis.common.OpenApi; +import org.hisp.dhis.common.Pager; import org.hisp.dhis.common.UID; import org.hisp.dhis.csv.CSV; import org.hisp.dhis.datavalue.DataValue; @@ -55,7 +55,7 @@ import org.hisp.dhis.fieldfilter.FieldFilterService; import org.hisp.dhis.fieldfiltering.FieldPreset; import org.hisp.dhis.minmax.MinMaxDataElement; -import org.hisp.dhis.minmax.MinMaxDataElementQueryParams; +import org.hisp.dhis.minmax.MinMaxDataElementParams; import org.hisp.dhis.minmax.MinMaxDataElementService; import org.hisp.dhis.minmax.MinMaxValue; import org.hisp.dhis.minmax.MinMaxValueDeleteRequest; @@ -65,7 +65,6 @@ import org.hisp.dhis.node.types.RootNode; import org.hisp.dhis.query.QueryParserException; import org.hisp.dhis.security.RequiresAuthority; -import org.hisp.dhis.webapi.service.ContextService; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.DeleteMapping; @@ -88,28 +87,26 @@ @AllArgsConstructor public class MinMaxDataElementController { - private final ContextService contextService; private final MinMaxDataElementService minMaxService; private final FieldFilterService fieldFilterService; @GetMapping - public @ResponseBody RootNode getObjectList(MinMaxDataElementQueryParams query) + public @ResponseBody RootNode getObjectList(MinMaxDataElementParams query) throws QueryParserException { - List fields = Lists.newArrayList(contextService.getParameterValues("fields")); - List filters = Lists.newArrayList(contextService.getParameterValues("filter")); - query.setFilters(filters); + List fields = query.fields(); if (fields.isEmpty()) { - fields.addAll(FieldPreset.ALL.getFields()); + fields = FieldPreset.ALL.getFields(); } List minMaxDataElements = minMaxService.getMinMaxDataElements(query); RootNode rootNode = NodeUtils.createMetadata(); - if (!query.isSkipPaging()) { - query.setTotal(minMaxService.countMinMaxDataElements(query)); - rootNode.addChild(NodeUtils.createPager(query.getPager())); + if (query.isPaged()) { + int total = minMaxService.countMinMaxDataElements(query); + Pager pager = new Pager(query.page(), total, query.pageSize()); + rootNode.addChild(NodeUtils.createPager(pager)); } rootNode.addChild( diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java new file mode 100644 index 000000000000..00ce9bbbbb35 --- /dev/null +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2004-2026, 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: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. 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. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.webapi.mvc; + +import java.util.List; +import java.util.Set; +import javax.annotation.Nonnull; +import org.hisp.dhis.common.UrlParams; +import org.hisp.dhis.feedback.ConflictException; +import org.hisp.dhis.jsontree.JsonBuilder; +import org.hisp.dhis.jsontree.JsonMixed; +import org.hisp.dhis.jsontree.JsonNode; +import org.hisp.dhis.jsontree.JsonObject; +import org.hisp.dhis.jsontree.Text; +import org.hisp.dhis.jsontree.Validation; +import org.hisp.dhis.jsontree.Validation.NodeType; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +public class UrlParamsMethodArgumentResolver implements HandlerMethodArgumentResolver { + @Override + public boolean supportsParameter(MethodParameter parameter) { + Class type = parameter.getParameterType(); + return type.isRecord() && UrlParams.class.isAssignableFrom(type); + } + + @Nonnull + @Override + public Object resolveArgument( + @Nonnull MethodParameter parameter, + @Nonnull ModelAndViewContainer mavContainer, + @Nonnull NativeWebRequest request, + @Nonnull WebDataBinderFactory binderFactory) + throws Exception { + + Class paramsType = parameter.getParameterType(); + + List properties = JsonObject.properties(paramsType); + JsonNode object = + JsonBuilder.createObject( + obj -> { + for (JsonObject.Property p : properties) { + Text name = p.jsonName(); + String key = name.toString(); + String[] values = request.getParameterValues(key); + if (values == null) continue; + Set types = p.types(); + if (values.length == 0) { + if (types.contains(NodeType.BOOLEAN)) { + obj.addBoolean(name, true); + } else if (types.contains(NodeType.ARRAY)) { + obj.addArray(name, arr -> {}); + } + } else { + NodeType type = types.iterator().next(); + if (types.contains(NodeType.ARRAY)) type = NodeType.ARRAY; + if (types.size() == 1) { + switch (type) { + case INTEGER, NUMBER, BOOLEAN, NULL -> + obj.addMember(name, JsonNode.of(values[0])); + case STRING, OBJECT -> + obj.addString( + name, values.length == 1 ? values[0] : String.join(",", values)); + case ARRAY -> + obj.addArray( + name, + arr -> { + for (String v : values) arr.addString(v); + }); + } + } + } + } + }); + JsonMixed params = JsonMixed.of(object); + Validation.Result result = params.validate(paramsType, Validation.Mode.PROBE_ALL); + if (!result.errors().isEmpty()) { + throw new ConflictException(result.toString()); + } + return params.to(paramsType); + } +} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/security/config/WebMvcConfig.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/security/config/WebMvcConfig.java index 098e5e34684b..f8a3f325c459 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/security/config/WebMvcConfig.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/security/config/WebMvcConfig.java @@ -47,6 +47,7 @@ import org.hisp.dhis.webapi.mvc.CurrentSystemSettingsHandlerMethodArgumentResolver; import org.hisp.dhis.webapi.mvc.CurrentUserHandlerMethodArgumentResolver; import org.hisp.dhis.webapi.mvc.CustomRequestMappingHandlerMapping; +import org.hisp.dhis.webapi.mvc.UrlParamsMethodArgumentResolver; import org.hisp.dhis.webapi.mvc.interceptor.AuthorityInterceptor; import org.hisp.dhis.webapi.mvc.interceptor.HandlerMethodInterceptor; import org.hisp.dhis.webapi.mvc.interceptor.SystemSettingsInterceptor; @@ -106,12 +107,12 @@ public class WebMvcConfig extends DelegatingWebMvcConfiguration { Pattern.compile("/api/(\\d\\d/)?dataValueSets(.xml)?(.+)?"), Pattern.compile("/api/(\\d\\d/)?completeDataSetRegistrations(.xml)?(.+)?")); - @Autowired - private CurrentUserHandlerMethodArgumentResolver currentUserHandlerMethodArgumentResolver; + @Autowired private CurrentUserHandlerMethodArgumentResolver currentUserArgResolver; @Autowired - private CurrentSystemSettingsHandlerMethodArgumentResolver - currentSystemSettingsHandlerMethodArgumentResolver; + private CurrentSystemSettingsHandlerMethodArgumentResolver currentSystemSettingsArgResolver; + + @Autowired private UrlParamsMethodArgumentResolver urlParamsArgResolver; @Autowired private FieldsConverter fieldsConverter; @@ -156,8 +157,9 @@ public MultipartResolver multipartResolver() { @Override public void addArgumentResolvers(List resolvers) { - resolvers.add(currentUserHandlerMethodArgumentResolver); - resolvers.add(currentSystemSettingsHandlerMethodArgumentResolver); + resolvers.add(currentUserArgResolver); + resolvers.add(currentSystemSettingsArgResolver); + resolvers.add(urlParamsArgResolver); } @Bean From ab8e30349caa16c7936f58df29768cfd6f4ea91b Mon Sep 17 00:00:00 2001 From: Jan Bernitt Date: Wed, 6 May 2026 13:29:47 +0200 Subject: [PATCH 02/12] fix: update to version 1.9.2 and single error as bad request --- .../java/org/hisp/dhis/common/UrlParams.java | 3 ++ .../hisp/dhis/datavalue/DataEntryInput.java | 43 ++++++++----------- .../mvc/UrlParamsMethodArgumentResolver.java | 17 +++++--- .../dhis/webapi/openapi/OpenApiObject.java | 22 ++++------ .../dhis/webapi/openapi/OpenApiRenderer.java | 18 ++++---- dhis-2/pom.xml | 2 +- 6 files changed, 53 insertions(+), 52 deletions(-) diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/UrlParams.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/UrlParams.java index 717fa98802b4..044c3f32d390 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/UrlParams.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/UrlParams.java @@ -32,6 +32,9 @@ /** * Marker interface to be implemented by {@link Record} classes to be mapped via json-tree. * + *

This is so this becomes an opt-in feature. + * * @author Jan Bernitt + * @since 2.44 */ public interface UrlParams {} diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/DataEntryInput.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/DataEntryInput.java index 8dabd4b2fcf0..2017cc9c17e2 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/DataEntryInput.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/DataEntryInput.java @@ -57,6 +57,7 @@ import org.hisp.dhis.jsontree.JsonArray; import org.hisp.dhis.jsontree.JsonMixed; import org.hisp.dhis.jsontree.JsonNode; +import org.hisp.dhis.jsontree.JsonNode.Index; import org.hisp.dhis.jsontree.JsonObject; import org.hisp.dhis.jsontree.JsonString; import org.hisp.staxwax.factory.XMLFactory; @@ -237,7 +238,7 @@ public static List fromJson( wrapAndCheckCompressionFormat(in).readAllBytes(), StandardCharsets.UTF_8, null, - JsonNode.Index.AUTO_SKIP)); + Index.AUTO_SKIP)); String ds = dvs.getString("dataSet").string(); String completionDate = dvs.getString("completeDate").string(); // keys that are common for all values @@ -286,31 +287,25 @@ public static List fromJson( // values... List values = new ArrayList<>(); - // Note that this uses JsonNode API to iterate without indexing - // to make the processing memory footprint smaller JsonArray dataValues = dvs.get("dataValues"); if (dataValues.exists()) - dataValues.stream(JsonNode.Index.SKIP) - .forEach( - dv -> { - JsonString coc = dv.getString("categoryOptionCombo"); - values.add( - new DataEntryValue.Input( - dv.getString("dataElement").string(), - dv.getString("orgUnit").string(), - coc.isString() ? coc.string() : null, - coc.isObject() - ? coc.asMap(JsonString.class).toMap(JsonString::string) - : null, - dv.getString("attributeOptionCombo").string(), - null, - null, - dv.getString("period").string(), - dv.getString("value").string(), - dv.getString("comment").string(), - dv.getBoolean("followUp").bool(), - dv.getBoolean("deleted").bool())); - }); + for (JsonObject dv : dataValues.values(Index.SKIP)) { + JsonString coc = dv.getString("categoryOptionCombo"); + values.add( + new DataEntryValue.Input( + dv.getString("dataElement").string(), + dv.getString("orgUnit").string(), + coc.isString() ? coc.string() : null, + coc.isObject() ? coc.asMap(JsonString.class).toMap(JsonString::string) : null, + dv.getString("attributeOptionCombo").string(), + null, + null, + dv.getString("period").string(), + dv.getString("value").string(), + dv.getString("comment").string(), + dv.getBoolean("followUp").bool(), + dv.getBoolean("deleted").bool())); + } DataEntryGroup.Ids ids = DataEntryGroup.Ids.of(schemes); return List.of( new DataEntryGroup.Input( diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java index 00ce9bbbbb35..f4d3b2d19d0c 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java @@ -33,7 +33,7 @@ import java.util.Set; import javax.annotation.Nonnull; import org.hisp.dhis.common.UrlParams; -import org.hisp.dhis.feedback.ConflictException; +import org.hisp.dhis.feedback.BadRequestException; import org.hisp.dhis.jsontree.JsonBuilder; import org.hisp.dhis.jsontree.JsonMixed; import org.hisp.dhis.jsontree.JsonNode; @@ -65,9 +65,10 @@ public Object resolveArgument( @Nonnull WebDataBinderFactory binderFactory) throws Exception { - Class paramsType = parameter.getParameterType(); + @SuppressWarnings("unchecked") + Class paramsType = (Class) parameter.getParameterType(); - List properties = JsonObject.properties(paramsType); + List properties = JsonObject.collapsedProperties(paramsType); JsonNode object = JsonBuilder.createObject( obj -> { @@ -105,9 +106,15 @@ public Object resolveArgument( } }); JsonMixed params = JsonMixed.of(object); - Validation.Result result = params.validate(paramsType, Validation.Mode.PROBE_ALL); + Validation.Result result = params.validate(paramsType, Validation.Mode.PROBE); if (!result.errors().isEmpty()) { - throw new ConflictException(result.toString()); + // TODO do we want to have different error codes per validation Rule? + Validation.Error e0 = result.errors().get(0); + throw new BadRequestException( + "URL parameter `" + + e0.path().segment() + + "` " + + e0.template().formatted(e0.args().toArray())); } return params.to(paramsType); } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiObject.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiObject.java index 28029c6bd25c..4b4c346da60d 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiObject.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiObject.java @@ -89,8 +89,7 @@ default JsonList tags() { } default Stream operations() { - return $paths() - .values() + return $paths().entries().stream() .flatMap( item -> Stream.of( @@ -305,8 +304,7 @@ default Set parameterNames() { } default String responseSuccessCode() { - return responses() - .keys() + return responses().keys().stream() .filter(code -> code.startsWith("2")) .map(Text::toString) .findFirst() @@ -314,13 +312,12 @@ default String responseSuccessCode() { } default List responseCodes() { - return responses().keys().distinct().sorted().map(Text::toString).toList(); + return responses().keys().stream().distinct().sorted().map(Text::toString).toList(); } default List responseMediaSubTypes() { - return responses() - .values() - .flatMap(r -> r.content().keys()) + return responses().entries().stream() + .flatMap(r -> r.content().keys().stream()) .map(type -> type.toString().substring(type.indexOf('/') + 1).toLowerCase()) .distinct() .sorted() @@ -348,7 +345,7 @@ default List responseSuccessSchemas() { private static List toListOfSchemas(JsonMap content) { if (content.isUndefined() || content.isEmpty()) return List.of(); - List schemas = content.values().map(MediaTypeObject::schema).toList(); + List schemas = content.entries().map(MediaTypeObject::schema).toList(); if (content.size() == 1) return schemas; if (MediaTypeObject.isUniform(content)) return List.of(schemas.get(0)); return schemas; @@ -455,7 +452,7 @@ static boolean isUniform(JsonMap content) { if (content.isUndefined()) return false; if (content.size() == 1) return true; List types = - content.values().map(MediaTypeObject::schema).map(SchemaObject::resolve).toList(); + content.entries().map(MediaTypeObject::schema).map(SchemaObject::resolve).toList(); SchemaObject type0 = types.get(0); if (type0.isShared()) return types.stream() @@ -637,9 +634,8 @@ default boolean isEnvelope() { JsonMap properties = properties(); if (properties.isUndefined()) return false; if (properties.size() != 2) return false; - if (properties.values().noneMatch(SchemaObject::isArrayType)) return false; - return properties - .values() + if (properties.entries().stream().noneMatch(SchemaObject::isArrayType)) return false; + return properties.entries().stream() .anyMatch(s -> s.isObjectType() || s.isRef() && s.resolve().isObjectType()); } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiRenderer.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiRenderer.java index 7924aa407ee5..45b102eecc02 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiRenderer.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/openapi/OpenApiRenderer.java @@ -33,7 +33,6 @@ import static java.util.Map.entry; import static java.util.stream.Collectors.counting; import static java.util.stream.Collectors.groupingBy; -import static java.util.stream.Collectors.toUnmodifiableSet; import static org.hisp.dhis.webapi.openapi.OpenApiHtmlUtils.stripHtml; import static org.hisp.dhis.webapi.openapi.OpenApiMarkdown.markdownToHTML; @@ -1171,7 +1170,7 @@ private void renderResponses(OperationObject op) { if (responses.isUndefined() || responses.isEmpty()) return; renderOperationSectionHeader("::", "Responses"); - responses.values().forEach(e -> renderResponse(op, e.getKey(), e)); + responses.entries().forEach(e -> renderResponse(op, e.getKey(), e)); } private void renderResponse(OperationObject op, Text code, ResponseObject response) { @@ -1198,7 +1197,7 @@ private void renderResponseSummary(Text code, ResponseObject response) { appendCode("mime", "="); if (content.size() == 1) { - MediaTypeObject common = content.values().toList().get(0); + MediaTypeObject common = content.entries().toList().get(0); appendCode("mime secondary", common.getKey()); appendCode("mime secondary", ":"); renderSchemaSignature(common.schema()); @@ -1206,7 +1205,7 @@ private void renderResponseSummary(Text code, ResponseObject response) { // they all share the same schema appendCode("mime secondary", "*"); appendCode("mime secondary", ":"); - SchemaObject common = content.values().limit(1).toList().get(0).schema(); + SchemaObject common = content.entries().stream().limit(1).toList().get(0).schema(); renderSchemaSignature(common); } else { // they are different, only list media types in summary @@ -1322,7 +1321,7 @@ private void renderSchemaSignatureTypeObject(SchemaObject schema) { renderSchemaSignatureType(schema.additionalProperties()); appendRaw("}"); } else if (schema.isWrapper()) { - SchemaObject p0 = schema.properties().values().limit(1).toList().get(0); + SchemaObject p0 = schema.properties().entries().stream().limit(1).toList().get(0); appendRaw("{"); appendEscaped(p0.getKey().toString()); appendRaw(":"); @@ -1330,7 +1329,10 @@ private void renderSchemaSignatureTypeObject(SchemaObject schema) { appendRaw("}"); } else if (schema.isEnvelope()) { SchemaObject values = - schema.properties().values().filter(SchemaObject::isArrayType).findFirst().orElse(null); + schema.properties().entries().stream() + .filter(SchemaObject::isArrayType) + .findFirst() + .orElse(null); if (values != null) { appendRaw("{#,"); // # short for the pager, comma for next property appendEscaped(values.getKey()); @@ -1409,9 +1411,7 @@ private void renderSchemaDetails( if (schema.isFlat()) return; if (schema.$type() != null) { Set names = - schema.isObjectType() - ? schema.properties().keys().collect(toUnmodifiableSet()) - : Set.of(); + schema.isObjectType() ? Set.copyOf(schema.properties().keys().toList()) : Set.of(); appendTag("header", markdownToHTML(schema.description(), names)); if (!skipDefault) renderLabelledValue("default", schema.$default(), "columns", 0); renderLabelledValue("enum", schema.$enum(), "columns", 0); diff --git a/dhis-2/pom.xml b/dhis-2/pom.xml index f33ecdd218ea..f631ff773af3 100644 --- a/dhis-2/pom.xml +++ b/dhis-2/pom.xml @@ -96,7 +96,7 @@ 1.4.6 2.0.0 - 1.9.1 + 1.9.2 6.5.10 From db737be7e41084a134ca5722121a077be25275fa Mon Sep 17 00:00:00 2001 From: Jan Bernitt Date: Tue, 12 May 2026 11:42:00 +0200 Subject: [PATCH 03/12] fix: compile errors --- .../dhis/test/webapi/json/domain/JsonSchema.java | 12 ++---------- .../controller/DataSummaryControllerTest.java | 16 ++++++++-------- .../webapi/controller/OpenApiControllerTest.java | 2 +- .../webapi/controller/SystemControllerTest.java | 2 +- 4 files changed, 12 insertions(+), 20 deletions(-) diff --git a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/webapi/json/domain/JsonSchema.java b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/webapi/json/domain/JsonSchema.java index 45e319fc9148..88aa692eb465 100644 --- a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/webapi/json/domain/JsonSchema.java +++ b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/webapi/json/domain/JsonSchema.java @@ -46,16 +46,8 @@ default Class getKlass() { return getString("klass").parsedClass(); } - default List> getReferences() { - return getArray("references") - .values( - klass -> { - try { - return Class.forName(klass); - } catch (ClassNotFoundException ex) { - throw new IllegalArgumentException(ex); - } - }); + default List> getReferences() { + return getArray("references").values().map(e -> e.parsedChecked(Class::forName)).toList(); } default String getRelativeApiEndpoint() { diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java index 1004144914f3..d42bff3427f3 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/DataSummaryControllerTest.java @@ -132,13 +132,13 @@ void canGetSummaryStatistics() { content .get("objectCounts") .asMap(JsonValue.class) - .values() + .entries() .forEach(value -> assertTrue(value.isInteger(), "Object count values should be integers")); assertTrue(content.has("activeUsers"), "Active users are missing"); content .get("activeUsers") .asMap(JsonValue.class) - .values() + .entries() .forEach(value -> assertTrue(value.isInteger(), "Active user values should be integers")); content .get("activeUsers") @@ -149,7 +149,7 @@ void canGetSummaryStatistics() { content .get("logins") .asMap(JsonValue.class) - .values() + .entries() .forEach(value -> assertTrue(value.isInteger(), "Login values should be integers")); content .get("logins") @@ -160,14 +160,14 @@ void canGetSummaryStatistics() { content .get("activeUsers") .asMap(JsonValue.class) - .values() + .entries() .forEach( value -> assertTrue(value.isInteger(), "User invitation values should be integers")); assertTrue(content.has("dataValueCount"), "Data value counts are missing"); content .get("dataValueCount") .asMap(JsonValue.class) - .values() + .entries() .forEach( value -> assertTrue(value.isInteger(), "Data value count values should be integers")); content @@ -182,7 +182,7 @@ void canGetSummaryStatistics() { content .get("eventCount") .asMap(JsonValue.class) - .values() + .entries() .forEach(value -> assertTrue(value.isInteger(), "Event count values should be integers")); content .get("eventCount") @@ -193,7 +193,7 @@ void canGetSummaryStatistics() { content .get("trackerEventCount") .asMap(JsonValue.class) - .values() + .entries() .forEach( value -> assertTrue(value.isInteger(), "Tracker event count values should be integers")); @@ -208,7 +208,7 @@ void canGetSummaryStatistics() { content .get("singleEventCount") .asMap(JsonValue.class) - .values() + .entries() .forEach( value -> assertTrue(value.isInteger(), "Single event count values should be integers")); content diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/OpenApiControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/OpenApiControllerTest.java index 0450fe43e716..4a02db86f062 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/OpenApiControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/OpenApiControllerTest.java @@ -221,7 +221,7 @@ void testGetOpenApiDocument_GetObjectListResponse() { assertEquals( Set.of("pager", "organisationUnits"), - properties.keys().map(Text::toString).collect(toSet()), + Set.copyOf(properties.keys().map(Text::toString).toList()), "there should only be a pager and an entity list property"); SchemaObject listSchema = properties.get("organisationUnits"); diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/SystemControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/SystemControllerTest.java index 07d5bfc5a55e..ef34caca7146 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/SystemControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/SystemControllerTest.java @@ -55,7 +55,7 @@ class SystemControllerTest extends H2ControllerIntegrationTestBase { void testGetTasksJson() { JsonObject tasks = GET("/system/tasks").content(HttpStatus.OK); assertTrue(tasks.isObject()); - tasks.values().forEach(m -> assertTrue(m.isObject(), m + " is not an object")); + tasks.entries().forEach(m -> assertTrue(m.isObject(), m + " is not an object")); } @Test From 23fc9d6538a65abeb531f8303ab675d5b30d2917 Mon Sep 17 00:00:00 2001 From: Jan Bernitt Date: Tue, 12 May 2026 14:09:24 +0200 Subject: [PATCH 04/12] fix: data export OU with children filter --- .../dhis/datavalue/hibernate/HibernateDataExportStore.java | 7 +++---- .../dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java | 3 +++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataExportStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataExportStore.java index 04f144e22961..73e322f1ae7c 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataExportStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataExportStore.java @@ -174,10 +174,9 @@ ou_ids AS ( ), ou_with_descendants_ids AS ( SELECT DISTINCT ou.organisationunitid - FROM organisationunit ou - LEFT JOIN organisationunit parent_ou ON (ou.path LIKE parent_ou.path || '%') - WHERE ou.organisationunitid IN (SELECT organisationunitid FROM ou_ids) - OR parent_ou.organisationunitid IN (SELECT organisationunitid FROM ou_ids) + FROM ou_ids + JOIN organisationunit root USING (organisationunitid) + JOIN organisationunit ou ON ou.path LIKE root.path || '%' ) SELECT de.uid AS deid, diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java index f4d3b2d19d0c..da7c0ed2c17f 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java @@ -105,6 +105,9 @@ public Object resolveArgument( } } }); + + //TODO move the below into a validation service + JsonMixed params = JsonMixed.of(object); Validation.Result result = params.validate(paramsType, Validation.Mode.PROBE); if (!result.errors().isEmpty()) { From 856c3276cb4ba2e87a75ecfb8c0026ac9eefc48f Mon Sep 17 00:00:00 2001 From: Jan Bernitt Date: Tue, 12 May 2026 16:51:01 +0200 Subject: [PATCH 05/12] chore: extract validation service --- .../validation/InputValidationService.java | 69 ++++++++++++ .../DefaultInputValidationService.java | 106 ++++++++++++++++++ .../mvc/UrlParamsMethodArgumentResolver.java | 73 ++---------- 3 files changed, 185 insertions(+), 63 deletions(-) create mode 100644 dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/validation/InputValidationService.java create mode 100644 dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/common/validation/DefaultInputValidationService.java diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/validation/InputValidationService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/validation/InputValidationService.java new file mode 100644 index 000000000000..96c0b90b99f5 --- /dev/null +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/validation/InputValidationService.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2004-2026, 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: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. 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. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.common.validation; + +import java.util.function.Function; +import org.hisp.dhis.feedback.BadRequestException; +import org.hisp.dhis.jsontree.JsonObject; + +/** + * Validates input against a schema {@link Class} which has {@link + * org.hisp.dhis.jsontree.Validation} annotations. + * + *

Formal input validation is all the validation that can be performed solely based on a target + * schema. In other words, it is a validation of input within the static (type) context without + * checking validity in the context the value will exist. Such semantic validation is performed in + * later stages. + * + * @author Jan Bernitt + * @since 2.44 + */ +public interface InputValidationService { + + /** + * Decodes key-value input such as request parameters into a JSON value based on the target + * schema. + * + * @param schema the target the key-value data should conform to + * @param values a lookup function to return the values for a given key + * @return a JSON object with the key-value data found in the given schema as provided by the + * values loopup function + */ + JsonObject decode(Class schema, Function values); + + /** + * Formal validation of the given input against the given schema. + * + * @param schema the target type the input should conform to + * @param input typically user input such as request bodies + * @throws BadRequestException in case the input is not valid + */ + void validate(Class schema, JsonObject input) throws BadRequestException; +} diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/common/validation/DefaultInputValidationService.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/common/validation/DefaultInputValidationService.java new file mode 100644 index 000000000000..d98f1e911c63 --- /dev/null +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/common/validation/DefaultInputValidationService.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2004-2026, 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: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. 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. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.common.validation; + +import java.util.List; +import java.util.Set; +import java.util.function.Function; +import org.hisp.dhis.common.NonTransactional; +import org.hisp.dhis.feedback.BadRequestException; +import org.hisp.dhis.jsontree.JsonBuilder; +import org.hisp.dhis.jsontree.JsonMixed; +import org.hisp.dhis.jsontree.JsonNode; +import org.hisp.dhis.jsontree.JsonObject; +import org.hisp.dhis.jsontree.Text; +import org.hisp.dhis.jsontree.Validation; +import org.springframework.stereotype.Service; + +@Service +public class DefaultInputValidationService implements InputValidationService { + + @Override + @NonTransactional + public JsonObject decode( + Class schema, Function propertyLookup) { + List properties = JsonObject.collapsedProperties(schema); + JsonNode object = + JsonBuilder.createObject( + obj -> { + for (JsonObject.Property p : properties) { + Text name = p.jsonName(); + String key = name.toString(); + String[] values = propertyLookup.apply(key); + if (values == null) continue; + Set types = p.types(); + if (values.length == 0) { + if (types.contains(Validation.NodeType.BOOLEAN)) { + obj.addBoolean(name, true); + } else if (types.contains(Validation.NodeType.ARRAY)) { + obj.addArray(name, arr -> {}); + } + } else { + Validation.NodeType type = types.iterator().next(); + if (types.contains(Validation.NodeType.ARRAY)) type = Validation.NodeType.ARRAY; + if (types.size() == 1) { + switch (type) { + case INTEGER, NUMBER, BOOLEAN, NULL -> + obj.addMember(name, JsonNode.of(values[0])); + case STRING, OBJECT -> + obj.addString( + name, values.length == 1 ? values[0] : String.join(",", values)); + case ARRAY -> + obj.addArray( + name, + arr -> { + for (String v : values) arr.addString(v); + }); + } + } + } + } + }); + return JsonMixed.of(object); + } + + @Override + @NonTransactional + public void validate(Class schema, JsonObject input) throws BadRequestException { + Validation.Result result = input.validate(schema, Validation.Mode.PROBE); + if (!result.errors().isEmpty()) { + // TODO do we want to have different error codes per validation Rule? + Validation.Error e0 = result.errors().get(0); + throw new BadRequestException( + "URL parameter `" + + e0.path().segment() + + "` " + + e0.template().formatted(e0.args().toArray())); + } + } +} diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java index da7c0ed2c17f..d03a41ae733f 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java @@ -29,18 +29,11 @@ */ package org.hisp.dhis.webapi.mvc; -import java.util.List; -import java.util.Set; import javax.annotation.Nonnull; +import lombok.RequiredArgsConstructor; import org.hisp.dhis.common.UrlParams; -import org.hisp.dhis.feedback.BadRequestException; -import org.hisp.dhis.jsontree.JsonBuilder; -import org.hisp.dhis.jsontree.JsonMixed; -import org.hisp.dhis.jsontree.JsonNode; +import org.hisp.dhis.common.validation.InputValidationService; import org.hisp.dhis.jsontree.JsonObject; -import org.hisp.dhis.jsontree.Text; -import org.hisp.dhis.jsontree.Validation; -import org.hisp.dhis.jsontree.Validation.NodeType; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -49,7 +42,11 @@ import org.springframework.web.method.support.ModelAndViewContainer; @Component +@RequiredArgsConstructor public class UrlParamsMethodArgumentResolver implements HandlerMethodArgumentResolver { + + private final InputValidationService inputValidationService; + @Override public boolean supportsParameter(MethodParameter parameter) { Class type = parameter.getParameterType(); @@ -66,59 +63,9 @@ public Object resolveArgument( throws Exception { @SuppressWarnings("unchecked") - Class paramsType = (Class) parameter.getParameterType(); - - List properties = JsonObject.collapsedProperties(paramsType); - JsonNode object = - JsonBuilder.createObject( - obj -> { - for (JsonObject.Property p : properties) { - Text name = p.jsonName(); - String key = name.toString(); - String[] values = request.getParameterValues(key); - if (values == null) continue; - Set types = p.types(); - if (values.length == 0) { - if (types.contains(NodeType.BOOLEAN)) { - obj.addBoolean(name, true); - } else if (types.contains(NodeType.ARRAY)) { - obj.addArray(name, arr -> {}); - } - } else { - NodeType type = types.iterator().next(); - if (types.contains(NodeType.ARRAY)) type = NodeType.ARRAY; - if (types.size() == 1) { - switch (type) { - case INTEGER, NUMBER, BOOLEAN, NULL -> - obj.addMember(name, JsonNode.of(values[0])); - case STRING, OBJECT -> - obj.addString( - name, values.length == 1 ? values[0] : String.join(",", values)); - case ARRAY -> - obj.addArray( - name, - arr -> { - for (String v : values) arr.addString(v); - }); - } - } - } - } - }); - - //TODO move the below into a validation service - - JsonMixed params = JsonMixed.of(object); - Validation.Result result = params.validate(paramsType, Validation.Mode.PROBE); - if (!result.errors().isEmpty()) { - // TODO do we want to have different error codes per validation Rule? - Validation.Error e0 = result.errors().get(0); - throw new BadRequestException( - "URL parameter `" - + e0.path().segment() - + "` " - + e0.template().formatted(e0.args().toArray())); - } - return params.to(paramsType); + Class schema = (Class) parameter.getParameterType(); + JsonObject params = inputValidationService.decode(schema, request::getParameterValues); + inputValidationService.validate(schema, params); + return params.to(schema); } } From 50841375ca9a4931224cee8103da8793f096a21f Mon Sep 17 00:00:00 2001 From: Jan Bernitt Date: Wed, 13 May 2026 13:44:12 +0200 Subject: [PATCH 06/12] chore: use record for DataValueChangelogQueryParams --- .../hisp/dhis/common/input/InputUtils.java} | 123 ++++++++++++++---- .../validation/InputValidationService.java | 69 ---------- .../DataValueChangelogQueryParams.java | 58 ++++++--- .../HibernateDataValueChangelogStore.java | 20 ++- .../DataValueChangelogQueryBuilderTest.java | 46 +++---- .../DataValueChangelogServiceTest.java | 69 ++++++---- .../DataValueChangelogStoreTest.java | 22 ++-- .../CategoryOptionComboMergeServiceTest.java | 4 +- .../DataElementMergeServiceTest.java | 14 +- .../webapi/controller/AuditController.java | 50 +------ .../mvc/UrlParamsMethodArgumentResolver.java | 20 ++- 11 files changed, 253 insertions(+), 242 deletions(-) rename dhis-2/{dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/common/validation/DefaultInputValidationService.java => dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java} (55%) delete mode 100644 dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/validation/InputValidationService.java diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/common/validation/DefaultInputValidationService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java similarity index 55% rename from dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/common/validation/DefaultInputValidationService.java rename to dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java index d98f1e911c63..be3d9509b575 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/common/validation/DefaultInputValidationService.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java @@ -27,28 +27,105 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.hisp.dhis.common.validation; +package org.hisp.dhis.common.input; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Function; -import org.hisp.dhis.common.NonTransactional; +import javax.annotation.Nonnull; +import org.hisp.dhis.common.UID; import org.hisp.dhis.feedback.BadRequestException; +import org.hisp.dhis.jsontree.JsonAccess; import org.hisp.dhis.jsontree.JsonBuilder; import org.hisp.dhis.jsontree.JsonMixed; import org.hisp.dhis.jsontree.JsonNode; import org.hisp.dhis.jsontree.JsonObject; import org.hisp.dhis.jsontree.Text; import org.hisp.dhis.jsontree.Validation; -import org.springframework.stereotype.Service; +import org.hisp.dhis.period.Period; +import org.springframework.util.MultiValueMap; +import org.springframework.web.util.UriComponentsBuilder; -@Service -public class DefaultInputValidationService implements InputValidationService { +/** + * Utilities around generic input decoding and formal (static context) validation. + * + * @author Jan Bernitt + * @since 2.44 + */ +public final class InputUtils { - @Override - @NonTransactional - public JsonObject decode( - Class schema, Function propertyLookup) { + static { + JsonAccess global = JsonAccess.GLOBAL; + global.addStringAs(UID.class, UID::of); + global.addStringAs(Period.class, Period::of); + } + + /** + * Formal input validation against a schema {@link Class} which has {@link + * org.hisp.dhis.jsontree.Validation} annotations for restrictions. + * + *

Formal input validation is all the validation that can be performed solely based on a target + * schema. In other words, it is a validation of input within the static (type) context without + * checking validity in the context the value will exist. Such semantic validation is performed in + * later stages. + * + * @param schema the target type the input should conform to + * @param input typically user input such as request bodies + * @throws BadRequestException in case the input is not valid + */ + public static void validateInput(@Nonnull Class schema, @Nonnull JsonObject input) + throws BadRequestException { + Validation.Result result = input.validate(schema, Validation.Mode.PROBE); + if (!result.errors().isEmpty()) { + // TODO do we want to have different error codes per validation Rule? + Validation.Error e0 = result.errors().get(0); + throw new BadRequestException( + "URL parameter `" + + e0.path().segment() + + "` " + + e0.template().formatted(e0.args().toArray())); + } + } + + @Nonnull + public static T decodeInput( + @Nonnull Class schema, @Nonnull Map properties) { + return decodeInput( + schema, + name -> { + Object value = properties.get(name); + return value == null ? null : new String[] {value.toString()}; + }) + .to(schema); + } + + @Nonnull + public static T decodeInput( + @Nonnull Class schema, @Nonnull String properties) { + MultiValueMap params = + UriComponentsBuilder.fromUriString("?" + properties).build().getQueryParams(); + return decodeInput( + schema, + name -> { + List values = params.get(name); + return values == null ? null : values.toArray(String[]::new); + }) + .to(schema); + } + + /** + * Decodes key-value input such as request parameters into a JSON value based on the target + * schema. + * + * @param schema the target the key-value data should conform to + * @param propertyLookup a lookup function to return the values for a given key + * @return a JSON object with the key-value data found in the given schema as provided by the + * values lookup-function + */ + @Nonnull + public static JsonObject decodeInput( + @Nonnull Class schema, @Nonnull Function propertyLookup) { List properties = JsonObject.collapsedProperties(schema); JsonNode object = JsonBuilder.createObject( @@ -59,7 +136,18 @@ public JsonObject decode( String[] values = propertyLookup.apply(key); if (values == null) continue; Set types = p.types(); - if (values.length == 0) { + if (types.isEmpty()) { + // default is string or array of string + if (values.length == 1) { + obj.addString(name, values[0]); + } else { + obj.addArray( + name, + arr -> { + for (String v : values) arr.addString(v); + }); + } + } else if (values.length == 0) { if (types.contains(Validation.NodeType.BOOLEAN)) { obj.addBoolean(name, true); } else if (types.contains(Validation.NodeType.ARRAY)) { @@ -88,19 +176,4 @@ public JsonObject decode( }); return JsonMixed.of(object); } - - @Override - @NonTransactional - public void validate(Class schema, JsonObject input) throws BadRequestException { - Validation.Result result = input.validate(schema, Validation.Mode.PROBE); - if (!result.errors().isEmpty()) { - // TODO do we want to have different error codes per validation Rule? - Validation.Error e0 = result.errors().get(0); - throw new BadRequestException( - "URL parameter `" - + e0.path().segment() - + "` " - + e0.template().formatted(e0.args().toArray())); - } - } } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/validation/InputValidationService.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/validation/InputValidationService.java deleted file mode 100644 index 96c0b90b99f5..000000000000 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/validation/InputValidationService.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) 2004-2026, 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: - * - * 1. Redistributions of source code must retain the above copyright notice, this - * list of conditions and the following disclaimer. - * - * 2. 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. - * - * 3. Neither the name of the copyright holder nor the names of its contributors - * may be used to endorse or promote products derived from this software without - * specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ -package org.hisp.dhis.common.validation; - -import java.util.function.Function; -import org.hisp.dhis.feedback.BadRequestException; -import org.hisp.dhis.jsontree.JsonObject; - -/** - * Validates input against a schema {@link Class} which has {@link - * org.hisp.dhis.jsontree.Validation} annotations. - * - *

Formal input validation is all the validation that can be performed solely based on a target - * schema. In other words, it is a validation of input within the static (type) context without - * checking validity in the context the value will exist. Such semantic validation is performed in - * later stages. - * - * @author Jan Bernitt - * @since 2.44 - */ -public interface InputValidationService { - - /** - * Decodes key-value input such as request parameters into a JSON value based on the target - * schema. - * - * @param schema the target the key-value data should conform to - * @param values a lookup function to return the values for a given key - * @return a JSON object with the key-value data found in the given schema as provided by the - * values loopup function - */ - JsonObject decode(Class schema, Function values); - - /** - * Formal validation of the given input against the given schema. - * - * @param schema the target type the input should conform to - * @param input typically user input such as request bodies - * @throws BadRequestException in case the input is not valid - */ - void validate(Class schema, JsonObject input) throws BadRequestException; -} diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueChangelogQueryParams.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueChangelogQueryParams.java index e0fe9cff7b11..049a8f937cbc 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueChangelogQueryParams.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueChangelogQueryParams.java @@ -29,12 +29,14 @@ */ package org.hisp.dhis.datavalue; -import java.util.ArrayList; import java.util.List; -import lombok.Data; -import lombok.experimental.Accessors; -import org.hisp.dhis.common.Pager; +import org.hisp.dhis.category.CategoryOptionCombo; +import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.common.UID; +import org.hisp.dhis.common.UrlParams; +import org.hisp.dhis.dataelement.DataElement; +import org.hisp.dhis.dataset.DataSet; +import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.period.Period; /** @@ -42,20 +44,42 @@ * * @author Lars Helge Overland */ -@Data -@Accessors(chain = true) -public class DataValueChangelogQueryParams { +public record DataValueChangelogQueryParams( + List fields, + @OpenApi.Property({UID[].class, DataSet.class}) List ds, + @OpenApi.Property({UID[].class, DataElement.class}) List de, + List pe, + @OpenApi.Property({UID[].class, OrganisationUnit.class}) List ou, + @OpenApi.Property({UID[].class, CategoryOptionCombo.class}) UID co, // COC + @OpenApi.Property({UID[].class, CategoryOptionCombo.class}) UID cc, // AOC + List type, + Boolean skipPaging, + boolean paging, + int page, + int pageSize) + implements UrlParams { - private List dataSets = new ArrayList<>(); - private List dataElements = new ArrayList<>(); - private List periods = new ArrayList<>(); - private List orgUnits = new ArrayList<>(); - private UID categoryOptionCombo; - private UID attributeOptionCombo; - private List types = new ArrayList<>(); - private Pager pager; + public static final DataValueChangelogQueryParams DEFAULT = ofType(); - public boolean hasPaging() { - return pager != null; + public static DataValueChangelogQueryParams ofType(DataValueChangelogType... types) { + return new DataValueChangelogQueryParams( + List.of(), + List.of(), + List.of(), + List.of(), + List.of(), + null, + null, + List.of(types), + null, + true, + 1, + 50); + } + + @OpenApi.Ignore + public boolean isPaged() { + if (skipPaging != null) return !skipPaging; + return paging; } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueChangelogStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueChangelogStore.java index 3006e6386bbe..4bf07f1e9902 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueChangelogStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueChangelogStore.java @@ -34,7 +34,6 @@ import java.util.List; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; -import org.hisp.dhis.common.Pager; import org.hisp.dhis.common.UID; import org.hisp.dhis.datavalue.DataValueChangelog; import org.hisp.dhis.datavalue.DataValueChangelogEntry; @@ -130,17 +129,16 @@ static QueryBuilder createEntriesQuery(DataValueChangelogQueryParams params, SQL AND pe.iso = ANY(:pe) ORDER BY dva.created DESC"""; - Pager pager = params.getPager(); return SQL.of(sql, api) - .setParameter("types", params.getTypes(), DataValueChangelogType::name) - .setParameter("pe", params.getPeriods(), Period::getIsoDate) - .setParameter("ds", params.getDataSets()) - .setParameter("de", params.getDataElements()) - .setParameter("ou", params.getOrgUnits()) - .setParameter("coc", params.getCategoryOptionCombo()) - .setParameter("aoc", params.getAttributeOptionCombo()) - .setOffset(pager == null ? null : pager.getOffset()) - .setLimit(pager == null ? null : pager.getPageSize()) + .setParameter("types", params.type(), DataValueChangelogType::name) + .setParameter("pe", params.pe(), Period::getIsoDate) + .setParameter("ds", params.ds()) + .setParameter("de", params.de()) + .setParameter("ou", params.ou()) + .setParameter("coc", params.co()) + .setParameter("aoc", params.cc()) + .setOffset(params.isPaged() ? (params.page() - 1) * params.pageSize() : null) + .setLimit(params.isPaged() ? params.pageSize() : null) .eraseNullParameterLines() .useEqualsOverInForParameters("types", "ds", "de", "ou", "pe") .eraseNullParameterJoinLine("de", "de") diff --git a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/datavalue/hibernate/DataValueChangelogQueryBuilderTest.java b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/datavalue/hibernate/DataValueChangelogQueryBuilderTest.java index 3b80a1fcaaa1..741d11d34e9b 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/datavalue/hibernate/DataValueChangelogQueryBuilderTest.java +++ b/dhis-2/dhis-services/dhis-service-core/src/test/java/org/hisp/dhis/datavalue/hibernate/DataValueChangelogQueryBuilderTest.java @@ -29,12 +29,10 @@ */ package org.hisp.dhis.datavalue.hibernate; +import static org.hisp.dhis.common.input.InputUtils.decodeInput; import static org.hisp.dhis.datavalue.hibernate.HibernateDataValueChangelogStore.createEntriesQuery; -import static org.hisp.dhis.period.Period.of; -import java.util.List; import java.util.Set; -import org.hisp.dhis.common.UID; import org.hisp.dhis.datavalue.DataValueChangelogQueryParams; import org.hisp.dhis.datavalue.DataValueChangelogType; import org.hisp.dhis.sql.AbstractQueryBuilderTest; @@ -53,8 +51,8 @@ class DataValueChangelogQueryBuilderTest extends AbstractQueryBuilderTest { @Test void testFilterByTypes() { DataValueChangelogQueryParams params = - new DataValueChangelogQueryParams() - .setTypes(List.of(DataValueChangelogType.CREATE, DataValueChangelogType.UPDATE)); + DataValueChangelogQueryParams.ofType( + DataValueChangelogType.CREATE, DataValueChangelogType.UPDATE); assertSQL( """ SELECT * @@ -68,7 +66,7 @@ void testFilterByTypes() { @Test void testFilterByTypes_Single() { DataValueChangelogQueryParams params = - new DataValueChangelogQueryParams().setTypes(List.of(DataValueChangelogType.UPDATE)); + DataValueChangelogQueryParams.ofType(DataValueChangelogType.UPDATE); assertSQL( """ SELECT * @@ -82,8 +80,8 @@ void testFilterByTypes_Single() { @Test void testCountByTypes() { DataValueChangelogQueryParams params = - new DataValueChangelogQueryParams() - .setTypes(List.of(DataValueChangelogType.UPDATE, DataValueChangelogType.CREATE)); + DataValueChangelogQueryParams.ofType( + DataValueChangelogType.UPDATE, DataValueChangelogType.CREATE); assertCountSQL( """ SELECT count(*) @@ -96,7 +94,7 @@ SELECT count(*) @Test void testCountByTypes_Single() { DataValueChangelogQueryParams params = - new DataValueChangelogQueryParams().setTypes(List.of(DataValueChangelogType.UPDATE)); + DataValueChangelogQueryParams.ofType(DataValueChangelogType.UPDATE); assertCountSQL( """ SELECT count(*) @@ -109,8 +107,7 @@ SELECT count(*) @Test void testFilterByDataElements() { DataValueChangelogQueryParams params = - new DataValueChangelogQueryParams() - .setDataElements(List.of(UID.of("de123456789"), UID.of("de987654321"))); + decodeInput(DataValueChangelogQueryParams.class, "de=de123456789&de=de987654321"); assertSQL( """ SELECT * @@ -125,7 +122,7 @@ void testFilterByDataElements() { @Test void testFilterByDataElements_Single() { DataValueChangelogQueryParams params = - new DataValueChangelogQueryParams().setDataElements(List.of(UID.of("de123456789"))); + decodeInput(DataValueChangelogQueryParams.class, "de=de123456789"); assertSQL( """ SELECT * @@ -140,8 +137,7 @@ void testFilterByDataElements_Single() { @Test void testFilterByOrgUnits() { DataValueChangelogQueryParams params = - new DataValueChangelogQueryParams() - .setOrgUnits(List.of(UID.of("ou123456789"), UID.of("ou987654321"))); + decodeInput(DataValueChangelogQueryParams.class, "ou=ou123456789&ou=ou987654321"); assertSQL( """ SELECT * @@ -156,7 +152,7 @@ void testFilterByOrgUnits() { @Test void testFilterByOrgUnits_Single() { DataValueChangelogQueryParams params = - new DataValueChangelogQueryParams().setOrgUnits(List.of(UID.of("ou123456789"))); + decodeInput(DataValueChangelogQueryParams.class, "ou=ou123456789"); assertSQL( """ SELECT * @@ -171,8 +167,7 @@ void testFilterByOrgUnits_Single() { @Test void testFilterByDataSets() { DataValueChangelogQueryParams params = - new DataValueChangelogQueryParams() - .setDataSets(List.of(UID.of("ds123456789"), UID.of("ds987654321"))); + decodeInput(DataValueChangelogQueryParams.class, "ds=ds123456789&ds=ds987654321"); assertSQL( """ SELECT * @@ -188,7 +183,7 @@ void testFilterByDataSets() { @Test void testFilterByDataSets_Single() { DataValueChangelogQueryParams params = - new DataValueChangelogQueryParams().setDataSets(List.of(UID.of("ds123456789"))); + decodeInput(DataValueChangelogQueryParams.class, "ds=ds123456789"); assertSQL( """ SELECT * @@ -204,7 +199,7 @@ void testFilterByDataSets_Single() { @Test void testFilterByPeriods() { DataValueChangelogQueryParams params = - new DataValueChangelogQueryParams().setPeriods(List.of(of("2021"), of("2022"))); + decodeInput(DataValueChangelogQueryParams.class, "pe=2021&pe=2022"); assertSQL( """ SELECT * @@ -219,7 +214,7 @@ void testFilterByPeriods() { @Test void testFilterByPeriods_Single() { DataValueChangelogQueryParams params = - new DataValueChangelogQueryParams().setPeriods(List.of(of("2021"))); + decodeInput(DataValueChangelogQueryParams.class, "pe=2021"); assertSQL( """ SELECT * @@ -234,7 +229,7 @@ void testFilterByPeriods_Single() { @Test void testFilterByCategoryOptionCombo() { DataValueChangelogQueryParams params = - new DataValueChangelogQueryParams().setCategoryOptionCombo(UID.of("coc23456789")); + decodeInput(DataValueChangelogQueryParams.class, "co=coc23456789"); assertSQL( """ SELECT * @@ -248,7 +243,7 @@ void testFilterByCategoryOptionCombo() { @Test void testFilterByAttributeOptionCombo() { DataValueChangelogQueryParams params = - new DataValueChangelogQueryParams().setAttributeOptionCombo(UID.of("aoc23456789")); + decodeInput(DataValueChangelogQueryParams.class, "cc=aoc23456789"); assertSQL( """ SELECT * @@ -262,10 +257,9 @@ void testFilterByAttributeOptionCombo() { @Test void testFilterByMixed() { DataValueChangelogQueryParams params = - new DataValueChangelogQueryParams() - .setTypes(List.of(DataValueChangelogType.UPDATE)) - .setDataSets(List.of(UID.of("ds123456789"), UID.of("ds987654321"))) - .setPeriods(List.of(of("2022"))); + decodeInput( + DataValueChangelogQueryParams.class, + "type=UPDATE&ds=ds123456789&ds=ds987654321&pe=2022"); assertSQL( """ SELECT * diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/datavalue/DataValueChangelogServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/datavalue/DataValueChangelogServiceTest.java index 3e8a360eaf08..619d0e2a7378 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/datavalue/DataValueChangelogServiceTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/datavalue/DataValueChangelogServiceTest.java @@ -29,10 +29,11 @@ */ package org.hisp.dhis.datavalue; +import static org.hisp.dhis.common.input.InputUtils.decodeInput; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.fail; -import java.util.List; +import java.util.Map; import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.category.CategoryService; import org.hisp.dhis.common.UID; @@ -144,12 +145,14 @@ void testAddGetDataValueAuditFromDataValue() { @Test void testAddGetDataValueAuditSingleRecord() { DataValueChangelogQueryParams params = - new DataValueChangelogQueryParams() - .setDataElements(List.of(UID.of(dataElementA))) - .setPeriods(List.of(periodA)) - .setOrgUnits(List.of(UID.of(orgUnitA))) - .setCategoryOptionCombo(UID.of(optionCombo)) - .setAttributeOptionCombo(UID.of(optionCombo)); + decodeInput( + DataValueChangelogQueryParams.class, + Map.of( + "de", UID.of(dataElementA), + "pe", periodA, + "ou", UID.of(orgUnitA), + "co", UID.of(optionCombo), + "cc", UID.of(optionCombo))); assertEquals(1, dataValueChangelogService.getChangelogEntries(params).size()); } @@ -157,45 +160,55 @@ void testAddGetDataValueAuditSingleRecord() { @Test void testGetDataValueAudit() { DataValueChangelogQueryParams params = - new DataValueChangelogQueryParams() - .setDataElements(List.of(UID.of(dataElementA))) - .setPeriods(List.of(periodA)) - .setOrgUnits(List.of(UID.of(orgUnitA))) - .setCategoryOptionCombo(UID.of(optionCombo)) - .setTypes(List.of(DataValueChangelogType.CREATE)); + decodeInput( + DataValueChangelogQueryParams.class, + Map.of( + "de", UID.of(dataElementA), + "pe", periodA, + "ou", UID.of(orgUnitA), + "co", UID.of(optionCombo), + "type", DataValueChangelogType.CREATE)); assertEquals(1, dataValueChangelogService.getChangelogEntries(params).size()); params = - new DataValueChangelogQueryParams() - .setDataElements(List.of(UID.of(dataElementA), UID.of(dataElementB))) - .setPeriods(List.of(periodA, periodB)) - .setOrgUnits(List.of(UID.of(orgUnitA), UID.of(orgUnitB))) - .setCategoryOptionCombo(UID.of(optionCombo)) - .setTypes(List.of(DataValueChangelogType.CREATE)); + decodeInput( + DataValueChangelogQueryParams.class, + "de=%s&de=%s&pe=%s&pe=%s&ou=%s&ou=%s&co=%s&type=%s" + .formatted( + UID.of(dataElementA), + UID.of(dataElementB), + periodA, + periodB, + UID.of(orgUnitA), + UID.of(orgUnitB), + UID.of(optionCombo), + DataValueChangelogType.CREATE)); assertEquals(2, dataValueChangelogService.getChangelogEntries(params).size()); dataValueC.setValue("5"); addDataValues(dataValueC); - params = new DataValueChangelogQueryParams().setTypes(List.of(DataValueChangelogType.UPDATE)); + params = DataValueChangelogQueryParams.ofType(DataValueChangelogType.UPDATE); assertEquals(1, dataValueChangelogService.getChangelogEntries(params).size()); dataValueD.setDeleted(true); addDataValues(dataValueD); params = - new DataValueChangelogQueryParams() - .setTypes(List.of(DataValueChangelogType.UPDATE, DataValueChangelogType.DELETE)); + DataValueChangelogQueryParams.ofType( + DataValueChangelogType.UPDATE, DataValueChangelogType.DELETE); assertEquals(2, dataValueChangelogService.getChangelogEntries(params).size()); } @Test void testGetDataValueAuditNoResult() { DataValueChangelogQueryParams params = - new DataValueChangelogQueryParams() - .setDataElements(List.of(UID.of(dataElementA))) - .setPeriods(List.of(periodD)) - .setOrgUnits(List.of(UID.of(orgUnitA))) - .setCategoryOptionCombo(UID.of(optionCombo)) - .setTypes(List.of(DataValueChangelogType.DELETE)); + decodeInput( + DataValueChangelogQueryParams.class, + Map.of( + "de", UID.of(dataElementA), + "pe", periodD, + "ou", UID.of(orgUnitA), + "co", UID.of(optionCombo), + "type", DataValueChangelogType.DELETE)); assertEquals(0, dataValueChangelogService.getChangelogEntries(params).size()); } diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/datavalue/DataValueChangelogStoreTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/datavalue/DataValueChangelogStoreTest.java index 33d063266d72..87be1310f9f3 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/datavalue/DataValueChangelogStoreTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/datavalue/DataValueChangelogStoreTest.java @@ -29,11 +29,13 @@ */ package org.hisp.dhis.datavalue; +import static org.hisp.dhis.common.input.InputUtils.decodeInput; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; import java.util.List; +import java.util.Map; import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.category.CategoryService; import org.hisp.dhis.common.IdentifiableObjectManager; @@ -112,15 +114,15 @@ void testAddGetDataValueAuditFromDataValue() { // state before delete List dvaCoc1Before = dataValueChangelogStore.getEntries( - new DataValueChangelogQueryParams().setCategoryOptionCombo(UID.of(coc1))); + decodeInput(DataValueChangelogQueryParams.class, Map.of("co", UID.of(coc1)))); List dvaCoc2Before = dataValueChangelogStore.getEntries( - new DataValueChangelogQueryParams().setAttributeOptionCombo(UID.of(coc2))); + decodeInput(DataValueChangelogQueryParams.class, Map.of("cc", UID.of(coc2)))); List dvaCoc3Before = dataValueChangelogStore.getEntries( - new DataValueChangelogQueryParams() - .setCategoryOptionCombo(UID.of(coc3)) - .setAttributeOptionCombo(UID.of(coc3))); + decodeInput( + DataValueChangelogQueryParams.class, + Map.of("co", UID.of(coc3), "cc", UID.of(coc3)))); assertEquals(2, dvaCoc1Before.size(), "There should be 2 audits referencing Cat Opt Combo 1"); assertEquals(2, dvaCoc2Before.size(), "There should be 2 audits referencing Cat Opt Combo 2"); @@ -133,15 +135,15 @@ void testAddGetDataValueAuditFromDataValue() { // then List dvaCoc1After = dataValueChangelogStore.getEntries( - new DataValueChangelogQueryParams().setCategoryOptionCombo(UID.of(coc1))); + decodeInput(DataValueChangelogQueryParams.class, Map.of("co", UID.of(coc1)))); List dvaCoc2After = dataValueChangelogStore.getEntries( - new DataValueChangelogQueryParams().setAttributeOptionCombo(UID.of(coc2))); + decodeInput(DataValueChangelogQueryParams.class, Map.of("cc", UID.of(coc2)))); List dvaCoc3After = dataValueChangelogStore.getEntries( - new DataValueChangelogQueryParams() - .setCategoryOptionCombo(UID.of(coc3)) - .setAttributeOptionCombo(UID.of(coc3))); + decodeInput( + DataValueChangelogQueryParams.class, + Map.of("co", UID.of(coc3), "cc", UID.of(coc3)))); assertTrue(dvaCoc1After.isEmpty(), "There should be 0 audits referencing Cat Opt Combo 1"); assertTrue(dvaCoc2After.isEmpty(), "There should be 0 audits referencing Cat Opt Combo 2"); diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/category/CategoryOptionComboMergeServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/category/CategoryOptionComboMergeServiceTest.java index 40622dd5d560..74fe04122095 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/category/CategoryOptionComboMergeServiceTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/category/CategoryOptionComboMergeServiceTest.java @@ -29,6 +29,7 @@ */ package org.hisp.dhis.merge.category; +import static org.hisp.dhis.common.input.InputUtils.decodeInput; import static org.hisp.dhis.dataapproval.DataApprovalAction.APPROVE; import static org.hisp.dhis.feedback.ErrorCode.E1540; import static org.hisp.dhis.tracker.test.TrackerTestBase.createEnrollment; @@ -45,6 +46,7 @@ import java.util.Date; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import org.hisp.dhis.category.CategoryCombo; @@ -2272,7 +2274,7 @@ private DataApproval createDataApproval( } private DataValueChangelogQueryParams getQueryParams(CategoryOptionCombo coc) { - return new DataValueChangelogQueryParams().setCategoryOptionCombo(UID.of(coc)); + return decodeInput(DataValueChangelogQueryParams.class, Map.of("co", UID.of(coc))); } private DataValue addDataValue(DataValue value) { diff --git a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/dataelement/DataElementMergeServiceTest.java b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/dataelement/DataElementMergeServiceTest.java index b27f0435f519..d5b7f866a756 100644 --- a/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/dataelement/DataElementMergeServiceTest.java +++ b/dhis-2/dhis-test-integration/src/test/java/org/hisp/dhis/merge/dataelement/DataElementMergeServiceTest.java @@ -31,6 +31,7 @@ import static org.hisp.dhis.changelog.ChangeLogType.CREATE; import static org.hisp.dhis.common.IdentifiableObjectUtils.getUidsNonNull; +import static org.hisp.dhis.common.input.InputUtils.decodeInput; import static org.hisp.dhis.security.acl.AccessStringHelper.READ_ONLY; import static org.hisp.dhis.tracker.test.TrackerTestBase.createEnrollment; import static org.hisp.dhis.tracker.test.TrackerTestBase.createEvent; @@ -3093,9 +3094,16 @@ private DataEntryValue.Input createDataValue(DataElement de, String value, Perio private DataValueChangelogQueryParams getQueryParams( List dataElements, List periods) { - return new DataValueChangelogQueryParams() - .setDataElements(dataElements.stream().map(DataElement::getUid).map(UID::of).toList()) - .setPeriods(periods); + return decodeInput( + DataValueChangelogQueryParams.class, + name -> + switch (name) { + case "de" -> + dataElements.stream().map(DataElement::getUid).toArray(String[]::new); + case "pe" -> periods.stream().map(Period::getIsoDate).toArray(String[]::new); + default -> null; + }) + .to(DataValueChangelogQueryParams.class); } private void assertMergeSuccessfulSourcesNotDeleted( diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AuditController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AuditController.java index 764b0787f11d..b88e9e3a9ab9 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AuditController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AuditController.java @@ -56,12 +56,9 @@ import org.hisp.dhis.dataapproval.DataApprovalAuditService; import org.hisp.dhis.dataapproval.DataApprovalLevel; import org.hisp.dhis.dataapproval.DataApprovalWorkflow; -import org.hisp.dhis.dataelement.DataElement; -import org.hisp.dhis.dataset.DataSet; import org.hisp.dhis.datavalue.DataValueChangelog; import org.hisp.dhis.datavalue.DataValueChangelogQueryParams; import org.hisp.dhis.datavalue.DataValueChangelogService; -import org.hisp.dhis.datavalue.DataValueChangelogType; import org.hisp.dhis.dxf2.webmessage.WebMessageException; import org.hisp.dhis.dxf2.webmessage.responses.FileResourceWebMessageResponse; import org.hisp.dhis.external.conf.ConfigurationKey; @@ -156,59 +153,22 @@ public void getFileAudit( } @GetMapping("dataValue") - public RootNode getAggregateDataValueChangelog( - @OpenApi.Param({UID[].class, DataSet.class}) @RequestParam(required = false) List ds, - @OpenApi.Param({UID[].class, DataElement.class}) @RequestParam(required = false) List de, - @OpenApi.Param(Period[].class) @RequestParam(required = false) List pe, - @OpenApi.Param({UID[].class, OrganisationUnit.class}) @RequestParam(required = false) - List ou, - @OpenApi.Param({UID.class, CategoryOptionCombo.class}) @RequestParam(required = false) UID co, - @OpenApi.Param({UID.class, CategoryOptionCombo.class}) @RequestParam(required = false) UID cc, - @RequestParam(name = "auditType", required = false) List type, - @RequestParam(required = false) Boolean skipPaging, - @RequestParam(required = false) Boolean paging, - @RequestParam(required = false, defaultValue = "50") int pageSize, - @RequestParam(required = false, defaultValue = "1") int page) { - List fields = Lists.newArrayList(contextService.getParameterValues("fields")); + public RootNode getAggregateDataValueChangelog(DataValueChangelogQueryParams params) { + List fields = params.fields(); if (fields.isEmpty()) { fields.addAll(FieldPreset.ALL.getFields()); } - List periods = getPeriods(pe); - List types = emptyIfNull(type); - - DataValueChangelogQueryParams params = - new DataValueChangelogQueryParams() - .setDataSets(ds) - .setDataElements(de) - .setPeriods(periods) - .setOrgUnits(ou) - .setCategoryOptionCombo(co) - .setAttributeOptionCombo(cc) - .setTypes(types); - List entries; Pager pager = null; - if (PagerUtils.isSkipPaging(skipPaging, paging)) { + if (!params.isPaged()) { entries = dataValueChangelogService.getChangelogEntries(params); } else { int total = dataValueChangelogService.countEntries(params); - - pager = new Pager(page, total, pageSize); - - entries = - dataValueChangelogService.getChangelogEntries( - new DataValueChangelogQueryParams() - .setDataSets(ds) - .setDataElements(de) - .setPeriods(periods) - .setOrgUnits(ou) - .setCategoryOptionCombo(co) - .setAttributeOptionCombo(cc) - .setTypes(types) - .setPager(pager)); + pager = new Pager(params.page(), total, params.pageSize()); + entries = dataValueChangelogService.getChangelogEntries(params); } RootNode rootNode = NodeUtils.createMetadata(); diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java index d03a41ae733f..158c9f96b39a 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java @@ -29,10 +29,11 @@ */ package org.hisp.dhis.webapi.mvc; +import static org.hisp.dhis.common.input.InputUtils.decodeInput; +import static org.hisp.dhis.common.input.InputUtils.validateInput; + import javax.annotation.Nonnull; -import lombok.RequiredArgsConstructor; import org.hisp.dhis.common.UrlParams; -import org.hisp.dhis.common.validation.InputValidationService; import org.hisp.dhis.jsontree.JsonObject; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; @@ -41,12 +42,17 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; +/** + * Adapter to map {@link Record}-classes endpoint method parameters that implement {@link + * UrlParams}. This is so this is an opt-in features to not break existing usages of record types in + * that position. + * + * @author Jan Bernitt + * @since 2.44 + */ @Component -@RequiredArgsConstructor public class UrlParamsMethodArgumentResolver implements HandlerMethodArgumentResolver { - private final InputValidationService inputValidationService; - @Override public boolean supportsParameter(MethodParameter parameter) { Class type = parameter.getParameterType(); @@ -64,8 +70,8 @@ public Object resolveArgument( @SuppressWarnings("unchecked") Class schema = (Class) parameter.getParameterType(); - JsonObject params = inputValidationService.decode(schema, request::getParameterValues); - inputValidationService.validate(schema, params); + JsonObject params = decodeInput(schema, request::getParameterValues); + validateInput(schema, params); return params.to(schema); } } From 322b9ad4a6c32d46c9c5eff82033336c538f3c66 Mon Sep 17 00:00:00 2001 From: Jan Bernitt Date: Wed, 13 May 2026 14:05:50 +0200 Subject: [PATCH 07/12] chore: extracted Paged proeprties --- .../org/hisp/dhis/common/input/Paged.java | 54 +++++++++++++++++++ .../dhis/common/{ => input}/UrlParams.java | 2 +- .../DataValueChangelogQueryParams.java | 20 ++----- .../dhis/minmax/MinMaxDataElementParams.java | 26 +++------ .../HibernateDataValueChangelogStore.java | 6 ++- .../HibernateMinMaxDataElementStore.java | 10 ++-- .../webapi/controller/AuditController.java | 6 ++- .../MinMaxDataElementController.java | 6 ++- .../mvc/UrlParamsMethodArgumentResolver.java | 2 +- 9 files changed, 84 insertions(+), 48 deletions(-) create mode 100644 dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/Paged.java rename dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/{ => input}/UrlParams.java (98%) diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/Paged.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/Paged.java new file mode 100644 index 000000000000..6f08d44b1128 --- /dev/null +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/Paged.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2004-2026, 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: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. 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. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.common.input; + +import static org.hisp.dhis.jsontree.Validation.YesNo.NO; + +import org.hisp.dhis.common.OpenApi; +import org.hisp.dhis.jsontree.Validation; + +public record Paged( + Boolean skipPaging, + @Validation(required = NO) boolean paging, + @Validation(required = NO, minimum = 1) int page, + @Validation(required = NO, minimum = 1, maximum = 1000) int pageSize) { + + public static final Paged DEFAULT = new Paged(null, true, 1, 50); + + @OpenApi.Ignore + public boolean isPaged() { + if (skipPaging != null) return !skipPaging; + return paging; + } + + public int offset() { + return (page - 1) * pageSize; + } +} diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/UrlParams.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/UrlParams.java similarity index 98% rename from dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/UrlParams.java rename to dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/UrlParams.java index 044c3f32d390..7c0f86a184de 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/UrlParams.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/UrlParams.java @@ -27,7 +27,7 @@ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package org.hisp.dhis.common; +package org.hisp.dhis.common.input; /** * Marker interface to be implemented by {@link Record} classes to be mapped via json-tree. diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueChangelogQueryParams.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueChangelogQueryParams.java index 049a8f937cbc..3f7bcf136749 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueChangelogQueryParams.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueChangelogQueryParams.java @@ -33,9 +33,11 @@ import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.common.UID; -import org.hisp.dhis.common.UrlParams; +import org.hisp.dhis.common.input.Paged; +import org.hisp.dhis.common.input.UrlParams; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.dataset.DataSet; +import org.hisp.dhis.jsontree.Collapsed; import org.hisp.dhis.organisationunit.OrganisationUnit; import org.hisp.dhis.period.Period; @@ -53,10 +55,7 @@ public record DataValueChangelogQueryParams( @OpenApi.Property({UID[].class, CategoryOptionCombo.class}) UID co, // COC @OpenApi.Property({UID[].class, CategoryOptionCombo.class}) UID cc, // AOC List type, - Boolean skipPaging, - boolean paging, - int page, - int pageSize) + @Collapsed Paged paged) implements UrlParams { public static final DataValueChangelogQueryParams DEFAULT = ofType(); @@ -71,15 +70,6 @@ public static DataValueChangelogQueryParams ofType(DataValueChangelogType... typ null, null, List.of(types), - null, - true, - 1, - 50); - } - - @OpenApi.Ignore - public boolean isPaged() { - if (skipPaging != null) return !skipPaging; - return paging; + Paged.DEFAULT); } } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementParams.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementParams.java index b2807af7c011..59f814d7e542 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementParams.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementParams.java @@ -29,35 +29,21 @@ */ package org.hisp.dhis.minmax; -import static org.hisp.dhis.jsontree.Validation.YesNo.NO; - import java.util.List; -import org.hisp.dhis.common.OpenApi; -import org.hisp.dhis.common.UrlParams; -import org.hisp.dhis.jsontree.Validation; +import org.hisp.dhis.common.input.Paged; +import org.hisp.dhis.common.input.UrlParams; +import org.hisp.dhis.jsontree.Collapsed; /** * @author Viet Nguyen */ public record MinMaxDataElementParams( - List fields, - List filters, - Boolean skipPaging, - @Validation(required = NO) boolean paging, - @Validation(required = NO, minimum = 1) int page, - @Validation(required = NO, minimum = 1, maximum = 1000) int pageSize) - implements UrlParams { + List fields, List filters, @Collapsed Paged paged) implements UrlParams { public static final MinMaxDataElementParams DEFAULT = - new MinMaxDataElementParams(List.of(), List.of(), null, true, 1, 50); + new MinMaxDataElementParams(List.of(), List.of(), Paged.DEFAULT); public MinMaxDataElementParams(List filters) { - this(List.of(), filters, null, true, 1, 50); - } - - @OpenApi.Ignore - public boolean isPaged() { - if (skipPaging != null) return !skipPaging; - return paging; + this(List.of(), filters, Paged.DEFAULT); } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueChangelogStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueChangelogStore.java index 4bf07f1e9902..499ff75d575f 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueChangelogStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueChangelogStore.java @@ -35,6 +35,7 @@ import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import org.hisp.dhis.common.UID; +import org.hisp.dhis.common.input.Paged; import org.hisp.dhis.datavalue.DataValueChangelog; import org.hisp.dhis.datavalue.DataValueChangelogEntry; import org.hisp.dhis.datavalue.DataValueChangelogQueryParams; @@ -129,6 +130,7 @@ static QueryBuilder createEntriesQuery(DataValueChangelogQueryParams params, SQL AND pe.iso = ANY(:pe) ORDER BY dva.created DESC"""; + Paged paged = params.paged(); return SQL.of(sql, api) .setParameter("types", params.type(), DataValueChangelogType::name) .setParameter("pe", params.pe(), Period::getIsoDate) @@ -137,8 +139,8 @@ static QueryBuilder createEntriesQuery(DataValueChangelogQueryParams params, SQL .setParameter("ou", params.ou()) .setParameter("coc", params.co()) .setParameter("aoc", params.cc()) - .setOffset(params.isPaged() ? (params.page() - 1) * params.pageSize() : null) - .setLimit(params.isPaged() ? params.pageSize() : null) + .setOffset(paged.isPaged() ? paged.offset() : null) + .setLimit(paged.isPaged() ? paged.pageSize() : null) .eraseNullParameterLines() .useEqualsOverInForParameters("types", "ds", "de", "ou", "pe") .eraseNullParameterJoinLine("de", "de") diff --git a/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/hibernate/HibernateMinMaxDataElementStore.java b/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/hibernate/HibernateMinMaxDataElementStore.java index 197d45ac4e9e..613d242be62d 100644 --- a/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/hibernate/HibernateMinMaxDataElementStore.java +++ b/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/hibernate/HibernateMinMaxDataElementStore.java @@ -48,6 +48,7 @@ import org.hibernate.Session; import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.common.UID; +import org.hisp.dhis.common.input.Paged; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.hibernate.HibernateGenericStore; import org.hisp.dhis.hibernate.JpaQueryParameters; @@ -139,11 +140,10 @@ public List query(MinMaxDataElementParams query) { JpaQueryParameters parameters = newJpaParameters(); parameters.addPredicate(root -> parseFilter(builder, root, query.filters())); - if (query.isPaged()) { - int pageSize = query.pageSize(); - int offset = (query.page() - 1) * pageSize; - parameters.setFirstResult(offset); - parameters.setMaxResults(pageSize); + Paged paged = query.paged(); + if (paged.isPaged()) { + parameters.setFirstResult(paged.offset()); + parameters.setMaxResults(paged.pageSize()); } return getList(builder, parameters); diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AuditController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AuditController.java index b88e9e3a9ab9..eb6ab0c80f25 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AuditController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AuditController.java @@ -51,6 +51,7 @@ import org.hisp.dhis.common.Pager; import org.hisp.dhis.common.PagerUtils; import org.hisp.dhis.common.UID; +import org.hisp.dhis.common.input.Paged; import org.hisp.dhis.dataapproval.DataApprovalAudit; import org.hisp.dhis.dataapproval.DataApprovalAuditQueryParams; import org.hisp.dhis.dataapproval.DataApprovalAuditService; @@ -163,11 +164,12 @@ public RootNode getAggregateDataValueChangelog(DataValueChangelogQueryParams par List entries; Pager pager = null; - if (!params.isPaged()) { + Paged paged = params.paged(); + if (!paged.isPaged()) { entries = dataValueChangelogService.getChangelogEntries(params); } else { int total = dataValueChangelogService.countEntries(params); - pager = new Pager(params.page(), total, params.pageSize()); + pager = new Pager(paged.page(), total, paged.pageSize()); entries = dataValueChangelogService.getChangelogEntries(params); } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MinMaxDataElementController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MinMaxDataElementController.java index 17842dfb42e9..7dbef7c05016 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MinMaxDataElementController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MinMaxDataElementController.java @@ -44,6 +44,7 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.common.Pager; import org.hisp.dhis.common.UID; +import org.hisp.dhis.common.input.Paged; import org.hisp.dhis.csv.CSV; import org.hisp.dhis.datavalue.DataValue; import org.hisp.dhis.feedback.BadRequestException; @@ -103,9 +104,10 @@ public class MinMaxDataElementController { RootNode rootNode = NodeUtils.createMetadata(); - if (query.isPaged()) { + Paged paged = query.paged(); + if (paged.isPaged()) { int total = minMaxService.countMinMaxDataElements(query); - Pager pager = new Pager(query.page(), total, query.pageSize()); + Pager pager = new Pager(paged.page(), total, paged.pageSize()); rootNode.addChild(NodeUtils.createPager(pager)); } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java index 158c9f96b39a..fccd3ccc0d3e 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java @@ -33,7 +33,7 @@ import static org.hisp.dhis.common.input.InputUtils.validateInput; import javax.annotation.Nonnull; -import org.hisp.dhis.common.UrlParams; +import org.hisp.dhis.common.input.UrlParams; import org.hisp.dhis.jsontree.JsonObject; import org.springframework.core.MethodParameter; import org.springframework.stereotype.Component; From 68eea5c2aaa2a0a0132a594b7001a17de9f79807 Mon Sep 17 00:00:00 2001 From: Jan Bernitt Date: Wed, 13 May 2026 14:29:26 +0200 Subject: [PATCH 08/12] fix: avoid web dependency --- .../org/hisp/dhis/common/input/InputUtils.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java index be3d9509b575..f345cea6329a 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java @@ -29,6 +29,8 @@ */ package org.hisp.dhis.common.input; +import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -44,8 +46,6 @@ import org.hisp.dhis.jsontree.Text; import org.hisp.dhis.jsontree.Validation; import org.hisp.dhis.period.Period; -import org.springframework.util.MultiValueMap; -import org.springframework.web.util.UriComponentsBuilder; /** * Utilities around generic input decoding and formal (static context) validation. @@ -103,12 +103,17 @@ public static T decodeInput( @Nonnull public static T decodeInput( @Nonnull Class schema, @Nonnull String properties) { - MultiValueMap params = - UriComponentsBuilder.fromUriString("?" + properties).build().getQueryParams(); + Map> map = new HashMap<>(); + for (String p : properties.split("&")) { + int eqIndex = p.indexOf('='); + String name = p.substring(0, eqIndex); + String value = p.substring(eqIndex + 1); + map.computeIfAbsent(name, key -> new ArrayList<>()).add(value); + } return decodeInput( schema, name -> { - List values = params.get(name); + List values = map.get(name); return values == null ? null : values.toArray(String[]::new); }) .to(schema); From 6be364cb646a124b27fed67830878359f2e53198 Mon Sep 17 00:00:00 2001 From: Jan Bernitt Date: Wed, 13 May 2026 16:15:31 +0200 Subject: [PATCH 09/12] fix: sonar issues --- .../hisp/dhis/common/input/InputUtils.java | 4 +++- .../input/{Paged.java => PagedParams.java} | 14 +++++++++-- .../DataValueChangelogQueryParams.java | 6 ++--- .../dhis/minmax/MinMaxDataElementParams.java | 8 +++---- .../HibernateDataValueChangelogStore.java | 4 ++-- .../HibernateMinMaxDataElementStore.java | 4 ++-- .../test/webapi/json/domain/JsonSchema.java | 8 +++++-- .../webapi/controller/AuditController.java | 24 ++----------------- .../MinMaxDataElementController.java | 4 ++-- .../mvc/UrlParamsMethodArgumentResolver.java | 5 ++-- dhis-2/pom.xml | 2 +- 11 files changed, 40 insertions(+), 43 deletions(-) rename dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/{Paged.java => PagedParams.java} (83%) diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java index f345cea6329a..c94e0c773068 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java @@ -36,6 +36,8 @@ import java.util.Set; import java.util.function.Function; import javax.annotation.Nonnull; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; import org.hisp.dhis.common.UID; import org.hisp.dhis.feedback.BadRequestException; import org.hisp.dhis.jsontree.JsonAccess; @@ -53,6 +55,7 @@ * @author Jan Bernitt * @since 2.44 */ +@NoArgsConstructor(access = AccessLevel.PRIVATE) public final class InputUtils { static { @@ -78,7 +81,6 @@ public static void validateInput(@Nonnull Class schema, @Nonnull JsonObject i throws BadRequestException { Validation.Result result = input.validate(schema, Validation.Mode.PROBE); if (!result.errors().isEmpty()) { - // TODO do we want to have different error codes per validation Rule? Validation.Error e0 = result.errors().get(0); throw new BadRequestException( "URL parameter `" diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/Paged.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/PagedParams.java similarity index 83% rename from dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/Paged.java rename to dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/PagedParams.java index 6f08d44b1128..dbd1e2fcef5d 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/Paged.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/PagedParams.java @@ -34,13 +34,23 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.jsontree.Validation; -public record Paged( +/** + * URL parameters for endpoints that offer paging. + * + *

Include via @{@link org.hisp.dhis.jsontree.Collapsed}. + * + * @param skipPaging override to {@link #paging()} to skip paging + * @param paging paging on/off (default on) + * @param page page no to show (default 1) + * @param pageSize entries per page (default 50) + */ +public record PagedParams( Boolean skipPaging, @Validation(required = NO) boolean paging, @Validation(required = NO, minimum = 1) int page, @Validation(required = NO, minimum = 1, maximum = 1000) int pageSize) { - public static final Paged DEFAULT = new Paged(null, true, 1, 50); + public static final PagedParams DEFAULT = new PagedParams(null, true, 1, 50); @OpenApi.Ignore public boolean isPaged() { diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueChangelogQueryParams.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueChangelogQueryParams.java index 3f7bcf136749..197703a0bb09 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueChangelogQueryParams.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/datavalue/DataValueChangelogQueryParams.java @@ -33,7 +33,7 @@ import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.common.UID; -import org.hisp.dhis.common.input.Paged; +import org.hisp.dhis.common.input.PagedParams; import org.hisp.dhis.common.input.UrlParams; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.dataset.DataSet; @@ -55,7 +55,7 @@ public record DataValueChangelogQueryParams( @OpenApi.Property({UID[].class, CategoryOptionCombo.class}) UID co, // COC @OpenApi.Property({UID[].class, CategoryOptionCombo.class}) UID cc, // AOC List type, - @Collapsed Paged paged) + @Collapsed PagedParams paged) implements UrlParams { public static final DataValueChangelogQueryParams DEFAULT = ofType(); @@ -70,6 +70,6 @@ public static DataValueChangelogQueryParams ofType(DataValueChangelogType... typ null, null, List.of(types), - Paged.DEFAULT); + PagedParams.DEFAULT); } } diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementParams.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementParams.java index 59f814d7e542..98fa6b503583 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementParams.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementParams.java @@ -30,7 +30,7 @@ package org.hisp.dhis.minmax; import java.util.List; -import org.hisp.dhis.common.input.Paged; +import org.hisp.dhis.common.input.PagedParams; import org.hisp.dhis.common.input.UrlParams; import org.hisp.dhis.jsontree.Collapsed; @@ -38,12 +38,12 @@ * @author Viet Nguyen */ public record MinMaxDataElementParams( - List fields, List filters, @Collapsed Paged paged) implements UrlParams { + List fields, List filters, @Collapsed PagedParams paged) implements UrlParams { public static final MinMaxDataElementParams DEFAULT = - new MinMaxDataElementParams(List.of(), List.of(), Paged.DEFAULT); + new MinMaxDataElementParams(List.of(), List.of(), PagedParams.DEFAULT); public MinMaxDataElementParams(List filters) { - this(List.of(), filters, Paged.DEFAULT); + this(List.of(), filters, PagedParams.DEFAULT); } } diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueChangelogStore.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueChangelogStore.java index 499ff75d575f..88e35398dc38 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueChangelogStore.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/datavalue/hibernate/HibernateDataValueChangelogStore.java @@ -35,7 +35,7 @@ import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import org.hisp.dhis.common.UID; -import org.hisp.dhis.common.input.Paged; +import org.hisp.dhis.common.input.PagedParams; import org.hisp.dhis.datavalue.DataValueChangelog; import org.hisp.dhis.datavalue.DataValueChangelogEntry; import org.hisp.dhis.datavalue.DataValueChangelogQueryParams; @@ -130,7 +130,7 @@ static QueryBuilder createEntriesQuery(DataValueChangelogQueryParams params, SQL AND pe.iso = ANY(:pe) ORDER BY dva.created DESC"""; - Paged paged = params.paged(); + PagedParams paged = params.paged(); return SQL.of(sql, api) .setParameter("types", params.type(), DataValueChangelogType::name) .setParameter("pe", params.pe(), Period::getIsoDate) diff --git a/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/hibernate/HibernateMinMaxDataElementStore.java b/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/hibernate/HibernateMinMaxDataElementStore.java index 613d242be62d..8138a4161816 100644 --- a/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/hibernate/HibernateMinMaxDataElementStore.java +++ b/dhis-2/dhis-services/dhis-service-validation/src/main/java/org/hisp/dhis/minmax/hibernate/HibernateMinMaxDataElementStore.java @@ -48,7 +48,7 @@ import org.hibernate.Session; import org.hisp.dhis.category.CategoryOptionCombo; import org.hisp.dhis.common.UID; -import org.hisp.dhis.common.input.Paged; +import org.hisp.dhis.common.input.PagedParams; import org.hisp.dhis.dataelement.DataElement; import org.hisp.dhis.hibernate.HibernateGenericStore; import org.hisp.dhis.hibernate.JpaQueryParameters; @@ -140,7 +140,7 @@ public List query(MinMaxDataElementParams query) { JpaQueryParameters parameters = newJpaParameters(); parameters.addPredicate(root -> parseFilter(builder, root, query.filters())); - Paged paged = query.paged(); + PagedParams paged = query.paged(); if (paged.isPaged()) { parameters.setFirstResult(paged.offset()); parameters.setMaxResults(paged.pageSize()); diff --git a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/webapi/json/domain/JsonSchema.java b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/webapi/json/domain/JsonSchema.java index 88aa692eb465..e6a3587b6bdd 100644 --- a/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/webapi/json/domain/JsonSchema.java +++ b/dhis-2/dhis-support/dhis-support-test/src/main/java/org/hisp/dhis/test/webapi/json/domain/JsonSchema.java @@ -33,6 +33,7 @@ import org.hisp.dhis.jsontree.JsonList; import org.hisp.dhis.jsontree.JsonObject; import org.hisp.dhis.jsontree.JsonURL; +import org.hisp.dhis.jsontree.Streamable; import org.hisp.dhis.security.AuthorityType; /** @@ -46,8 +47,11 @@ default Class getKlass() { return getString("klass").parsedClass(); } - default List> getReferences() { - return getArray("references").values().map(e -> e.parsedChecked(Class::forName)).toList(); + default List> getReferences() { + // compiler inference issue... + Streamable.Sized> tmp = + getArray("references").values().map(e -> e.parsedChecked(Class::forName)); + return tmp.toList(); } default String getRelativeApiEndpoint() { diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AuditController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AuditController.java index eb6ab0c80f25..84f669496370 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AuditController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AuditController.java @@ -37,7 +37,6 @@ import com.google.common.collect.Lists; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.List; @@ -51,7 +50,7 @@ import org.hisp.dhis.common.Pager; import org.hisp.dhis.common.PagerUtils; import org.hisp.dhis.common.UID; -import org.hisp.dhis.common.input.Paged; +import org.hisp.dhis.common.input.PagedParams; import org.hisp.dhis.dataapproval.DataApprovalAudit; import org.hisp.dhis.dataapproval.DataApprovalAuditQueryParams; import org.hisp.dhis.dataapproval.DataApprovalAuditService; @@ -75,7 +74,6 @@ import org.hisp.dhis.node.types.CollectionNode; import org.hisp.dhis.node.types.RootNode; import org.hisp.dhis.organisationunit.OrganisationUnit; -import org.hisp.dhis.period.Period; import org.hisp.dhis.trackedentity.TrackedEntityAuditQueryParams; import org.hisp.dhis.tracker.audit.TrackedEntityAudit; import org.hisp.dhis.tracker.audit.TrackedEntityAuditService; @@ -164,7 +162,7 @@ public RootNode getAggregateDataValueChangelog(DataValueChangelogQueryParams par List entries; Pager pager = null; - Paged paged = params.paged(); + PagedParams paged = params.paged(); if (!paged.isPaged()) { entries = dataValueChangelogService.getChangelogEntries(params); } else { @@ -301,22 +299,4 @@ public RootNode getTrackedEntityAudit( return rootNode; } - - // ----------------------------------------------------------------------------------------------------------------- - // Helpers - // ----------------------------------------------------------------------------------------------------------------- - - private List getPeriods(List isoPeriods) { - if (isoPeriods == null) { - return new ArrayList<>(); - } - - List periods = new ArrayList<>(); - - for (String pe : isoPeriods) { - periods.add(Period.of(pe)); - } - - return periods; - } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MinMaxDataElementController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MinMaxDataElementController.java index 7dbef7c05016..d63fcc97ca47 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MinMaxDataElementController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MinMaxDataElementController.java @@ -44,7 +44,7 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.common.Pager; import org.hisp.dhis.common.UID; -import org.hisp.dhis.common.input.Paged; +import org.hisp.dhis.common.input.PagedParams; import org.hisp.dhis.csv.CSV; import org.hisp.dhis.datavalue.DataValue; import org.hisp.dhis.feedback.BadRequestException; @@ -104,7 +104,7 @@ public class MinMaxDataElementController { RootNode rootNode = NodeUtils.createMetadata(); - Paged paged = query.paged(); + PagedParams paged = query.paged(); if (paged.isPaged()) { int total = minMaxService.countMinMaxDataElements(query); Pager pager = new Pager(paged.page(), total, paged.pageSize()); diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java index fccd3ccc0d3e..759dea32d2a0 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java @@ -33,6 +33,7 @@ import static org.hisp.dhis.common.input.InputUtils.validateInput; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.hisp.dhis.common.input.UrlParams; import org.hisp.dhis.jsontree.JsonObject; import org.springframework.core.MethodParameter; @@ -63,9 +64,9 @@ public boolean supportsParameter(MethodParameter parameter) { @Override public Object resolveArgument( @Nonnull MethodParameter parameter, - @Nonnull ModelAndViewContainer mavContainer, + @Nullable ModelAndViewContainer mavContainer, @Nonnull NativeWebRequest request, - @Nonnull WebDataBinderFactory binderFactory) + @Nullable WebDataBinderFactory binderFactory) throws Exception { @SuppressWarnings("unchecked") diff --git a/dhis-2/pom.xml b/dhis-2/pom.xml index 65a0f9e5a13f..1c82775a897c 100644 --- a/dhis-2/pom.xml +++ b/dhis-2/pom.xml @@ -96,7 +96,7 @@ 1.4.6 2.0.0 - 1.9.2 + 1.9.3 6.5.10 From bc13ed7b92e27f7dd4e5af5ac46ab10d9bd4a0b9 Mon Sep 17 00:00:00 2001 From: Jan Bernitt Date: Mon, 18 May 2026 11:48:58 +0200 Subject: [PATCH 10/12] chore: cleanup of pager creation and collapsed properties validation --- .../hisp/dhis/common/input/InputUtils.java | 2 +- .../hisp/dhis/common/input/PagedParams.java | 10 ++++++++ .../webapi/controller/AuditController.java | 24 ++++--------------- .../MinMaxDataElementController.java | 19 ++++----------- dhis-2/pom.xml | 2 +- 5 files changed, 21 insertions(+), 36 deletions(-) diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java index c94e0c773068..7785e736aae4 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java @@ -133,7 +133,7 @@ public static T decodeInput( @Nonnull public static JsonObject decodeInput( @Nonnull Class schema, @Nonnull Function propertyLookup) { - List properties = JsonObject.collapsedProperties(schema); + List properties = JsonObject.properties(schema); JsonNode object = JsonBuilder.createObject( obj -> { diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/PagedParams.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/PagedParams.java index dbd1e2fcef5d..31c10d3521c4 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/PagedParams.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/PagedParams.java @@ -31,7 +31,9 @@ import static org.hisp.dhis.jsontree.Validation.YesNo.NO; +import java.util.function.ToIntFunction; import org.hisp.dhis.common.OpenApi; +import org.hisp.dhis.common.Pager; import org.hisp.dhis.jsontree.Validation; /** @@ -61,4 +63,12 @@ public boolean isPaged() { public int offset() { return (page - 1) * pageSize; } + + public Pager toPager(int totalPages) { + return !isPaged() ? null : new Pager(page, totalPages, pageSize); + } + + public Pager toPager(T params, ToIntFunction count) { + return !isPaged() ? null : toPager(count.applyAsInt(params)); + } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AuditController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AuditController.java index 84f669496370..fe8901433741 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AuditController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AuditController.java @@ -50,7 +50,6 @@ import org.hisp.dhis.common.Pager; import org.hisp.dhis.common.PagerUtils; import org.hisp.dhis.common.UID; -import org.hisp.dhis.common.input.PagedParams; import org.hisp.dhis.dataapproval.DataApprovalAudit; import org.hisp.dhis.dataapproval.DataApprovalAuditQueryParams; import org.hisp.dhis.dataapproval.DataApprovalAuditService; @@ -155,28 +154,13 @@ public void getFileAudit( public RootNode getAggregateDataValueChangelog(DataValueChangelogQueryParams params) { List fields = params.fields(); - if (fields.isEmpty()) { - fields.addAll(FieldPreset.ALL.getFields()); - } + if (fields.isEmpty()) fields = FieldPreset.ALL.getFields(); - List entries; - Pager pager = null; - - PagedParams paged = params.paged(); - if (!paged.isPaged()) { - entries = dataValueChangelogService.getChangelogEntries(params); - } else { - int total = dataValueChangelogService.countEntries(params); - pager = new Pager(paged.page(), total, paged.pageSize()); - entries = dataValueChangelogService.getChangelogEntries(params); - } + List entries = dataValueChangelogService.getChangelogEntries(params); + Pager pager = params.paged().toPager(params, dataValueChangelogService::countEntries); RootNode rootNode = NodeUtils.createMetadata(); - - if (pager != null) { - rootNode.addChild(NodeUtils.createPager(pager)); - } - + if (pager != null) rootNode.addChild(NodeUtils.createPager(pager)); CollectionNode trackedEntityAttributeValueAudits = rootNode.addChild(new CollectionNode("dataValueAudits", true)); trackedEntityAttributeValueAudits.addChildren( diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MinMaxDataElementController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MinMaxDataElementController.java index d63fcc97ca47..393a098c1132 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MinMaxDataElementController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/MinMaxDataElementController.java @@ -44,7 +44,6 @@ import org.hisp.dhis.common.OpenApi; import org.hisp.dhis.common.Pager; import org.hisp.dhis.common.UID; -import org.hisp.dhis.common.input.PagedParams; import org.hisp.dhis.csv.CSV; import org.hisp.dhis.datavalue.DataValue; import org.hisp.dhis.feedback.BadRequestException; @@ -96,24 +95,16 @@ public class MinMaxDataElementController { throws QueryParserException { List fields = query.fields(); - if (fields.isEmpty()) { - fields = FieldPreset.ALL.getFields(); - } + if (fields.isEmpty()) fields = FieldPreset.ALL.getFields(); - List minMaxDataElements = minMaxService.getMinMaxDataElements(query); + List entries = minMaxService.getMinMaxDataElements(query); + Pager pager = query.paged().toPager(query, minMaxService::countMinMaxDataElements); RootNode rootNode = NodeUtils.createMetadata(); - - PagedParams paged = query.paged(); - if (paged.isPaged()) { - int total = minMaxService.countMinMaxDataElements(query); - Pager pager = new Pager(paged.page(), total, paged.pageSize()); - rootNode.addChild(NodeUtils.createPager(pager)); - } - + if (pager != null) rootNode.addChild(NodeUtils.createPager(pager)); rootNode.addChild( fieldFilterService.toCollectionNode( - MinMaxDataElement.class, new FieldFilterParams(minMaxDataElements, fields))); + MinMaxDataElement.class, new FieldFilterParams(entries, fields))); return rootNode; } diff --git a/dhis-2/pom.xml b/dhis-2/pom.xml index fda32bd07350..c17367c1dd92 100644 --- a/dhis-2/pom.xml +++ b/dhis-2/pom.xml @@ -96,7 +96,7 @@ 1.4.6 2.0.0 - 1.9.3 + 1.9.4 6.5.10 From 093dfc4a0c16629f5ae51e58c6701b1ed20c1214 Mon Sep 17 00:00:00 2001 From: Jan Bernitt Date: Thu, 21 May 2026 12:12:18 +0200 Subject: [PATCH 11/12] feat: add JURL handling to input decoding --- .../hisp/dhis/common/input/InputUtils.java | 40 ++++---- .../dhis/common/input/InputUtilsTest.java | 97 +++++++++++++++++++ 2 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 dhis-2/dhis-api/src/test/java/org/hisp/dhis/common/input/InputUtilsTest.java diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java index 7785e736aae4..f0eea250d93e 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java @@ -45,6 +45,7 @@ import org.hisp.dhis.jsontree.JsonMixed; import org.hisp.dhis.jsontree.JsonNode; import org.hisp.dhis.jsontree.JsonObject; +import org.hisp.dhis.jsontree.Jurl; import org.hisp.dhis.jsontree.Text; import org.hisp.dhis.jsontree.Validation; import org.hisp.dhis.period.Period; @@ -144,16 +145,8 @@ public static JsonObject decodeInput( if (values == null) continue; Set types = p.types(); if (types.isEmpty()) { - // default is string or array of string - if (values.length == 1) { - obj.addString(name, values[0]); - } else { - obj.addArray( - name, - arr -> { - for (String v : values) arr.addString(v); - }); - } + // default behaviour + addAutoComplex(name, values, obj); } else if (values.length == 0) { if (types.contains(Validation.NodeType.BOOLEAN)) { obj.addBoolean(name, true); @@ -167,15 +160,10 @@ public static JsonObject decodeInput( switch (type) { case INTEGER, NUMBER, BOOLEAN, NULL -> obj.addMember(name, JsonNode.of(values[0])); - case STRING, OBJECT -> + case STRING -> obj.addString( name, values.length == 1 ? values[0] : String.join(",", values)); - case ARRAY -> - obj.addArray( - name, - arr -> { - for (String v : values) arr.addString(v); - }); + case ARRAY, OBJECT -> addAutoComplex(name, values, obj); } } } @@ -183,4 +171,22 @@ public static JsonObject decodeInput( }); return JsonMixed.of(object); } + + private static void addAutoComplex( + Text name, String[] values, JsonBuilder.JsonObjectBuilder obj) { + if (values.length == 1) { + // assume JURL + if (values[0].startsWith("(")) { + obj.addMember(name, Jurl.of(values[0]).node()); + } else { + obj.addString(name, values[0]); + } + } else { + obj.addArray( + name, + arr -> { + for (String v : values) arr.addString(v); + }); + } + } } diff --git a/dhis-2/dhis-api/src/test/java/org/hisp/dhis/common/input/InputUtilsTest.java b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/common/input/InputUtilsTest.java new file mode 100644 index 000000000000..7acd467e0255 --- /dev/null +++ b/dhis-2/dhis-api/src/test/java/org/hisp/dhis/common/input/InputUtilsTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2004-2026, 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: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. 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. + * + * 3. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.hisp.dhis.common.input; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.Map; +import org.hisp.dhis.jsontree.JsonAccess; +import org.hisp.dhis.jsontree.JsonAccessException; +import org.hisp.dhis.jsontree.JsonMixed; +import org.hisp.dhis.jsontree.JsonObject; +import org.junit.jupiter.api.Test; + +/** + * Test for {@link InputUtils}, mostly testing a complex example of mapping with JURL involved. + * + * @author Jan Bernitt + */ +class InputUtilsTest { + + public record ImportParams(List filter) {} + + /* + This is just an example of how JURL could be used to model something similar to current filter= parameter + used in many of our endpoints + */ + public record Filter(String name, String transform, List children) {} + + static { + JsonAccess.GLOBAL.add( + Filter.class, + json -> { + if (json.isString()) return new Filter(json.to(String.class), null, List.of()); + if (!json.isObject()) + throw new JsonAccessException("Filter must be JSON string or object"); + + String name = json.names().get(0); + JsonMixed value = json.get(name); + if (value.isString()) return new Filter(name, value.string(), List.of()); + return new Filter(name, null, value.stream().map(e -> e.to(Filter.class)).toList()); + }); + } + + @Test + void testToJurl() { + Map urlParameters = + Map.of("filter", new String[] {"(name,id,(dataElements:(id,name)),(orgUnits:size))"}); + + JsonObject params = InputUtils.decodeInput(ImportParams.class, urlParameters::get); + + assertEquals( + """ + {"filter":["name","id",{"dataElements":["id","name"]},{"orgUnits":"size"}]}""", + params.toJson()); + + assertEquals( + new ImportParams( + List.of( + new Filter("name", null, List.of()), + new Filter("id", null, List.of()), + new Filter( + "dataElements", + null, + List.of( + new Filter("id", null, List.of()), new Filter("name", null, List.of()))), + new Filter("orgUnits", "size", List.of()))), + params.to(ImportParams.class)); + } +} From fef8d88808c3904f1d9b72fe032e7a608e99cef5 Mon Sep 17 00:00:00 2001 From: Jan Bernitt Date: Thu, 21 May 2026 13:37:26 +0200 Subject: [PATCH 12/12] fix: auto-wrap in array if that is the target type --- .../org/hisp/dhis/common/input/InputUtils.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java index f0eea250d93e..fc84b3c1ef88 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java @@ -146,7 +146,7 @@ public static JsonObject decodeInput( Set types = p.types(); if (types.isEmpty()) { // default behaviour - addAutoComplex(name, values, obj); + addAutoComplex(name, values, true, obj); } else if (values.length == 0) { if (types.contains(Validation.NodeType.BOOLEAN)) { obj.addBoolean(name, true); @@ -163,7 +163,7 @@ public static JsonObject decodeInput( case STRING -> obj.addString( name, values.length == 1 ? values[0] : String.join(",", values)); - case ARRAY, OBJECT -> addAutoComplex(name, values, obj); + case ARRAY, OBJECT -> addAutoComplex(name, values, false, obj); } } } @@ -173,13 +173,16 @@ public static JsonObject decodeInput( } private static void addAutoComplex( - Text name, String[] values, JsonBuilder.JsonObjectBuilder obj) { + Text name, String[] values, boolean allowString, JsonBuilder.JsonObjectBuilder obj) { if (values.length == 1) { // assume JURL - if (values[0].startsWith("(")) { - obj.addMember(name, Jurl.of(values[0]).node()); + String value = values[0]; + if (value.startsWith("(")) { + obj.addMember(name, Jurl.of(value).node()); + } else if (allowString) { + obj.addString(name, value); } else { - obj.addString(name, values[0]); + obj.addArray(name, arr -> arr.addString(value)); } } else { obj.addArray(