diff --git a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/User.java b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/User.java index 1bb2880b6670..ef1e6cf92272 100644 --- a/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/User.java +++ b/dhis-2/dhis-api/src/main/java/org/hisp/dhis/user/User.java @@ -28,6 +28,7 @@ package org.hisp.dhis.user; import static org.apache.commons.collections4.CollectionUtils.emptyIfNull; +import static org.hisp.dhis.schema.annotation.Property.Value.FALSE; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; @@ -525,7 +526,7 @@ public void setPreviousPasswords(List previousPasswords) { @JsonProperty @JacksonXmlProperty(namespace = DxfNamespaces.DXF_2_0) - @Property(value = PropertyType.TEXT, required = Property.Value.FALSE) + @Property(value = PropertyType.TEXT, required = FALSE) public String getUsername() { return username; } @@ -702,10 +703,11 @@ public void updateOrganisationUnits(Set updates) { } } - /** Returns the concatenated first name and surname. */ @Override + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + @Property(required = FALSE, access = Property.Access.READ_ONLY) public String getName() { - return firstName + " " + surname; + return name; } /** diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/gist/GistPlanner.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/gist/GistPlanner.java index 7405b51759d2..411cd9f9edce 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/gist/GistPlanner.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/gist/GistPlanner.java @@ -62,7 +62,6 @@ import org.hisp.dhis.schema.RelativePropertyContext; import org.hisp.dhis.schema.Schema; import org.hisp.dhis.schema.annotation.Gist.Transform; -import org.hisp.dhis.user.User; /** * The {@link GistPlanner} is responsible to expand the list of {@link Field}s following the {@link @@ -90,7 +89,6 @@ private List planFields() { fields = withPresetFields(fields); // 1:n fields = withAttributeFields(fields); // 1:1 fields = withDisplayAsTranslatedFields(fields); // 1:1 - fields = withUserNameAsFromTransformedField(fields); // 1:1 fields = withInnerAsSeparateFields(fields); // 1:n fields = withCollectionItemPropertyAsTransformation(fields); // 1:1 fields = withEffectiveTransformation(fields); // 1:1 @@ -274,19 +272,6 @@ private List withDisplayAsTranslatedFields(List fields) { .withPropertyPath(pathOnSameParent(f.getPropertyPath(), "shortName"))); } - private List withUserNameAsFromTransformedField(List fields) { - return query.getElementType() != User.class - ? fields - : map1to1( - fields, - f -> f.getPropertyPath().equals("name"), - f -> - f.toBuilder() - .transformation(Transform.FROM) - .transformationArgument("firstName,surname") - .build()); - } - /** Transforms {@code field[a,b]} syntax to {@code field.a,field.b} */ private List withInnerAsSeparateFields(List fields) { List expanded = new ArrayList<>(); diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Restrictions.java b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Restrictions.java index 48bb0af8cd4b..0936831ddd6f 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Restrictions.java +++ b/dhis-2/dhis-services/dhis-service-core/src/main/java/org/hisp/dhis/query/Restrictions.java @@ -134,7 +134,12 @@ public static Restriction isEmpty(String path) { } public static Disjunction query(Schema schema, String query) { - return or(schema, eq("id", query), eq("code", query), ilike("name", query, MatchMode.ANYWHERE)); + Restriction name = ilike("name", query, MatchMode.ANYWHERE); + Restriction code = eq("code", query); + if (query.length() != 11) return or(schema, code, name); + // only a query with length 11 has a chance of matching a UID + Restriction id = eq("id", query); + return or(schema, id, code, name); } public static Disjunction or(Schema schema, Criterion... filters) { diff --git a/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/user/hibernate/User.hbm.xml b/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/user/hibernate/User.hbm.xml index 1a4fd3e4fea3..6b87436f7f73 100644 --- a/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/user/hibernate/User.hbm.xml +++ b/dhis-2/dhis-services/dhis-service-core/src/main/resources/org/hisp/dhis/user/hibernate/User.hbm.xml @@ -25,9 +25,11 @@ - + - + + + diff --git a/dhis-2/dhis-support/dhis-support-db-migration/src/main/resources/org/hisp/dhis/db/migration/2.42/V2_42_32__User_name_as_generated_column.sql b/dhis-2/dhis-support/dhis-support-db-migration/src/main/resources/org/hisp/dhis/db/migration/2.42/V2_42_32__User_name_as_generated_column.sql new file mode 100644 index 000000000000..09f777afa0c9 --- /dev/null +++ b/dhis-2/dhis-support/dhis-support-db-migration/src/main/resources/org/hisp/dhis/db/migration/2.42/V2_42_32__User_name_as_generated_column.sql @@ -0,0 +1,11 @@ +/* +Adds the "name" column to users as a computed column based on first and last name +to move named based filters into the DB. +This is important as the web API "query" search is a combination of multiple columns +one of which is name. Such combinations must stay all in DB to be correct. + +Since both source columns are not null the name can and should also be not null +and the computation does not need to handle null. +*/ +alter table userinfo + add column if not exists name character varying(321) not null generated always as ( firstname || ' ' || userinfo.surname ) stored; \ No newline at end of file diff --git a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/SchemaControllerTest.java b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/SchemaControllerTest.java index 8a93037b7cfd..0bf231cd1258 100644 --- a/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/SchemaControllerTest.java +++ b/dhis-2/dhis-test-web-api/src/test/java/org/hisp/dhis/webapi/controller/SchemaControllerTest.java @@ -33,10 +33,12 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Optional; import org.hisp.dhis.http.HttpStatus; import org.hisp.dhis.jsontree.JsonObject; import org.hisp.dhis.schema.PropertyType; import org.hisp.dhis.test.webapi.H2ControllerIntegrationTestBase; +import org.hisp.dhis.test.webapi.json.domain.JsonProperty; import org.hisp.dhis.test.webapi.json.domain.JsonSchema; import org.junit.jupiter.api.Test; import org.springframework.transaction.annotation.Transactional; @@ -130,4 +132,17 @@ void testAttributeWritable() { } }); } + + @Test + void testUserNameIsPersistedButReadOnly() { + JsonSchema user = GET("/schemas/user").content().as(JsonSchema.class); + Optional maybeName = + user.getProperties().stream().filter(p -> "name".equals(p.getName())).findFirst(); + assertTrue(maybeName.isPresent()); + JsonProperty name = maybeName.get(); + assertTrue(name.isPersisted()); + assertTrue(name.isReadable()); + assertFalse(name.isWritable()); + assertFalse(name.isRequired()); + } } diff --git a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyController.java b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyController.java index 2034dcf490dd..d0f6cad51d74 100644 --- a/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyController.java +++ b/dhis-2/dhis-web-api/src/main/java/org/hisp/dhis/webapi/controller/AbstractFullReadOnlyController.java @@ -71,6 +71,7 @@ import org.hisp.dhis.query.Criterion; import org.hisp.dhis.query.GetObjectListParams; import org.hisp.dhis.query.GetObjectParams; +import org.hisp.dhis.query.Junction; import org.hisp.dhis.query.Query; import org.hisp.dhis.query.QueryParserException; import org.hisp.dhis.query.QueryService; @@ -200,7 +201,11 @@ protected final ResponseEntity> getObjectListInternal( addProgrammaticModifiers(params); - boolean isAlwaysEmpty = additionalFilters.stream().anyMatch(Criterion::isAlwaysFalse); + // a top level restriction combined with AND that is always false always results in an empty + // list + boolean isAlwaysEmpty = + params.getRootJunction() == Junction.Type.AND + && additionalFilters.stream().anyMatch(Criterion::isAlwaysFalse); List entities = isAlwaysEmpty ? List.of() : getEntityList(params, additionalFilters); postProcessResponseEntities(entities, params);