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
new file mode 100644
index 000000000000..fc84b3c1ef88
--- /dev/null
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/InputUtils.java
@@ -0,0 +1,195 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+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;
+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.Jurl;
+import org.hisp.dhis.jsontree.Text;
+import org.hisp.dhis.jsontree.Validation;
+import org.hisp.dhis.period.Period;
+
+/**
+ * Utilities around generic input decoding and formal (static context) validation.
+ *
+ * @author Jan Bernitt
+ * @since 2.44
+ */
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class InputUtils {
+
+ 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()) {
+ 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) {
+ 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 = map.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 extends Record> schema, @Nonnull Function propertyLookup) {
+ List properties = JsonObject.properties(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 (types.isEmpty()) {
+ // default behaviour
+ addAutoComplex(name, values, true, obj);
+ } else 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 ->
+ obj.addString(
+ name, values.length == 1 ? values[0] : String.join(",", values));
+ case ARRAY, OBJECT -> addAutoComplex(name, values, false, obj);
+ }
+ }
+ }
+ }
+ });
+ return JsonMixed.of(object);
+ }
+
+ private static void addAutoComplex(
+ Text name, String[] values, boolean allowString, JsonBuilder.JsonObjectBuilder obj) {
+ if (values.length == 1) {
+ // assume JURL
+ String value = values[0];
+ if (value.startsWith("(")) {
+ obj.addMember(name, Jurl.of(value).node());
+ } else if (allowString) {
+ obj.addString(name, value);
+ } else {
+ obj.addArray(name, arr -> arr.addString(value));
+ }
+ } else {
+ obj.addArray(
+ name,
+ arr -> {
+ for (String v : values) arr.addString(v);
+ });
+ }
+ }
+}
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
new file mode 100644
index 000000000000..31c10d3521c4
--- /dev/null
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/PagedParams.java
@@ -0,0 +1,74 @@
+/*
+ * 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 java.util.function.ToIntFunction;
+import org.hisp.dhis.common.OpenApi;
+import org.hisp.dhis.common.Pager;
+import org.hisp.dhis.jsontree.Validation;
+
+/**
+ * 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 PagedParams DEFAULT = new PagedParams(null, true, 1, 50);
+
+ @OpenApi.Ignore
+ public boolean isPaged() {
+ if (skipPaging != null) return !skipPaging;
+ return paging;
+ }
+
+ 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-api/src/main/java/org/hisp/dhis/common/input/UrlParams.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/UrlParams.java
new file mode 100644
index 000000000000..7c0f86a184de
--- /dev/null
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/common/input/UrlParams.java
@@ -0,0 +1,40 @@
+/*
+ * 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;
+
+/**
+ * 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-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..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
@@ -29,12 +29,16 @@
*/
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.input.PagedParams;
+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;
/**
@@ -42,20 +46,30 @@
*
* @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,
+ @Collapsed PagedParams paged)
+ 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),
+ 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
new file mode 100644
index 000000000000..98fa6b503583
--- /dev/null
+++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementParams.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2004-2022, University of Oslo
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 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.minmax;
+
+import java.util.List;
+import org.hisp.dhis.common.input.PagedParams;
+import org.hisp.dhis.common.input.UrlParams;
+import org.hisp.dhis.jsontree.Collapsed;
+
+/**
+ * @author Viet Nguyen
+ */
+public record MinMaxDataElementParams(
+ List fields, List filters, @Collapsed PagedParams paged) implements UrlParams {
+
+ public static final MinMaxDataElementParams DEFAULT =
+ new MinMaxDataElementParams(List.of(), List.of(), PagedParams.DEFAULT);
+
+ public MinMaxDataElementParams(List filters) {
+ this(List.of(), filters, PagedParams.DEFAULT);
+ }
+}
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/MinMaxDataElementQueryParams.java
deleted file mode 100644
index c4e2b4522791..000000000000
--- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/minmax/MinMaxDataElementQueryParams.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * Copyright (c) 2004-2022, University of Oslo
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 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.minmax;
-
-import com.google.common.base.MoreObjects;
-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;
-
-/**
- * @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 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 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;
- }
-
- @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();
- }
-}
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-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));
+ }
+}
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-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..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
@@ -34,8 +34,8 @@
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.common.input.PagedParams;
import org.hisp.dhis.datavalue.DataValueChangelog;
import org.hisp.dhis.datavalue.DataValueChangelogEntry;
import org.hisp.dhis.datavalue.DataValueChangelogQueryParams;
@@ -130,17 +130,17 @@ static QueryBuilder createEntriesQuery(DataValueChangelogQueryParams params, SQL
AND pe.iso = ANY(:pe)
ORDER BY dva.created DESC""";
- Pager pager = params.getPager();
+ PagedParams paged = params.paged();
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(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-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-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..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
@@ -47,13 +47,13 @@
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.common.input.PagedParams;
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 +134,29 @@ 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());
+ PagedParams paged = query.paged();
+ if (paged.isPaged()) {
+ parameters.setFirstResult(paged.offset());
+ parameters.setMaxResults(paged.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-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..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;
/**
@@ -47,15 +48,10 @@ default Class> getKlass() {
}
default List> getReferences() {
- return getArray("references")
- .values(
- klass -> {
- try {
- return Class.forName(klass);
- } catch (ClassNotFoundException ex) {
- throw new IllegalArgumentException(ex);
- }
- });
+ // 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-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 bb64598dc254..4a5bd8da9167 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;
@@ -3101,9 +3102,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-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-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
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..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
@@ -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;
@@ -56,12 +55,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;
@@ -77,7 +73,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;
@@ -156,67 +151,16 @@ 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"));
-
- 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)) {
- entries = dataValueChangelogService.getChangelogEntries(params);
- } else {
- int total = dataValueChangelogService.countEntries(params);
+ public RootNode getAggregateDataValueChangelog(DataValueChangelogQueryParams params) {
+ List fields = params.fields();
- pager = new Pager(page, total, pageSize);
+ if (fields.isEmpty()) fields = FieldPreset.ALL.getFields();
- entries =
- dataValueChangelogService.getChangelogEntries(
- new DataValueChangelogQueryParams()
- .setDataSets(ds)
- .setDataElements(de)
- .setPeriods(periods)
- .setOrgUnits(ou)
- .setCategoryOptionCombo(co)
- .setAttributeOptionCombo(cc)
- .setTypes(types)
- .setPager(pager));
- }
+ 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(
@@ -339,22 +283,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 70ecaf0bc51c..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
@@ -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,33 +87,24 @@
@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);
- if (fields.isEmpty()) {
- fields.addAll(FieldPreset.ALL.getFields());
- }
+ List fields = query.fields();
+ 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();
-
- if (!query.isSkipPaging()) {
- query.setTotal(minMaxService.countMinMaxDataElements(query));
- rootNode.addChild(NodeUtils.createPager(query.getPager()));
- }
-
+ 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/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..759dea32d2a0
--- /dev/null
+++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/mvc/UrlParamsMethodArgumentResolver.java
@@ -0,0 +1,78 @@
+/*
+ * 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 static org.hisp.dhis.common.input.InputUtils.decodeInput;
+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;
+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;
+
+/**
+ * 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
+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,
+ @Nullable ModelAndViewContainer mavContainer,
+ @Nonnull NativeWebRequest request,
+ @Nullable WebDataBinderFactory binderFactory)
+ throws Exception {
+
+ @SuppressWarnings("unchecked")
+ Class extends Record> schema = (Class extends Record>) parameter.getParameterType();
+ JsonObject params = decodeInput(schema, request::getParameterValues);
+ validateInput(schema, params);
+ return params.to(schema);
+ }
+}
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/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
diff --git a/dhis-2/pom.xml b/dhis-2/pom.xml
index 6acf2e626645..c17367c1dd92 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.4
6.5.10