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 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 schema = (Class) 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