github-advanced-security[bot] commented on code in PR #1300:
URL: https://github.com/apache/syncope/pull/1300#discussion_r2845885064


##########
core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java:
##########
@@ -89,68 +152,518 @@
         return neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + ") WHERE n.fullPath = 
$fullPath RETURN n.id").
                 bindAll(Map.of("fullPath", fullPath)).fetch().one().
-                flatMap(toOptional("n.id", Neo4jRealm.class, cache));
+                flatMap(found -> 
realmDAO.findById(found.get("n.id").toString()).map(n -> (Realm) n));
+    }
+
+    protected List<Realm> toList(
+            final Collection<Map<String, Object>> result,
+            final String property) {
+
+        return result.stream().
+                map(found -> 
realmDAO.findById(found.get(property).toString())).
+                flatMap(Optional::stream).map(n -> (Realm) n).toList();
     }
 
     @Override
     public List<Realm> findByName(final String name) {
         return toList(neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + ") WHERE n.name = $name RETURN 
n.id").
-                bindAll(Map.of("name", name)).fetch().all(), "n.id", 
Neo4jRealm.class, cache);
+                bindAll(Map.of("name", name)).fetch().all(), "n.id");
     }
 
     @Override
     public List<Realm> findChildren(final Realm realm) {
         return toList(neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + " {id: $id})<-[r:" + 
Neo4jRealm.PARENT_REL + "]-(c) RETURN c.id").
-                bindAll(Map.of("id", realm.getKey())).fetch().all(), "c.id", 
Neo4jRealm.class, cache);
+                bindAll(Map.of("id", realm.getKey())).fetch().all(), "c.id");
     }
 
     @Override
