diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java index 9bef72a4aaa..148f52ac002 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java @@ -38,6 +38,7 @@ import org.apache.syncope.core.persistence.api.entity.PlainAttrValue; import org.apache.syncope.core.persistence.api.entity.PlainSchema; import org.apache.syncope.core.provisioning.api.serialization.POJOHelper; +import org.springframework.data.domain.Sort; public class MariaDBJPAAnySearchDAO extends AbstractJPAAnySearchDAO { @@ -68,6 +69,35 @@ public MariaDBJPAAnySearchDAO( entityManager); } + @Override + protected void parseOrderByForPlainSchema( + final SearchSupport svs, + final OrderBySupport obs, + final OrderBySupport.Item item, + final Sort.Order clause, + final PlainSchema schema, + final String fieldName) { + + // keep track of involvement of non-mandatory schemas in the order by clauses + obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition()); + + obs.views.add(svs.field()); + + item.select = new StringBuilder(). + append("( SELECT usa").append('.').append(key(schema.getType())). + append(" FROM ").append(schema.isUniqueConstraint() + ? svs.asSearchViewSupport().uniqueAttr().name() + : svs.asSearchViewSupport().attr().name()). + append(" usa WHERE usa.any_id = "). + append(defaultSV(svs).alias()). + append(".any_id"). + append(" AND usa.schema_id ='").append(fieldName).append("'"). + append(" LIMIT 1"). + append(") AS ").append(fieldName).toString(); + item.where = "plainSchema = '" + fieldName + '\''; + item.orderBy = fieldName + ' ' + clause.getDirection().name(); + } + @Override protected Pair getQuery( final AttrCond cond, diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java index ca3b237fcb8..4a5e4970677 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java @@ -84,9 +84,17 @@ protected void parseOrderByForPlainSchema( obs.views.add(svs.field()); - item.select = svs.field().alias() + '.' - + (schema.isUniqueConstraint() ? "attrUniqueValue" : key(schema.getType())) - + " AS " + fieldName; + item.select = new StringBuilder().append(schema.isUniqueConstraint() + ? "( SELECT JSON_UNQUOTE(JSON_EXTRACT(usa.attrUniqueValue, '$. " + key(schema.getType()) + "')) " + : "( SELECT usa." + key(schema.getType())). + append(" FROM "). + append(svs.field().name()). + append(" usa WHERE usa.any_id = "). + append(svs.field().alias()). + append(".any_id"). + append(" AND usa.plainSchema ='").append(fieldName).append("'"). + append(" LIMIT 1"). + append(") AS ").append(fieldName).toString(); item.where = "plainSchema = '" + fieldName + '\''; item.orderBy = fieldName + ' ' + clause.getDirection().name(); } diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java index e1dc24f772c..b27d83fec63 100644 --- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java +++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java @@ -36,10 +36,12 @@ import org.apache.syncope.common.lib.SyncopeConstants; import org.apache.syncope.common.lib.types.AnyTypeKind; import org.apache.syncope.common.lib.types.IdRepoEntitlement; +import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.AnySearchDAO; import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO; import org.apache.syncope.core.persistence.api.dao.GroupDAO; +import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; import org.apache.syncope.core.persistence.api.dao.RealmDAO; import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO; import org.apache.syncope.core.persistence.api.dao.RoleDAO; @@ -56,6 +58,7 @@ import org.apache.syncope.core.persistence.api.dao.search.SearchCond; import org.apache.syncope.core.persistence.api.entity.AnyType; import org.apache.syncope.core.persistence.api.entity.PlainAttr; +import org.apache.syncope.core.persistence.api.entity.PlainSchema; import org.apache.syncope.core.persistence.api.entity.anyobject.AMembership; import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject; import org.apache.syncope.core.persistence.api.entity.group.Group; @@ -105,6 +108,12 @@ public class AnySearchTest extends AbstractTest { @Autowired private RoleDAO roleDAO; + @Autowired + private PlainSchemaDAO plainSchemaDAO; + + @Autowired + private PlainAttrValidationManager validator; + @BeforeEach public void adjustLoginDateForLocalSystem() throws ParseException { User rossini = userDAO.findByUsername("rossini").orElseThrow(); @@ -948,4 +957,85 @@ public void issueSYNCOPE1419() { assertNotNull(users); assertEquals(4, users.size()); } + + @Test + public void issueSYNCOPE1906() { + User bellini = userDAO.findByUsername("bellini").orElseThrow(); + + PlainSchema ctypeSchema = plainSchemaDAO.findById("ctype").orElseThrow(); + assertNotNull(ctypeSchema); + + userDAO.save(addPlainAttr(bellini, ctypeSchema, "aa1")); + + User puccini = userDAO.findByUsername("puccini").orElseThrow(); + assertNotNull(bellini); + + userDAO.save(addPlainAttr(puccini, ctypeSchema, "aa2")); + + User verdi = userDAO.findByUsername("verdi").orElseThrow(); + assertNotNull(verdi); + + userDAO.save(addPlainAttr(verdi, ctypeSchema, "aa3")); + + User vivaldi = userDAO.findByUsername("vivaldi").orElseThrow(); + assertNotNull(vivaldi); + + userDAO.save(addPlainAttr(vivaldi, ctypeSchema, "aa4")); + + User rossini = userDAO.findByUsername("rossini").orElseThrow(); + assertNotNull(rossini); + + userDAO.save(addPlainAttr(rossini, ctypeSchema, "aa5")); + + entityManager.flush(); + + AnyCond idCond = new AnyCond(AttrCond.Type.ISNOTNULL); + idCond.setSchema("id"); + + List users = + searchDAO.search(SearchCond.of(idCond), List.of(new Sort.Order(Sort.Direction.DESC, "ctype")), + AnyTypeKind.USER); + + assertEquals("bellini", users.get(4).getUsername()); + assertEquals("puccini", users.get(3).getUsername()); + assertEquals("verdi", users.get(2).getUsername()); + assertEquals("vivaldi", users.get(1).getUsername()); + assertEquals("rossini", users.get(0).getUsername()); + + // order by ctype even not searching by it + AttrCond surnameCond = new AttrCond(AttrCond.Type.ILIKE); + surnameCond.setSchema("surname"); + surnameCond.setExpression("%ini"); + + AttrCond coolCond = new AttrCond(AttrCond.Type.ISNULL); + coolCond.setSchema("cool"); + + users = searchDAO.search(SearchCond.and(SearchCond.of(surnameCond), SearchCond.of(coolCond)), + List.of(new Sort.Order(Sort.Direction.DESC, "ctype")), AnyTypeKind.USER); + + assertFalse(users.isEmpty()); + assertEquals(2, users.size()); + + // order by unique attribute + users = searchDAO.search(SearchCond.of(idCond), List.of(new Sort.Order(Sort.Direction.DESC, "fullname")), + AnyTypeKind.USER); + + assertEquals("vivaldi", users.get(4).getUsername()); + assertEquals("puccini", users.get(3).getUsername()); + assertEquals("rossini", users.get(2).getUsername()); + assertEquals("verdi", users.get(1).getUsername()); + assertEquals("bellini", users.get(0).getUsername()); + } + + private User addPlainAttr(final User user, final PlainSchema plainSchema, final String value) { + user.getPlainAttr(plainSchema.getKey()) + .ifPresentOrElse(ctype -> ctype.getValues().get(0).setStringValue(value), () -> { + PlainAttr attr = new PlainAttr(); + attr.setPlainSchema(plainSchema); + attr.add(validator, value); + + user.add(attr); + }); + return user; + } } diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java index e6070482743..e10c4ac3106 100644 --- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java +++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java @@ -102,6 +102,7 @@ import org.apache.syncope.common.lib.types.TaskType; import org.apache.syncope.common.lib.types.UnmatchingRule; import org.apache.syncope.common.rest.api.RESTHeaders; +import org.apache.syncope.common.rest.api.beans.AnyQuery; import org.apache.syncope.common.rest.api.beans.RealmQuery; import org.apache.syncope.common.rest.api.beans.ReconQuery; import org.apache.syncope.common.rest.api.beans.TaskQuery; @@ -1895,4 +1896,105 @@ public void issueSYNCOPE1894() { SCHEMA_SERVICE.delete(SchemaType.PLAIN, userWithDotSchema.getKey()); } } + + @Test + public void issueSYNCOPE1906() { + UserCR userCR = UserITCase.getUniqueSample("issuesyncope1906@syncope.apache.org"); + userCR.getPlainAttrs() + .removeIf(attr -> "ctype".equals(attr.getSchema()) || "fullname".equals(attr.getSchema())); + userCR.getPlainAttrs().add(attr("ctype", "aa1")); + userCR.getPlainAttrs().add(attr("fullname", "aa1")); + UserTO aa1 = createUser(userCR).getEntity(); + + userCR = UserITCase.getUniqueSample("issuesyncope1906@syncope.apache.org"); + userCR.getPlainAttrs() + .removeIf(attr -> "ctype".equals(attr.getSchema()) || "fullname".equals(attr.getSchema())); + userCR.getPlainAttrs().add(attr("ctype", "aa2")); + userCR.getPlainAttrs().add(attr("fullname", "aa2")); + UserTO aa2 = createUser(userCR).getEntity(); + + userCR = UserITCase.getUniqueSample("issuesyncope1906@syncope.apache.org"); + userCR.getPlainAttrs() + .removeIf(attr -> "ctype".equals(attr.getSchema()) || "fullname".equals(attr.getSchema())); + userCR.getPlainAttrs().add(attr("ctype", "aa3")); + userCR.getPlainAttrs().add(attr("fullname", "aa3")); + UserTO aa3 = createUser(userCR).getEntity(); + + userCR = UserITCase.getUniqueSample("issuesyncope1906@syncope.apache.org"); + userCR.getPlainAttrs() + .removeIf(attr -> "ctype".equals(attr.getSchema()) || "fullname".equals(attr.getSchema())); + userCR.getPlainAttrs().add(attr("ctype", "aa4")); + userCR.getPlainAttrs().add(attr("fullname", "aa4")); + UserTO aa4 = createUser(userCR).getEntity(); + + userCR = UserITCase.getUniqueSample("issuesyncope1906@syncope.apache.org"); + userCR.getPlainAttrs() + .removeIf(attr -> "ctype".equals(attr.getSchema()) || "fullname".equals(attr.getSchema())); + userCR.getPlainAttrs().add(attr("ctype", "aa5")); + userCR.getPlainAttrs().add(attr("fullname", "aa5")); + UserTO aa5 = createUser(userCR).getEntity(); + + // add also other users with valued ctype + userCR = UserITCase.getUniqueSample("actype@syncope.apache.org"); + userCR.getPlainAttrs() + .removeIf(attr -> "ctype".equals(attr.getSchema()) || "fullname".equals(attr.getSchema())); + userCR.getPlainAttrs().add(attr("ctype", "a ctype")); + userCR.getPlainAttrs().add(attr("fullname", "a fullname")); + createUser(userCR); + + userCR = UserITCase.getUniqueSample("actypetwo@syncope.apache.org"); + userCR.getPlainAttrs() + .removeIf(attr -> "ctype".equals(attr.getSchema()) || "fullname".equals(attr.getSchema())); + userCR.getPlainAttrs().add(attr("ctype", "a ctype 2")); + userCR.getPlainAttrs().add(attr("fullname", "a fullname 2")); + createUser(userCR); + try { + await().until(() -> USER_SERVICE.search(new AnyQuery.Builder().fiql( + SyncopeClient.getUserSearchConditionBuilder().is("email").equalTo("*issuesyncope1906*") + .query()) + .size(0) + .page(1) + .build()).getTotalCount() == 5); + List users = USER_SERVICE.search(new AnyQuery.Builder().fiql( + SyncopeClient.getUserSearchConditionBuilder().is("email").equalTo("*issuesyncope1906*") + .query()).size(10).page(1).orderBy("ctype DESC").build()).getResult(); + + assertEquals(5, users.size()); + + assertEquals(aa1.getUsername(), users.get(4).getUsername()); + assertEquals(aa2.getUsername(), users.get(3).getUsername()); + assertEquals(aa3.getUsername(), users.get(2).getUsername()); + assertEquals(aa4.getUsername(), users.get(1).getUsername()); + assertEquals(aa5.getUsername(), users.get(0).getUsername()); + + users = USER_SERVICE.search(new AnyQuery.Builder().fiql( + SyncopeClient.getUserSearchConditionBuilder().is("email").equalTo("*issuesyncope1906*") + .query()).size(10).page(1).orderBy("ctype ASC").build()).getResult(); + + assertEquals(aa5.getUsername(), users.get(4).getUsername()); + assertEquals(aa4.getUsername(), users.get(3).getUsername()); + assertEquals(aa3.getUsername(), users.get(2).getUsername()); + assertEquals(aa2.getUsername(), users.get(1).getUsername()); + assertEquals(aa1.getUsername(), users.get(0).getUsername()); + + // order by unique attribute + users = USER_SERVICE.search(new AnyQuery.Builder().fiql( + SyncopeClient.getUserSearchConditionBuilder().is("email").equalTo("*issuesyncope1906*") + .query()).size(10).page(1).orderBy("fullname ASC").build()).getResult(); + + assertEquals(aa5.getUsername(), users.get(4).getUsername()); + assertEquals(aa4.getUsername(), users.get(3).getUsername()); + assertEquals(aa3.getUsername(), users.get(2).getUsername()); + assertEquals(aa2.getUsername(), users.get(1).getUsername()); + assertEquals(aa1.getUsername(), users.get(0).getUsername()); + + } finally { + USER_SERVICE.search(new AnyQuery.Builder().fiql( + SyncopeClient.getUserSearchConditionBuilder().is("ctype").equalTo("aa*").query()) + .size(10) + .page(1) + .orderBy("ctype DESC") + .build()).getResult().forEach(u -> deleteUser(u.getKey())); + } + } }