-    public long countDescendants(final String base, final String keyword) {
-        return countDescendants(Set.of(base), keyword);
+    public List<Realm> findDescendants(final String base, final String prefix) 
{
+        Map<String, Object> parameters = new HashMap<>();
+
+        StringBuilder query = new StringBuilder("MATCH 
(n:").append(Neo4jRealm.NODE).append(") ").
+                append("WHERE (").append("n.fullPath = $base OR n.fullPath =~ 
$like").append(')');
+        parameters.put("base", base);
+        parameters.put("like", SyncopeConstants.ROOT_REALM.equals(base) ? 
"/.*" : base + "/.*");
+
+        if (prefix != null) {
+            query.append(" AND (n.fullPath = $prefix OR n.fullPath =~ 
$likePrefix)");
+            parameters.put("prefix", prefix);
+            parameters.put("likePrefix", 
SyncopeConstants.ROOT_REALM.equals(prefix) ? "/.*" : prefix + "/.*");
+        }
+
+        query.append(" RETURN n.id ORDER BY n.fullPath");
+
+        return toList(neo4jClient.query(
+                query.toString()).bindAll(parameters).fetch().all(), "n.id");
     }
 
-    @Override
-    public long countDescendants(final Set<String> bases, final String 
keyword) {
-        Map<String, Object> parameters = new HashMap<>();
+    protected QueryInfo getQuery(final SearchCond cond, final Map<String, 
Object> parameters) {
+        boolean not = cond.getType() == SearchCond.Type.NOT_LEAF;
+
+        TextStringBuilder query = new TextStringBuilder();
+        Set<String> involvedFields = new HashSet<>();
+        Set<PlainSchema> involvedPlainSchemas = new HashSet<>();
 
-        StringBuilder queryString = buildDescendantsQuery(bases, keyword, 
parameters).append(" RETURN COUNT(n)");
-        return neo4jTemplate.count(queryString.toString(), parameters);
+        switch (cond.getType()) {
+            case LEAF, NOT_LEAF -> {
+                cond.asLeaf(AnyCond.class).ifPresentOrElse(
+                        anyCond -> {
+                            AnyCondQuery anyCondQuery = getQuery(anyCond, not, 
parameters);
+                            query.append(anyCondQuery.query());
+                            
Optional.ofNullable(anyCondQuery.field()).ifPresent(involvedFields::add);
+                        },
+                        () -> cond.asLeaf(AttrCond.class).ifPresent(leaf -> {
+                            AttrCondQuery attrCondQuery = getQuery(leaf, not, 
parameters);
+                            query.append(attrCondQuery.query());
+                            involvedPlainSchemas.add(attrCondQuery.schema());
+                        }));
+
+                // allow for additional search conditions
+                getQueryForCustomConds(cond, parameters, not, query);
+            }
+            case AND -> {
+                QueryInfo leftAndInfo = getQuery(cond.getLeft(), parameters);
+                involvedFields.addAll(leftAndInfo.fields());
+                involvedPlainSchemas.addAll(leftAndInfo.plainSchemas());
+
+                QueryInfo rigthAndInfo = getQuery(cond.getRight(), parameters);
+                involvedFields.addAll(rigthAndInfo.fields());
+                involvedPlainSchemas.addAll(rigthAndInfo.plainSchemas());
+
+                queryOp(query, "AND", leftAndInfo, rigthAndInfo);
+            }
+
+            case OR -> {
+                QueryInfo leftOrInfo = getQuery(cond.getLeft(), parameters);
+                involvedFields.addAll(leftOrInfo.fields());
+                involvedPlainSchemas.addAll(leftOrInfo.plainSchemas());
+
+                QueryInfo rigthOrInfo = getQuery(cond.getRight(), parameters);
+                involvedFields.addAll(rigthOrInfo.fields());
+                involvedPlainSchemas.addAll(rigthOrInfo.plainSchemas());
+
+                queryOp(query, "OR", leftOrInfo, rigthOrInfo);
+            }
+
+            default -> {
+            }
+        }
+
+        return new QueryInfo(query, involvedFields, involvedPlainSchemas);
     }
 
-    @Override
-    public List<Realm> findDescendants(final String base, final String 
keyword, final Pageable pageable) {
-        return findDescendants(Set.of(base), keyword, pageable);
+    protected void wrapQuery(
+            final Set<String> bases,
+            final QueryInfo queryInfo,
+            final Streamable<Order> orderBy,
+            final Map<String, Object> parameters) {
+
+        TextStringBuilder match = new TextStringBuilder("MATCH 
(n:").append(Neo4jRealm.NODE).append(") ").
+                append("WITH n.id AS id");
+
+        // take fields into account
+        queryInfo.fields().remove("id");
+        Stream.concat(
+                queryInfo.fields().stream(),
+                orderBy.stream().filter(clause -> 
!"id".equals(clause.getProperty())
+                && 
realmUtils.getField(clause.getProperty()).isPresent()).map(Order::getProperty)).
+                distinct().forEach(field -> match.append(", 
n.").append(field).append(" AS ").append(field));
+
+        // take plain schemas into account
+        Stream.concat(
+                queryInfo.plainSchemas().stream(),
+                orderBy.stream().map(clause -> 
plainSchemaDAO.findById(clause.getProperty())).
+                        flatMap(Optional::stream)).distinct().forEach(schema 
-> {
+
+            match.append(", apoc.convert.getJsonProperty(n, 
'plainAttrs.").append(schema.getKey());
+            if (schema.isUniqueConstraint()) {
+                match.append("', '$.uniqueValue')");
+            } else {
+                match.append("', '$.values')");
+            }
+            match.append(" AS ").append(schema.getKey());
+        });
+
+        TextStringBuilder query = queryInfo.query();
+
+        // take bases into account
+        AtomicInteger index = new AtomicInteger(0);
+        String basesClause = bases.stream().map(base -> {
+            int idx = index.incrementAndGet();
+            parameters.put("base" + idx, base);
+            parameters.put("like" + idx, 
SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base + "/.*");
+            return "n.fullPath = $base" + idx + " OR n.fullPath =~ $like" + 
idx;
+        }).collect(Collectors.joining(" OR "));
+        if (query.startsWith("MATCH (n)")) {
+            query.replaceFirst("MATCH (n)", match + " WHERE (EXISTS { MATCH 
(n)");
+            query.append("} ");
+        } else {
+            query.replaceFirst("WHERE EXISTS", "WHERE (EXISTS");
+            query.insert(0, match.append(' '));
+        }
+        query.append(") AND EXISTS { ").append("(n) WHERE 
(").append(basesClause).append(")").append(" } ");
+    }
+
+    protected AttrCondQuery getQuery(
+            final AttrCond cond,
+            final boolean not,
+            final Map<String, Object> parameters) {
+
+        CheckResult<AttrCond> checked = check(cond);
+
+        TextStringBuilder query = new TextStringBuilder("MATCH (n) ");
+        switch (cond.getType()) {
+            case ISNOTNULL ->
+                query.append("WHERE 
n.`plainAttrs.").append(checked.schema().getKey()).append("` IS NOT NULL");
+
+            case ISNULL ->
+                query.append("WHERE 
n.`plainAttrs.").append(checked.schema().getKey()).append("` IS NULL");
+
+            default ->
+                fillAttrQuery(query, checked.value(), checked.schema(), cond, 
not, parameters);
+        }
+
+        return new AttrCondQuery(query.toString(), checked.schema());
+    }
+
+    protected void getQueryForCustomConds(
+            final SearchCond cond,

Review Comment:
   ## Useless parameter
   
   The parameter 'cond' is never used.
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2577)



##########
core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java:
##########
@@ -89,68 +152,518 @@
         return neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + ") WHERE n.fullPath = 
$fullPath RETURN n.id").
                 bindAll(Map.of("fullPath", fullPath)).fetch().one().
-                flatMap(toOptional("n.id", Neo4jRealm.class, cache));
+                flatMap(found -> 
realmDAO.findById(found.get("n.id").toString()).map(n -> (Realm) n));
+    }
+
+    protected List<Realm> toList(
+            final Collection<Map<String, Object>> result,
+            final String property) {
+
+        return result.stream().
+                map(found -> 
realmDAO.findById(found.get(property).toString())).
+                flatMap(Optional::stream).map(n -> (Realm) n).toList();
     }
 
     @Override
     public List<Realm> findByName(final String name) {
         return toList(neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + ") WHERE n.name = $name RETURN 
n.id").
-                bindAll(Map.of("name", name)).fetch().all(), "n.id", 
Neo4jRealm.class, cache);
+                bindAll(Map.of("name", name)).fetch().all(), "n.id");
     }
 
     @Override
     public List<Realm> findChildren(final Realm realm) {
         return toList(neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + " {id: $id})<-[r:" + 
Neo4jRealm.PARENT_REL + "]-(c) RETURN c.id").
-                bindAll(Map.of("id", realm.getKey())).fetch().all(), "c.id", 
Neo4jRealm.class, cache);
+                bindAll(Map.of("id", realm.getKey())).fetch().all(), "c.id");
     }
 
     @Override
-    public long countDescendants(final String base, final String keyword) {
-        return countDescendants(Set.of(base), keyword);
+    public List<Realm> findDescendants(final String base, final String prefix) 
{
+        Map<String, Object> parameters = new HashMap<>();
+
+        StringBuilder query = new StringBuilder("MATCH 
(n:").append(Neo4jRealm.NODE).append(") ").
+                append("WHERE (").append("n.fullPath = $base OR n.fullPath =~ 
$like").append(')');
+        parameters.put("base", base);
+        parameters.put("like", SyncopeConstants.ROOT_REALM.equals(base) ? 
"/.*" : base + "/.*");
+
+        if (prefix != null) {
+            query.append(" AND (n.fullPath = $prefix OR n.fullPath =~ 
$likePrefix)");
+            parameters.put("prefix", prefix);
+            parameters.put("likePrefix", 
SyncopeConstants.ROOT_REALM.equals(prefix) ? "/.*" : prefix + "/.*");
+        }
+
+        query.append(" RETURN n.id ORDER BY n.fullPath");
+
+        return toList(neo4jClient.query(
+                query.toString()).bindAll(parameters).fetch().all(), "n.id");
     }
 
-    @Override
-    public long countDescendants(final Set<String> bases, final String 
keyword) {
-        Map<String, Object> parameters = new HashMap<>();
+    protected QueryInfo getQuery(final SearchCond cond, final Map<String, 
Object> parameters) {
+        boolean not = cond.getType() == SearchCond.Type.NOT_LEAF;
+
+        TextStringBuilder query = new TextStringBuilder();
+        Set<String> involvedFields = new HashSet<>();
+        Set<PlainSchema> involvedPlainSchemas = new HashSet<>();
 
-        StringBuilder queryString = buildDescendantsQuery(bases, keyword, 
parameters).append(" RETURN COUNT(n)");
-        return neo4jTemplate.count(queryString.toString(), parameters);
+        switch (cond.getType()) {
+            case LEAF, NOT_LEAF -> {
+                cond.asLeaf(AnyCond.class).ifPresentOrElse(
+                        anyCond -> {
+                            AnyCondQuery anyCondQuery = getQuery(anyCond, not, 
parameters);
+                            query.append(anyCondQuery.query());
+                            
Optional.ofNullable(anyCondQuery.field()).ifPresent(involvedFields::add);
+                        },
+                        () -> cond.asLeaf(AttrCond.class).ifPresent(leaf -> {
+                            AttrCondQuery attrCondQuery = getQuery(leaf, not, 
parameters);
+                            query.append(attrCondQuery.query());
+                            involvedPlainSchemas.add(attrCondQuery.schema());
+                        }));
+
+                // allow for additional search conditions
+                getQueryForCustomConds(cond, parameters, not, query);
+            }
+            case AND -> {
+                QueryInfo leftAndInfo = getQuery(cond.getLeft(), parameters);
+                involvedFields.addAll(leftAndInfo.fields());
+                involvedPlainSchemas.addAll(leftAndInfo.plainSchemas());
+
+                QueryInfo rigthAndInfo = getQuery(cond.getRight(), parameters);
+                involvedFields.addAll(rigthAndInfo.fields());
+                involvedPlainSchemas.addAll(rigthAndInfo.plainSchemas());
+
+                queryOp(query, "AND", leftAndInfo, rigthAndInfo);
+            }
+
+            case OR -> {
+                QueryInfo leftOrInfo = getQuery(cond.getLeft(), parameters);
+                involvedFields.addAll(leftOrInfo.fields());
+                involvedPlainSchemas.addAll(leftOrInfo.plainSchemas());
+
+                QueryInfo rigthOrInfo = getQuery(cond.getRight(), parameters);
+                involvedFields.addAll(rigthOrInfo.fields());
+                involvedPlainSchemas.addAll(rigthOrInfo.plainSchemas());
+
+                queryOp(query, "OR", leftOrInfo, rigthOrInfo);
+            }
+
+            default -> {
+            }
+        }
+
+        return new QueryInfo(query, involvedFields, involvedPlainSchemas);
     }
 
-    @Override
-    public List<Realm> findDescendants(final String base, final String 
keyword, final Pageable pageable) {
-        return findDescendants(Set.of(base), keyword, pageable);
+    protected void wrapQuery(
+            final Set<String> bases,
+            final QueryInfo queryInfo,
+            final Streamable<Order> orderBy,
+            final Map<String, Object> parameters) {
+
+        TextStringBuilder match = new TextStringBuilder("MATCH 
(n:").append(Neo4jRealm.NODE).append(") ").
+                append("WITH n.id AS id");
+
+        // take fields into account
+        queryInfo.fields().remove("id");
+        Stream.concat(
+                queryInfo.fields().stream(),
+                orderBy.stream().filter(clause -> 
!"id".equals(clause.getProperty())
+                && 
realmUtils.getField(clause.getProperty()).isPresent()).map(Order::getProperty)).
+                distinct().forEach(field -> match.append(", 
n.").append(field).append(" AS ").append(field));
+
+        // take plain schemas into account
+        Stream.concat(
+                queryInfo.plainSchemas().stream(),
+                orderBy.stream().map(clause -> 
plainSchemaDAO.findById(clause.getProperty())).
+                        flatMap(Optional::stream)).distinct().forEach(schema 
-> {
+
+            match.append(", apoc.convert.getJsonProperty(n, 
'plainAttrs.").append(schema.getKey());
+            if (schema.isUniqueConstraint()) {
+                match.append("', '$.uniqueValue')");
+            } else {
+                match.append("', '$.values')");
+            }
+            match.append(" AS ").append(schema.getKey());
+        });
+
+        TextStringBuilder query = queryInfo.query();
+
+        // take bases into account
+        AtomicInteger index = new AtomicInteger(0);
+        String basesClause = bases.stream().map(base -> {
+            int idx = index.incrementAndGet();
+            parameters.put("base" + idx, base);
+            parameters.put("like" + idx, 
SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base + "/.*");
+            return "n.fullPath = $base" + idx + " OR n.fullPath =~ $like" + 
idx;
+        }).collect(Collectors.joining(" OR "));
+        if (query.startsWith("MATCH (n)")) {
+            query.replaceFirst("MATCH (n)", match + " WHERE (EXISTS { MATCH 
(n)");
+            query.append("} ");
+        } else {
+            query.replaceFirst("WHERE EXISTS", "WHERE (EXISTS");
+            query.insert(0, match.append(' '));
+        }
+        query.append(") AND EXISTS { ").append("(n) WHERE 
(").append(basesClause).append(")").append(" } ");
+    }
+
+    protected AttrCondQuery getQuery(
+            final AttrCond cond,
+            final boolean not,
+            final Map<String, Object> parameters) {
+
+        CheckResult<AttrCond> checked = check(cond);
+
+        TextStringBuilder query = new TextStringBuilder("MATCH (n) ");
+        switch (cond.getType()) {
+            case ISNOTNULL ->
+                query.append("WHERE 
n.`plainAttrs.").append(checked.schema().getKey()).append("` IS NOT NULL");
+
+            case ISNULL ->
+                query.append("WHERE 
n.`plainAttrs.").append(checked.schema().getKey()).append("` IS NULL");
+
+            default ->
+                fillAttrQuery(query, checked.value(), checked.schema(), cond, 
not, parameters);
+        }
+
+        return new AttrCondQuery(query.toString(), checked.schema());
+    }
+
+    protected void getQueryForCustomConds(
+            final SearchCond cond,
+            final Map<String, Object> parameters,

Review Comment:
   ## Useless parameter
   
   The parameter 'parameters' is never used.
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2578)



##########
core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java:
##########
@@ -89,68 +152,518 @@
         return neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + ") WHERE n.fullPath = 
$fullPath RETURN n.id").
                 bindAll(Map.of("fullPath", fullPath)).fetch().one().
-                flatMap(toOptional("n.id", Neo4jRealm.class, cache));
+                flatMap(found -> 
realmDAO.findById(found.get("n.id").toString()).map(n -> (Realm) n));
+    }
+
+    protected List<Realm> toList(
+            final Collection<Map<String, Object>> result,
+            final String property) {
+
+        return result.stream().
+                map(found -> 
realmDAO.findById(found.get(property).toString())).
+                flatMap(Optional::stream).map(n -> (Realm) n).toList();
     }
 
     @Override
     public List<Realm> findByName(final String name) {
         return toList(neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + ") WHERE n.name = $name RETURN 
n.id").
-                bindAll(Map.of("name", name)).fetch().all(), "n.id", 
Neo4jRealm.class, cache);
+                bindAll(Map.of("name", name)).fetch().all(), "n.id");
     }
 
     @Override
     public List<Realm> findChildren(final Realm realm) {
         return toList(neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + " {id: $id})<-[r:" + 
Neo4jRealm.PARENT_REL + "]-(c) RETURN c.id").
-                bindAll(Map.of("id", realm.getKey())).fetch().all(), "c.id", 
Neo4jRealm.class, cache);
+                bindAll(Map.of("id", realm.getKey())).fetch().all(), "c.id");
     }
 
     @Override
-    public long countDescendants(final String base, final String keyword) {
-        return countDescendants(Set.of(base), keyword);
+    public List<Realm> findDescendants(final String base, final String prefix) 
{
+        Map<String, Object> parameters = new HashMap<>();
+
+        StringBuilder query = new StringBuilder("MATCH 
(n:").append(Neo4jRealm.NODE).append(") ").
+                append("WHERE (").append("n.fullPath = $base OR n.fullPath =~ 
$like").append(')');
+        parameters.put("base", base);
+        parameters.put("like", SyncopeConstants.ROOT_REALM.equals(base) ? 
"/.*" : base + "/.*");
+
+        if (prefix != null) {
+            query.append(" AND (n.fullPath = $prefix OR n.fullPath =~ 
$likePrefix)");
+            parameters.put("prefix", prefix);
+            parameters.put("likePrefix", 
SyncopeConstants.ROOT_REALM.equals(prefix) ? "/.*" : prefix + "/.*");
+        }
+
+        query.append(" RETURN n.id ORDER BY n.fullPath");
+
+        return toList(neo4jClient.query(
+                query.toString()).bindAll(parameters).fetch().all(), "n.id");
     }
 
-    @Override
-    public long countDescendants(final Set<String> bases, final String 
keyword) {
-        Map<String, Object> parameters = new HashMap<>();
+    protected QueryInfo getQuery(final SearchCond cond, final Map<String, 
Object> parameters) {
+        boolean not = cond.getType() == SearchCond.Type.NOT_LEAF;
+
+        TextStringBuilder query = new TextStringBuilder();
+        Set<String> involvedFields = new HashSet<>();
+        Set<PlainSchema> involvedPlainSchemas = new HashSet<>();
 
-        StringBuilder queryString = buildDescendantsQuery(bases, keyword, 
parameters).append(" RETURN COUNT(n)");
-        return neo4jTemplate.count(queryString.toString(), parameters);
+        switch (cond.getType()) {
+            case LEAF, NOT_LEAF -> {
+                cond.asLeaf(AnyCond.class).ifPresentOrElse(
+                        anyCond -> {
+                            AnyCondQuery anyCondQuery = getQuery(anyCond, not, 
parameters);
+                            query.append(anyCondQuery.query());
+                            
Optional.ofNullable(anyCondQuery.field()).ifPresent(involvedFields::add);
+                        },
+                        () -> cond.asLeaf(AttrCond.class).ifPresent(leaf -> {
+                            AttrCondQuery attrCondQuery = getQuery(leaf, not, 
parameters);
+                            query.append(attrCondQuery.query());
+                            involvedPlainSchemas.add(attrCondQuery.schema());
+                        }));
+
+                // allow for additional search conditions
+                getQueryForCustomConds(cond, parameters, not, query);
+            }
+            case AND -> {
+                QueryInfo leftAndInfo = getQuery(cond.getLeft(), parameters);
+                involvedFields.addAll(leftAndInfo.fields());
+                involvedPlainSchemas.addAll(leftAndInfo.plainSchemas());
+
+                QueryInfo rigthAndInfo = getQuery(cond.getRight(), parameters);
+                involvedFields.addAll(rigthAndInfo.fields());
+                involvedPlainSchemas.addAll(rigthAndInfo.plainSchemas());
+
+                queryOp(query, "AND", leftAndInfo, rigthAndInfo);
+            }
+
+            case OR -> {
+                QueryInfo leftOrInfo = getQuery(cond.getLeft(), parameters);
+                involvedFields.addAll(leftOrInfo.fields());
+                involvedPlainSchemas.addAll(leftOrInfo.plainSchemas());
+
+                QueryInfo rigthOrInfo = getQuery(cond.getRight(), parameters);
+                involvedFields.addAll(rigthOrInfo.fields());
+                involvedPlainSchemas.addAll(rigthOrInfo.plainSchemas());
+
+                queryOp(query, "OR", leftOrInfo, rigthOrInfo);
+            }
+
+            default -> {
+            }
+        }
+
+        return new QueryInfo(query, involvedFields, involvedPlainSchemas);
     }
 
-    @Override
-    public List<Realm> findDescendants(final String base, final String 
keyword, final Pageable pageable) {
-        return findDescendants(Set.of(base), keyword, pageable);
+    protected void wrapQuery(
+            final Set<String> bases,
+            final QueryInfo queryInfo,
+            final Streamable<Order> orderBy,
+            final Map<String, Object> parameters) {
+
+        TextStringBuilder match = new TextStringBuilder("MATCH 
(n:").append(Neo4jRealm.NODE).append(") ").
+                append("WITH n.id AS id");
+
+        // take fields into account
+        queryInfo.fields().remove("id");
+        Stream.concat(
+                queryInfo.fields().stream(),
+                orderBy.stream().filter(clause -> 
!"id".equals(clause.getProperty())
+                && 
realmUtils.getField(clause.getProperty()).isPresent()).map(Order::getProperty)).
+                distinct().forEach(field -> match.append(", 
n.").append(field).append(" AS ").append(field));
+
+        // take plain schemas into account
+        Stream.concat(
+                queryInfo.plainSchemas().stream(),
+                orderBy.stream().map(clause -> 
plainSchemaDAO.findById(clause.getProperty())).
+                        flatMap(Optional::stream)).distinct().forEach(schema 
-> {
+
+            match.append(", apoc.convert.getJsonProperty(n, 
'plainAttrs.").append(schema.getKey());
+            if (schema.isUniqueConstraint()) {
+                match.append("', '$.uniqueValue')");
+            } else {
+                match.append("', '$.values')");
+            }
+            match.append(" AS ").append(schema.getKey());
+        });
+
+        TextStringBuilder query = queryInfo.query();
+
+        // take bases into account
+        AtomicInteger index = new AtomicInteger(0);
+        String basesClause = bases.stream().map(base -> {
+            int idx = index.incrementAndGet();
+            parameters.put("base" + idx, base);
+            parameters.put("like" + idx, 
SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base + "/.*");
+            return "n.fullPath = $base" + idx + " OR n.fullPath =~ $like" + 
idx;
+        }).collect(Collectors.joining(" OR "));
+        if (query.startsWith("MATCH (n)")) {
+            query.replaceFirst("MATCH (n)", match + " WHERE (EXISTS { MATCH 
(n)");
+            query.append("} ");
+        } else {
+            query.replaceFirst("WHERE EXISTS", "WHERE (EXISTS");
+            query.insert(0, match.append(' '));
+        }
+        query.append(") AND EXISTS { ").append("(n) WHERE 
(").append(basesClause).append(")").append(" } ");
+    }
+
+    protected AttrCondQuery getQuery(
+            final AttrCond cond,
+            final boolean not,
+            final Map<String, Object> parameters) {
+
+        CheckResult<AttrCond> checked = check(cond);
+
+        TextStringBuilder query = new TextStringBuilder("MATCH (n) ");
+        switch (cond.getType()) {
+            case ISNOTNULL ->
+                query.append("WHERE 
n.`plainAttrs.").append(checked.schema().getKey()).append("` IS NOT NULL");
+
+            case ISNULL ->
+                query.append("WHERE 
n.`plainAttrs.").append(checked.schema().getKey()).append("` IS NULL");
+
+            default ->
+                fillAttrQuery(query, checked.value(), checked.schema(), cond, 
not, parameters);
+        }
+
+        return new AttrCondQuery(query.toString(), checked.schema());
+    }
+
+    protected void getQueryForCustomConds(
+            final SearchCond cond,
+            final Map<String, Object> parameters,
+            final boolean not,
+            final TextStringBuilder query) {
+
+        // do nothing by default, leave it open for subclasses
+    }
+
+    protected void fillAttrQuery(
+            final TextStringBuilder query,
+            final PlainAttrValue attrValue,
+            final PlainSchema schema,
+            final AttrCond cond,
+            final boolean not,
+            final Map<String, Object> parameters) {
+
+        if (not && cond.getType() == AttrCond.Type.ISNULL) {
+            cond.setType(AttrCond.Type.ISNOTNULL);
+            fillAttrQuery(query, attrValue, schema, cond, true, parameters);
+            return;
+        }
+        if (not) {
+            if (schema.isUniqueConstraint()) {
+                fillAttrQuery(query, attrValue, schema, cond, false, 
parameters);
+                query.replaceFirst("WHERE", "WHERE NOT(");
+                query.append(')');
+            } else {
+                fillAttrQuery(query, attrValue, schema, cond, false, 
parameters);
+                query.replaceAll("any(", schema.getKey() + " IS NULL OR 
none(");
+            }
+            return;
+        }
+
+        String value = Optional.ofNullable(attrValue.getDateValue()).
+                map(DateTimeFormatter.ISO_OFFSET_DATE_TIME::format).
+                orElseGet(cond::getExpression);
+
+        boolean isStr = true;
+        boolean lower = false;
+        if (schema.getType().isStringClass()) {
+            lower = (cond.getType() == AttrCond.Type.IEQ || cond.getType() == 
AttrCond.Type.ILIKE);
+        } else if (schema.getType() != AttrSchemaType.Date) {
+            lower = false;
+            try {
+                switch (schema.getType()) {
+                    case Long ->
+                        Long.valueOf(value);
+
+                    case Double ->
+                        Double.valueOf(value);
+
+                    case Boolean -> {
+                        if (!("true".equalsIgnoreCase(value) || 
"false".equalsIgnoreCase(value))) {
+                            throw new IllegalArgumentException();
+                        }
+                    }
+
+                    default -> {
+                    }
+                }
+
+                isStr = false;
+            } catch (Exception nfe) {
+                // ignore
+            }
+        }
+
+        query.append("WHERE ");
+
+        switch (cond.getType()) {
+            case ISNULL -> {
+            }
+
+            case ISNOTNULL ->
+                query.append(schema.getKey()).append(" IS NOT NULL");
+
+            case ILIKE, LIKE -> {
+                if (schema.getType().isStringClass()) {
+                    appendPlainAttrCond(
+                            query,
+                            schema,
+                            " =~ \"" + (lower ? "(?i)" : "")
+                            + 
AnyRepoExt.escapeForLikeRegex(value).replace("%", ".*") + '"');
+                } else {
+                    query.append(ALWAYS_FALSE_CLAUSE);
+                    LOG.error("LIKE is only compatible with string or enum 
schemas");
+                }
+            }
+
+            case IEQ, EQ -> {
+                if (StringUtils.containsAny(value, AnyRepoExt.REGEX_CHARS) || 
lower) {
+                    appendPlainAttrCond(
+                            query,
+                            schema,
+                            " =~ \"^" + (lower ? "(?i)" : "")
+                            + 
AnyRepoExt.escapeForLikeRegex(value).replace("%", ".*") + "$\"");
+                } else {
+                    appendPlainAttrCond(
+                            query,
+                            schema,
+                            " = " + escapeIfString(value, isStr));
+                }
+            }
+
+            case GE ->
+                appendPlainAttrCond(
+                        query,
+                        schema,
+                        " >= " + escapeIfString(value, isStr));
+
+            case GT ->
+                appendPlainAttrCond(
+                        query,
+                        schema,
+                        " > " + escapeIfString(value, isStr));
+
+            case LE ->
+                appendPlainAttrCond(
+                        query,
+                        schema,
+                        " <= " + escapeIfString(value, isStr));
+
+            case LT ->
+                appendPlainAttrCond(
+                        query,
+                        schema,
+                        " < " + escapeIfString(value, isStr));
+
+            default -> {
+            }
+        }
+        // shouldn't occour: processed before
+    }
+
+    protected void fillAttrQuery(

Review Comment:
   ## Confusing overloading of methods
   
   Method Neo4jRealmSearchDAO.fillAttrQuery(..) could be confused with 
overloaded method [fillAttrQuery](1), since dispatch depends on static types.
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2582)



##########
core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java:
##########
@@ -89,68 +152,518 @@
         return neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + ") WHERE n.fullPath = 
$fullPath RETURN n.id").
                 bindAll(Map.of("fullPath", fullPath)).fetch().one().
-                flatMap(toOptional("n.id", Neo4jRealm.class, cache));
+                flatMap(found -> 
realmDAO.findById(found.get("n.id").toString()).map(n -> (Realm) n));
+    }
+
+    protected List<Realm> toList(
+            final Collection<Map<String, Object>> result,
+            final String property) {
+
+        return result.stream().
+                map(found -> 
realmDAO.findById(found.get(property).toString())).
+                flatMap(Optional::stream).map(n -> (Realm) n).toList();
     }
 
     @Override
     public List<Realm> findByName(final String name) {
         return toList(neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + ") WHERE n.name = $name RETURN 
n.id").
-                bindAll(Map.of("name", name)).fetch().all(), "n.id", 
Neo4jRealm.class, cache);
+                bindAll(Map.of("name", name)).fetch().all(), "n.id");
     }
 
     @Override
     public List<Realm> findChildren(final Realm realm) {
         return toList(neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + " {id: $id})<-[r:" + 
Neo4jRealm.PARENT_REL + "]-(c) RETURN c.id").
-                bindAll(Map.of("id", realm.getKey())).fetch().all(), "c.id", 
Neo4jRealm.class, cache);
+                bindAll(Map.of("id", realm.getKey())).fetch().all(), "c.id");
     }
 
     @Override
-    public long countDescendants(final String base, final String keyword) {
-        return countDescendants(Set.of(base), keyword);
+    public List<Realm> findDescendants(final String base, final String prefix) 
{
+        Map<String, Object> parameters = new HashMap<>();
+
+        StringBuilder query = new StringBuilder("MATCH 
(n:").append(Neo4jRealm.NODE).append(") ").
+                append("WHERE (").append("n.fullPath = $base OR n.fullPath =~ 
$like").append(')');
+        parameters.put("base", base);
+        parameters.put("like", SyncopeConstants.ROOT_REALM.equals(base) ? 
"/.*" : base + "/.*");
+
+        if (prefix != null) {
+            query.append(" AND (n.fullPath = $prefix OR n.fullPath =~ 
$likePrefix)");
+            parameters.put("prefix", prefix);
+            parameters.put("likePrefix", 
SyncopeConstants.ROOT_REALM.equals(prefix) ? "/.*" : prefix + "/.*");
+        }
+
+        query.append(" RETURN n.id ORDER BY n.fullPath");
+
+        return toList(neo4jClient.query(
+                query.toString()).bindAll(parameters).fetch().all(), "n.id");
     }
 
-    @Override
-    public long countDescendants(final Set<String> bases, final String 
keyword) {
-        Map<String, Object> parameters = new HashMap<>();
+    protected QueryInfo getQuery(final SearchCond cond, final Map<String, 
Object> parameters) {
+        boolean not = cond.getType() == SearchCond.Type.NOT_LEAF;
+
+        TextStringBuilder query = new TextStringBuilder();
+        Set<String> involvedFields = new HashSet<>();
+        Set<PlainSchema> involvedPlainSchemas = new HashSet<>();
 
-        StringBuilder queryString = buildDescendantsQuery(bases, keyword, 
parameters).append(" RETURN COUNT(n)");
-        return neo4jTemplate.count(queryString.toString(), parameters);
+        switch (cond.getType()) {
+            case LEAF, NOT_LEAF -> {
+                cond.asLeaf(AnyCond.class).ifPresentOrElse(
+                        anyCond -> {
+                            AnyCondQuery anyCondQuery = getQuery(anyCond, not, 
parameters);
+                            query.append(anyCondQuery.query());
+                            
Optional.ofNullable(anyCondQuery.field()).ifPresent(involvedFields::add);
+                        },
+                        () -> cond.asLeaf(AttrCond.class).ifPresent(leaf -> {
+                            AttrCondQuery attrCondQuery = getQuery(leaf, not, 
parameters);
+                            query.append(attrCondQuery.query());
+                            involvedPlainSchemas.add(attrCondQuery.schema());
+                        }));
+
+                // allow for additional search conditions
+                getQueryForCustomConds(cond, parameters, not, query);
+            }
+            case AND -> {
+                QueryInfo leftAndInfo = getQuery(cond.getLeft(), parameters);
+                involvedFields.addAll(leftAndInfo.fields());
+                involvedPlainSchemas.addAll(leftAndInfo.plainSchemas());
+
+                QueryInfo rigthAndInfo = getQuery(cond.getRight(), parameters);
+                involvedFields.addAll(rigthAndInfo.fields());
+                involvedPlainSchemas.addAll(rigthAndInfo.plainSchemas());
+
+                queryOp(query, "AND", leftAndInfo, rigthAndInfo);
+            }
+
+            case OR -> {
+                QueryInfo leftOrInfo = getQuery(cond.getLeft(), parameters);
+                involvedFields.addAll(leftOrInfo.fields());
+                involvedPlainSchemas.addAll(leftOrInfo.plainSchemas());
+
+                QueryInfo rigthOrInfo = getQuery(cond.getRight(), parameters);
+                involvedFields.addAll(rigthOrInfo.fields());
+                involvedPlainSchemas.addAll(rigthOrInfo.plainSchemas());
+
+                queryOp(query, "OR", leftOrInfo, rigthOrInfo);
+            }
+
+            default -> {
+            }
+        }
+
+        return new QueryInfo(query, involvedFields, involvedPlainSchemas);
     }
 
-    @Override
-    public List<Realm> findDescendants(final String base, final String 
keyword, final Pageable pageable) {
-        return findDescendants(Set.of(base), keyword, pageable);
+    protected void wrapQuery(
+            final Set<String> bases,
+            final QueryInfo queryInfo,
+            final Streamable<Order> orderBy,
+            final Map<String, Object> parameters) {
+
+        TextStringBuilder match = new TextStringBuilder("MATCH 
(n:").append(Neo4jRealm.NODE).append(") ").
+                append("WITH n.id AS id");
+
+        // take fields into account
+        queryInfo.fields().remove("id");
+        Stream.concat(
+                queryInfo.fields().stream(),
+                orderBy.stream().filter(clause -> 
!"id".equals(clause.getProperty())
+                && 
realmUtils.getField(clause.getProperty()).isPresent()).map(Order::getProperty)).
+                distinct().forEach(field -> match.append(", 
n.").append(field).append(" AS ").append(field));
+
+        // take plain schemas into account
+        Stream.concat(
+                queryInfo.plainSchemas().stream(),
+                orderBy.stream().map(clause -> 
plainSchemaDAO.findById(clause.getProperty())).
+                        flatMap(Optional::stream)).distinct().forEach(schema 
-> {
+
+            match.append(", apoc.convert.getJsonProperty(n, 
'plainAttrs.").append(schema.getKey());
+            if (schema.isUniqueConstraint()) {
+                match.append("', '$.uniqueValue')");
+            } else {
+                match.append("', '$.values')");
+            }
+            match.append(" AS ").append(schema.getKey());
+        });
+
+        TextStringBuilder query = queryInfo.query();
+
+        // take bases into account
+        AtomicInteger index = new AtomicInteger(0);
+        String basesClause = bases.stream().map(base -> {
+            int idx = index.incrementAndGet();
+            parameters.put("base" + idx, base);
+            parameters.put("like" + idx, 
SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base + "/.*");
+            return "n.fullPath = $base" + idx + " OR n.fullPath =~ $like" + 
idx;
+        }).collect(Collectors.joining(" OR "));
+        if (query.startsWith("MATCH (n)")) {
+            query.replaceFirst("MATCH (n)", match + " WHERE (EXISTS { MATCH 
(n)");
+            query.append("} ");
+        } else {
+            query.replaceFirst("WHERE EXISTS", "WHERE (EXISTS");
+            query.insert(0, match.append(' '));
+        }
+        query.append(") AND EXISTS { ").append("(n) WHERE 
(").append(basesClause).append(")").append(" } ");
+    }
+
+    protected AttrCondQuery getQuery(
+            final AttrCond cond,
+            final boolean not,
+            final Map<String, Object> parameters) {
+
+        CheckResult<AttrCond> checked = check(cond);
+
+        TextStringBuilder query = new TextStringBuilder("MATCH (n) ");
+        switch (cond.getType()) {
+            case ISNOTNULL ->
+                query.append("WHERE 
n.`plainAttrs.").append(checked.schema().getKey()).append("` IS NOT NULL");
+
+            case ISNULL ->
+                query.append("WHERE 
n.`plainAttrs.").append(checked.schema().getKey()).append("` IS NULL");
+
+            default ->
+                fillAttrQuery(query, checked.value(), checked.schema(), cond, 
not, parameters);
+        }
+
+        return new AttrCondQuery(query.toString(), checked.schema());
+    }
+
+    protected void getQueryForCustomConds(
+            final SearchCond cond,
+            final Map<String, Object> parameters,
+            final boolean not,

Review Comment:
   ## Useless parameter
   
   The parameter 'not' is never used.
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2579)



##########
core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java:
##########
@@ -89,68 +152,518 @@
         return neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + ") WHERE n.fullPath = 
$fullPath RETURN n.id").
                 bindAll(Map.of("fullPath", fullPath)).fetch().one().
-                flatMap(toOptional("n.id", Neo4jRealm.class, cache));
+                flatMap(found -> 
realmDAO.findById(found.get("n.id").toString()).map(n -> (Realm) n));
+    }
+
+    protected List<Realm> toList(
+            final Collection<Map<String, Object>> result,
+            final String property) {
+
+        return result.stream().
+                map(found -> 
realmDAO.findById(found.get(property).toString())).
+                flatMap(Optional::stream).map(n -> (Realm) n).toList();
     }
 
     @Override
     public List<Realm> findByName(final String name) {
         return toList(neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + ") WHERE n.name = $name RETURN 
n.id").
-                bindAll(Map.of("name", name)).fetch().all(), "n.id", 
Neo4jRealm.class, cache);
+                bindAll(Map.of("name", name)).fetch().all(), "n.id");
     }
 
     @Override
     public List<Realm> findChildren(final Realm realm) {
         return toList(neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + " {id: $id})<-[r:" + 
Neo4jRealm.PARENT_REL + "]-(c) RETURN c.id").
-                bindAll(Map.of("id", realm.getKey())).fetch().all(), "c.id", 
Neo4jRealm.class, cache);
+                bindAll(Map.of("id", realm.getKey())).fetch().all(), "c.id");
     }
 
     @Override
-    public long countDescendants(final String base, final String keyword) {
-        return countDescendants(Set.of(base), keyword);
+    public List<Realm> findDescendants(final String base, final String prefix) 
{
+        Map<String, Object> parameters = new HashMap<>();
+
+        StringBuilder query = new StringBuilder("MATCH 
(n:").append(Neo4jRealm.NODE).append(") ").
+                append("WHERE (").append("n.fullPath = $base OR n.fullPath =~ 
$like").append(')');
+        parameters.put("base", base);
+        parameters.put("like", SyncopeConstants.ROOT_REALM.equals(base) ? 
"/.*" : base + "/.*");
+
+        if (prefix != null) {
+            query.append(" AND (n.fullPath = $prefix OR n.fullPath =~ 
$likePrefix)");
+            parameters.put("prefix", prefix);
+            parameters.put("likePrefix", 
SyncopeConstants.ROOT_REALM.equals(prefix) ? "/.*" : prefix + "/.*");
+        }
+
+        query.append(" RETURN n.id ORDER BY n.fullPath");
+
+        return toList(neo4jClient.query(
+                query.toString()).bindAll(parameters).fetch().all(), "n.id");
     }
 
-    @Override
-    public long countDescendants(final Set<String> bases, final String 
keyword) {
-        Map<String, Object> parameters = new HashMap<>();
+    protected QueryInfo getQuery(final SearchCond cond, final Map<String, 
Object> parameters) {
+        boolean not = cond.getType() == SearchCond.Type.NOT_LEAF;
+
+        TextStringBuilder query = new TextStringBuilder();
+        Set<String> involvedFields = new HashSet<>();
+        Set<PlainSchema> involvedPlainSchemas = new HashSet<>();
 
-        StringBuilder queryString = buildDescendantsQuery(bases, keyword, 
parameters).append(" RETURN COUNT(n)");
-        return neo4jTemplate.count(queryString.toString(), parameters);
+        switch (cond.getType()) {
+            case LEAF, NOT_LEAF -> {
+                cond.asLeaf(AnyCond.class).ifPresentOrElse(
+                        anyCond -> {
+                            AnyCondQuery anyCondQuery = getQuery(anyCond, not, 
parameters);
+                            query.append(anyCondQuery.query());
+                            
Optional.ofNullable(anyCondQuery.field()).ifPresent(involvedFields::add);
+                        },
+                        () -> cond.asLeaf(AttrCond.class).ifPresent(leaf -> {
+                            AttrCondQuery attrCondQuery = getQuery(leaf, not, 
parameters);
+                            query.append(attrCondQuery.query());
+                            involvedPlainSchemas.add(attrCondQuery.schema());
+                        }));
+
+                // allow for additional search conditions
+                getQueryForCustomConds(cond, parameters, not, query);
+            }
+            case AND -> {
+                QueryInfo leftAndInfo = getQuery(cond.getLeft(), parameters);
+                involvedFields.addAll(leftAndInfo.fields());
+                involvedPlainSchemas.addAll(leftAndInfo.plainSchemas());
+
+                QueryInfo rigthAndInfo = getQuery(cond.getRight(), parameters);
+                involvedFields.addAll(rigthAndInfo.fields());
+                involvedPlainSchemas.addAll(rigthAndInfo.plainSchemas());
+
+                queryOp(query, "AND", leftAndInfo, rigthAndInfo);
+            }
+
+            case OR -> {
+                QueryInfo leftOrInfo = getQuery(cond.getLeft(), parameters);
+                involvedFields.addAll(leftOrInfo.fields());
+                involvedPlainSchemas.addAll(leftOrInfo.plainSchemas());
+
+                QueryInfo rigthOrInfo = getQuery(cond.getRight(), parameters);
+                involvedFields.addAll(rigthOrInfo.fields());
+                involvedPlainSchemas.addAll(rigthOrInfo.plainSchemas());
+
+                queryOp(query, "OR", leftOrInfo, rigthOrInfo);
+            }
+
+            default -> {
+            }
+        }
+
+        return new QueryInfo(query, involvedFields, involvedPlainSchemas);
     }
 
-    @Override
-    public List<Realm> findDescendants(final String base, final String 
keyword, final Pageable pageable) {
-        return findDescendants(Set.of(base), keyword, pageable);
+    protected void wrapQuery(
+            final Set<String> bases,
+            final QueryInfo queryInfo,
+            final Streamable<Order> orderBy,
+            final Map<String, Object> parameters) {
+
+        TextStringBuilder match = new TextStringBuilder("MATCH 
(n:").append(Neo4jRealm.NODE).append(") ").
+                append("WITH n.id AS id");
+
+        // take fields into account
+        queryInfo.fields().remove("id");
+        Stream.concat(
+                queryInfo.fields().stream(),
+                orderBy.stream().filter(clause -> 
!"id".equals(clause.getProperty())
+                && 
realmUtils.getField(clause.getProperty()).isPresent()).map(Order::getProperty)).
+                distinct().forEach(field -> match.append(", 
n.").append(field).append(" AS ").append(field));
+
+        // take plain schemas into account
+        Stream.concat(
+                queryInfo.plainSchemas().stream(),
+                orderBy.stream().map(clause -> 
plainSchemaDAO.findById(clause.getProperty())).
+                        flatMap(Optional::stream)).distinct().forEach(schema 
-> {
+
+            match.append(", apoc.convert.getJsonProperty(n, 
'plainAttrs.").append(schema.getKey());
+            if (schema.isUniqueConstraint()) {
+                match.append("', '$.uniqueValue')");
+            } else {
+                match.append("', '$.values')");
+            }
+            match.append(" AS ").append(schema.getKey());
+        });
+
+        TextStringBuilder query = queryInfo.query();
+
+        // take bases into account
+        AtomicInteger index = new AtomicInteger(0);
+        String basesClause = bases.stream().map(base -> {
+            int idx = index.incrementAndGet();
+            parameters.put("base" + idx, base);
+            parameters.put("like" + idx, 
SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base + "/.*");
+            return "n.fullPath = $base" + idx + " OR n.fullPath =~ $like" + 
idx;
+        }).collect(Collectors.joining(" OR "));
+        if (query.startsWith("MATCH (n)")) {
+            query.replaceFirst("MATCH (n)", match + " WHERE (EXISTS { MATCH 
(n)");
+            query.append("} ");
+        } else {
+            query.replaceFirst("WHERE EXISTS", "WHERE (EXISTS");
+            query.insert(0, match.append(' '));
+        }
+        query.append(") AND EXISTS { ").append("(n) WHERE 
(").append(basesClause).append(")").append(" } ");
+    }
+
+    protected AttrCondQuery getQuery(
+            final AttrCond cond,
+            final boolean not,
+            final Map<String, Object> parameters) {
+
+        CheckResult<AttrCond> checked = check(cond);
+
+        TextStringBuilder query = new TextStringBuilder("MATCH (n) ");
+        switch (cond.getType()) {
+            case ISNOTNULL ->
+                query.append("WHERE 
n.`plainAttrs.").append(checked.schema().getKey()).append("` IS NOT NULL");
+
+            case ISNULL ->
+                query.append("WHERE 
n.`plainAttrs.").append(checked.schema().getKey()).append("` IS NULL");
+
+            default ->
+                fillAttrQuery(query, checked.value(), checked.schema(), cond, 
not, parameters);
+        }
+
+        return new AttrCondQuery(query.toString(), checked.schema());
+    }
+
+    protected void getQueryForCustomConds(
+            final SearchCond cond,
+            final Map<String, Object> parameters,
+            final boolean not,
+            final TextStringBuilder query) {
+
+        // do nothing by default, leave it open for subclasses
+    }
+
+    protected void fillAttrQuery(
+            final TextStringBuilder query,
+            final PlainAttrValue attrValue,
+            final PlainSchema schema,
+            final AttrCond cond,
+            final boolean not,
+            final Map<String, Object> parameters) {
+
+        if (not && cond.getType() == AttrCond.Type.ISNULL) {
+            cond.setType(AttrCond.Type.ISNOTNULL);
+            fillAttrQuery(query, attrValue, schema, cond, true, parameters);
+            return;
+        }
+        if (not) {
+            if (schema.isUniqueConstraint()) {
+                fillAttrQuery(query, attrValue, schema, cond, false, 
parameters);
+                query.replaceFirst("WHERE", "WHERE NOT(");
+                query.append(')');
+            } else {
+                fillAttrQuery(query, attrValue, schema, cond, false, 
parameters);
+                query.replaceAll("any(", schema.getKey() + " IS NULL OR 
none(");
+            }
+            return;
+        }
+
+        String value = Optional.ofNullable(attrValue.getDateValue()).
+                map(DateTimeFormatter.ISO_OFFSET_DATE_TIME::format).
+                orElseGet(cond::getExpression);
+
+        boolean isStr = true;
+        boolean lower = false;
+        if (schema.getType().isStringClass()) {
+            lower = (cond.getType() == AttrCond.Type.IEQ || cond.getType() == 
AttrCond.Type.ILIKE);
+        } else if (schema.getType() != AttrSchemaType.Date) {
+            lower = false;
+            try {
+                switch (schema.getType()) {
+                    case Long ->
+                        Long.valueOf(value);
+
+                    case Double ->
+                        Double.valueOf(value);
+
+                    case Boolean -> {
+                        if (!("true".equalsIgnoreCase(value) || 
"false".equalsIgnoreCase(value))) {
+                            throw new IllegalArgumentException();
+                        }
+                    }
+
+                    default -> {
+                    }
+                }
+
+                isStr = false;
+            } catch (Exception nfe) {
+                // ignore
+            }
+        }
+
+        query.append("WHERE ");
+
+        switch (cond.getType()) {
+            case ISNULL -> {
+            }
+
+            case ISNOTNULL ->
+                query.append(schema.getKey()).append(" IS NOT NULL");
+
+            case ILIKE, LIKE -> {
+                if (schema.getType().isStringClass()) {
+                    appendPlainAttrCond(
+                            query,
+                            schema,
+                            " =~ \"" + (lower ? "(?i)" : "")
+                            + 
AnyRepoExt.escapeForLikeRegex(value).replace("%", ".*") + '"');
+                } else {
+                    query.append(ALWAYS_FALSE_CLAUSE);
+                    LOG.error("LIKE is only compatible with string or enum 
schemas");
+                }
+            }
+
+            case IEQ, EQ -> {
+                if (StringUtils.containsAny(value, AnyRepoExt.REGEX_CHARS) || 
lower) {
+                    appendPlainAttrCond(
+                            query,
+                            schema,
+                            " =~ \"^" + (lower ? "(?i)" : "")
+                            + 
AnyRepoExt.escapeForLikeRegex(value).replace("%", ".*") + "$\"");
+                } else {
+                    appendPlainAttrCond(
+                            query,
+                            schema,
+                            " = " + escapeIfString(value, isStr));
+                }
+            }
+
+            case GE ->
+                appendPlainAttrCond(
+                        query,
+                        schema,
+                        " >= " + escapeIfString(value, isStr));
+
+            case GT ->
+                appendPlainAttrCond(
+                        query,
+                        schema,
+                        " > " + escapeIfString(value, isStr));
+
+            case LE ->
+                appendPlainAttrCond(
+                        query,
+                        schema,
+                        " <= " + escapeIfString(value, isStr));
+
+            case LT ->
+                appendPlainAttrCond(
+                        query,
+                        schema,
+                        " < " + escapeIfString(value, isStr));
+
+            default -> {
+            }
+        }
+        // shouldn't occour: processed before
+    }
+
+    protected void fillAttrQuery(
+            final TextStringBuilder query,
+            final PlainAttrValue attrValue,
+            final PlainSchema schema,
+            final AnyCond cond,
+            final boolean not,
+            final Map<String, Object> parameters) {
+
+        if (not && cond.getType() == AttrCond.Type.ISNULL) {
+            cond.setType(AttrCond.Type.ISNOTNULL);
+            fillAttrQuery(query, attrValue, schema, cond, true, parameters);
+            return;
+        }
+        if (not) {
+            query.append("NOT (");
+            fillAttrQuery(query, attrValue, schema, cond, false, parameters);
+            query.append(')');
+            return;
+        }
+        if (not && cond.getType() == AttrCond.Type.ISNULL) {
+            cond.setType(AttrCond.Type.ISNOTNULL);
+            fillAttrQuery(query, attrValue, schema, cond, true, parameters);
+            return;
+        }
+
+        boolean lower = schema.getType().isStringClass()
+                && (cond.getType() == AttrCond.Type.IEQ || cond.getType() == 
AttrCond.Type.ILIKE);
+
+        String property = "n." + cond.getSchema();
+        if (lower) {
+            property = "toLower (" + property + ')';
+        }
+
+        switch (cond.getType()) {
+
+            case ISNULL ->
+                query.append(property).append(" IS NULL");
+
+            case ISNOTNULL ->
+                query.append(property).append(" IS NOT NULL");
+
+            case ILIKE, LIKE -> {
+                if (schema.getType().isStringClass()) {
+                    query.append(property).append(" =~ ");
+                    if (lower) {
+                        query.append("toLower($").
+                                append(setParameter(parameters, 
cond.getExpression().replace("%", ".*"))).
+                                append(')');
+                    } else {
+                        query.append('$').append(setParameter(parameters, 
cond.getExpression().replace("%", ".*")));
+                    }
+                } else {
+                    query.append(' ').append(ALWAYS_FALSE_CLAUSE);
+                    LOG.error("LIKE is only compatible with string or enum 
schemas");
+                }
+            }
+
+            case IEQ, EQ -> {
+                query.append(property).append('=');
+
+                if (lower) {
+                    query.append("toLower($").append(setParameter(parameters, 
attrValue.getValue())).append(')');
+                } else {
+                    query.append('$').append(setParameter(parameters, 
attrValue.getValue()));
+                }
+            }
+
+            case GE -> {
+                query.append(property);
+                if (not) {
+                    query.append('<');
+                } else {
+                    query.append(">=");
+                }
+                query.append('$').append(setParameter(parameters, 
attrValue.getValue()));
+            }
+
+            case GT -> {
+                query.append(property);
+                if (not) {
+                    query.append("<=");
+                } else {
+                    query.append('>');
+                }
+                query.append('$').append(setParameter(parameters, 
attrValue.getValue()));
+            }
+
+            case LE -> {
+                query.append(property);
+                if (not) {
+                    query.append('>');
+                } else {
+                    query.append("<=");
+                }
+                query.append('$').append(setParameter(parameters, 
attrValue.getValue()));
+            }
+
+            case LT -> {
+                query.append(property);
+                if (not) {
+                    query.append(">=");
+                } else {
+                    query.append('<');
+                }
+                query.append('$').append(setParameter(parameters, 
attrValue.getValue()));
+            }
+
+            default -> {
+            }
+        }
+    }
+
+    protected AnyCondQuery getQuery(

Review Comment:
   ## Confusing overloading of methods
   
   Method Neo4jRealmSearchDAO.getQuery(..) could be confused with overloaded 
method [getQuery](1), since dispatch depends on static types.
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2581)



##########
core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java:
##########
@@ -89,68 +152,518 @@
         return neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + ") WHERE n.fullPath = 
$fullPath RETURN n.id").
                 bindAll(Map.of("fullPath", fullPath)).fetch().one().
-                flatMap(toOptional("n.id", Neo4jRealm.class, cache));
+                flatMap(found -> 
realmDAO.findById(found.get("n.id").toString()).map(n -> (Realm) n));
+    }
+
+    protected List<Realm> toList(
+            final Collection<Map<String, Object>> result,
+            final String property) {
+
+        return result.stream().
+                map(found -> 
realmDAO.findById(found.get(property).toString())).
+                flatMap(Optional::stream).map(n -> (Realm) n).toList();
     }
 
     @Override
     public List<Realm> findByName(final String name) {
         return toList(neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + ") WHERE n.name = $name RETURN 
n.id").
-                bindAll(Map.of("name", name)).fetch().all(), "n.id", 
Neo4jRealm.class, cache);
+                bindAll(Map.of("name", name)).fetch().all(), "n.id");
     }
 
     @Override
     public List<Realm> findChildren(final Realm realm) {
         return toList(neo4jClient.query(
                 "MATCH (n:" + Neo4jRealm.NODE + " {id: $id})<-[r:" + 
Neo4jRealm.PARENT_REL + "]-(c) RETURN c.id").
-                bindAll(Map.of("id", realm.getKey())).fetch().all(), "c.id", 
Neo4jRealm.class, cache);
+                bindAll(Map.of("id", realm.getKey())).fetch().all(), "c.id");
     }
 
     @Override
-    public long countDescendants(final String base, final String keyword) {
-        return countDescendants(Set.of(base), keyword);
+    public List<Realm> findDescendants(final String base, final String prefix) 
{
+        Map<String, Object> parameters = new HashMap<>();
+
+        StringBuilder query = new StringBuilder("MATCH 
(n:").append(Neo4jRealm.NODE).append(") ").
+                append("WHERE (").append("n.fullPath = $base OR n.fullPath =~ 
$like").append(')');
+        parameters.put("base", base);
+        parameters.put("like", SyncopeConstants.ROOT_REALM.equals(base) ? 
"/.*" : base + "/.*");
+
+        if (prefix != null) {
+            query.append(" AND (n.fullPath = $prefix OR n.fullPath =~ 
$likePrefix)");
+            parameters.put("prefix", prefix);
+            parameters.put("likePrefix", 
SyncopeConstants.ROOT_REALM.equals(prefix) ? "/.*" : prefix + "/.*");
+        }
+
+        query.append(" RETURN n.id ORDER BY n.fullPath");
+
+        return toList(neo4jClient.query(
+                query.toString()).bindAll(parameters).fetch().all(), "n.id");
     }
 
-    @Override
-    public long countDescendants(final Set<String> bases, final String 
keyword) {
-        Map<String, Object> parameters = new HashMap<>();
+    protected QueryInfo getQuery(final SearchCond cond, final Map<String, 
Object> parameters) {
+        boolean not = cond.getType() == SearchCond.Type.NOT_LEAF;
+
+        TextStringBuilder query = new TextStringBuilder();
+        Set<String> involvedFields = new HashSet<>();
+        Set<PlainSchema> involvedPlainSchemas = new HashSet<>();
 
-        StringBuilder queryString = buildDescendantsQuery(bases, keyword, 
parameters).append(" RETURN COUNT(n)");
-        return neo4jTemplate.count(queryString.toString(), parameters);
+        switch (cond.getType()) {
+            case LEAF, NOT_LEAF -> {
+                cond.asLeaf(AnyCond.class).ifPresentOrElse(
+                        anyCond -> {
+                            AnyCondQuery anyCondQuery = getQuery(anyCond, not, 
parameters);
+                            query.append(anyCondQuery.query());
+                            
Optional.ofNullable(anyCondQuery.field()).ifPresent(involvedFields::add);
+                        },
+                        () -> cond.asLeaf(AttrCond.class).ifPresent(leaf -> {
+                            AttrCondQuery attrCondQuery = getQuery(leaf, not, 
parameters);
+                            query.append(attrCondQuery.query());
+                            involvedPlainSchemas.add(attrCondQuery.schema());
+                        }));
+
+                // allow for additional search conditions
+                getQueryForCustomConds(cond, parameters, not, query);
+            }
+            case AND -> {
+                QueryInfo leftAndInfo = getQuery(cond.getLeft(), parameters);
+                involvedFields.addAll(leftAndInfo.fields());
+                involvedPlainSchemas.addAll(leftAndInfo.plainSchemas());
+
+                QueryInfo rigthAndInfo = getQuery(cond.getRight(), parameters);
+                involvedFields.addAll(rigthAndInfo.fields());
+                involvedPlainSchemas.addAll(rigthAndInfo.plainSchemas());
+
+                queryOp(query, "AND", leftAndInfo, rigthAndInfo);
+            }
+
+            case OR -> {
+                QueryInfo leftOrInfo = getQuery(cond.getLeft(), parameters);
+                involvedFields.addAll(leftOrInfo.fields());
+                involvedPlainSchemas.addAll(leftOrInfo.plainSchemas());
+
+                QueryInfo rigthOrInfo = getQuery(cond.getRight(), parameters);
+                involvedFields.addAll(rigthOrInfo.fields());
+                involvedPlainSchemas.addAll(rigthOrInfo.plainSchemas());
+
+                queryOp(query, "OR", leftOrInfo, rigthOrInfo);
+            }
+
+            default -> {
+            }
+        }
+
+        return new QueryInfo(query, involvedFields, involvedPlainSchemas);
     }
 
-    @Override
-    public List<Realm> findDescendants(final String base, final String 
keyword, final Pageable pageable) {
-        return findDescendants(Set.of(base), keyword, pageable);
+    protected void wrapQuery(
+            final Set<String> bases,
+            final QueryInfo queryInfo,
+            final Streamable<Order> orderBy,
+            final Map<String, Object> parameters) {
+
+        TextStringBuilder match = new TextStringBuilder("MATCH 
(n:").append(Neo4jRealm.NODE).append(") ").
+                append("WITH n.id AS id");
+
+        // take fields into account
+        queryInfo.fields().remove("id");
+        Stream.concat(
+                queryInfo.fields().stream(),
+                orderBy.stream().filter(clause -> 
!"id".equals(clause.getProperty())
+                && 
realmUtils.getField(clause.getProperty()).isPresent()).map(Order::getProperty)).
+                distinct().forEach(field -> match.append(", 
n.").append(field).append(" AS ").append(field));
+
+        // take plain schemas into account
+        Stream.concat(
+                queryInfo.plainSchemas().stream(),
+                orderBy.stream().map(clause -> 
plainSchemaDAO.findById(clause.getProperty())).
+                        flatMap(Optional::stream)).distinct().forEach(schema 
-> {
+
+            match.append(", apoc.convert.getJsonProperty(n, 
'plainAttrs.").append(schema.getKey());
+            if (schema.isUniqueConstraint()) {
+                match.append("', '$.uniqueValue')");
+            } else {
+                match.append("', '$.values')");
+            }
+            match.append(" AS ").append(schema.getKey());
+        });
+
+        TextStringBuilder query = queryInfo.query();
+
+        // take bases into account
+        AtomicInteger index = new AtomicInteger(0);
+        String basesClause = bases.stream().map(base -> {
+            int idx = index.incrementAndGet();
+            parameters.put("base" + idx, base);
+            parameters.put("like" + idx, 
SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base + "/.*");
+            return "n.fullPath = $base" + idx + " OR n.fullPath =~ $like" + 
idx;
+        }).collect(Collectors.joining(" OR "));
+        if (query.startsWith("MATCH (n)")) {
+            query.replaceFirst("MATCH (n)", match + " WHERE (EXISTS { MATCH 
(n)");
+            query.append("} ");
+        } else {
+            query.replaceFirst("WHERE EXISTS", "WHERE (EXISTS");
+            query.insert(0, match.append(' '));
+        }
+        query.append(") AND EXISTS { ").append("(n) WHERE 
(").append(basesClause).append(")").append(" } ");
+    }
+
+    protected AttrCondQuery getQuery(
+            final AttrCond cond,
+            final boolean not,
+            final Map<String, Object> parameters) {
+
+        CheckResult<AttrCond> checked = check(cond);
+
+        TextStringBuilder query = new TextStringBuilder("MATCH (n) ");
+        switch (cond.getType()) {
+            case ISNOTNULL ->
+                query.append("WHERE 
n.`plainAttrs.").append(checked.schema().getKey()).append("` IS NOT NULL");
+
+            case ISNULL ->
+                query.append("WHERE 
n.`plainAttrs.").append(checked.schema().getKey()).append("` IS NULL");
+
+            default ->
+                fillAttrQuery(query, checked.value(), checked.schema(), cond, 
not, parameters);
+        }
+
+        return new AttrCondQuery(query.toString(), checked.schema());
+    }
+
+    protected void getQueryForCustomConds(
+            final SearchCond cond,
+            final Map<String, Object> parameters,
+            final boolean not,
+            final TextStringBuilder query) {

Review Comment:
   ## Useless parameter
   
   The parameter 'query' is never used.
   
   [Show more 
details](https://github.com/apache/syncope/security/code-scanning/2580)



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to