This is an automated email from the ASF dual-hosted git repository.

ilgrosso pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/master by this push:
     new 10fc62dc38 [SYNCOPE-1871] Allow to search for Realms from multiple 
bases (#1041)
10fc62dc38 is described below

commit 10fc62dc38ba220e08f881933ff8ff9d6aa70524
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Tue Apr 1 16:46:42 2025 +0200

    [SYNCOPE-1871] Allow to search for Realms from multiple bases (#1041)
---
 .../clientapps/ClientAppModalPanelBuilder.java     |  2 +-
 .../client/console/status/ReconTaskPanel.java      |  2 +-
 .../wizards/resources/ConnectorDetailsPanel.java   |  2 +-
 .../client/console/SyncopeWebApplication.java      |  2 +-
 .../client/console/commons/RealmsUtils.java        | 15 +++++----
 .../client/console/panels/RealmChoicePanel.java    |  2 +-
 .../console/tasks/SchedTaskWizardBuilder.java      |  2 +-
 .../client/console/wizards/any/Details.java        |  2 +-
 .../console/wizards/role/RoleWizardBuilder.java    |  2 +-
 .../syncope/common/rest/api/beans/RealmQuery.java  | 39 ++++++++++++++++------
 .../org/apache/syncope/core/logic/RealmLogic.java  | 23 +++++++++----
 .../core/rest/cxf/service/RealmServiceImpl.java    |  2 +-
 .../core/persistence/api/dao/RealmSearchDAO.java   |  5 +++
 .../persistence/jpa/dao/JPARealmSearchDAO.java     | 34 +++++++++++++------
 .../core/persistence/jpa/inner/RealmTest.java      |  7 ++++
 .../persistence/neo4j/dao/Neo4jRealmSearchDAO.java | 39 +++++++++++++++-------
 .../core/persistence/neo4j/inner/RealmTest.java    |  7 ++++
 .../dao/ElasticsearchRealmSearchDAO.java           | 36 ++++++++++++++------
 .../opensearch/dao/OpenSearchRealmSearchDAO.java   | 37 +++++++++++++-------
 .../syncope/fit/core/reference/TestCommand.java    |  3 +-
 .../org/apache/syncope/fit/core/RealmITCase.java   | 12 +++++++
 21 files changed, 197 insertions(+), 78 deletions(-)

diff --git 
a/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppModalPanelBuilder.java
 
b/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppModalPanelBuilder.java
index 2c1322d906..4694247dc7 100644
--- 
a/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppModalPanelBuilder.java
+++ 
b/client/am/console/src/main/java/org/apache/syncope/client/console/clientapps/ClientAppModalPanelBuilder.java
@@ -192,7 +192,7 @@ public class ClientAppModalPanelBuilder<T extends 
ClientAppTO> extends AbstractM
                 @Override
                 protected Iterator<String> getChoices(final String input) {
                     return realmRestClient.search(fullRealmsTree
-                            ? RealmsUtils.buildRootQuery()
+                            ? RealmsUtils.buildBaseQuery()
                             : 
RealmsUtils.buildKeywordQuery(input)).getResult().stream().
                             map(RealmTO::getFullPath).iterator();
                 }
diff --git 
a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java
 
b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java
index 5503421af1..9851476317 100644
--- 
a/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java
+++ 
b/client/idm/console/src/main/java/org/apache/syncope/client/console/status/ReconTaskPanel.java
@@ -140,7 +140,7 @@ public class ReconTaskPanel extends 
MultilevelPanel.SecondLevel {
                 protected Iterator<String> getChoices(final String input) {
                     return (RealmsUtils.checkInput(input)
                             ? (realmRestClient.search(fullRealmsTree
-                                    ? RealmsUtils.buildRootQuery()
+                                    ? RealmsUtils.buildBaseQuery()
                                     : 
RealmsUtils.buildKeywordQuery(input)).getResult())
                             : List.<RealmTO>of()).stream().
                             map(RealmTO::getFullPath).iterator();
diff --git 
a/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java
 
b/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java
index b45698fb47..1918fa55d6 100644
--- 
a/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java
+++ 
b/client/idm/console/src/main/java/org/apache/syncope/client/console/wizards/resources/ConnectorDetailsPanel.java
@@ -68,7 +68,7 @@ public class ConnectorDetailsPanel extends WizardStep {
             protected Iterator<String> getChoices(final String input) {
                 return (RealmsUtils.checkInput(input)
                         ? (realmRestClient.search(fullRealmsTree
-                                ? RealmsUtils.buildRootQuery()
+                                ? RealmsUtils.buildBaseQuery()
                                 : 
RealmsUtils.buildKeywordQuery(input)).getResult())
                         : List.<RealmTO>of()).stream().
                         map(RealmTO::getFullPath).iterator();
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeWebApplication.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeWebApplication.java
index dd111445c3..b4003dd6d1 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeWebApplication.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/SyncopeWebApplication.java
@@ -316,7 +316,7 @@ public class SyncopeWebApplication extends 
WicketBootSecuredWebApplication {
             return false;
         }
 
-        RealmQuery query = RealmsUtils.buildRootQuery();
+        RealmQuery query = RealmsUtils.buildBaseQuery();
         query.setPage(1);
         query.setSize(0);
         return restClient.search(query).getTotalCount() < 
props.getRealmsFullTreeThreshold();
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/RealmsUtils.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/RealmsUtils.java
index f22df570c3..c578e59425 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/RealmsUtils.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/commons/RealmsUtils.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.client.console.commons;
 
+import java.util.List;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.client.console.SyncopeConsoleSession;
 import org.apache.syncope.common.lib.SyncopeConstants;
@@ -41,12 +42,14 @@ public final class RealmsUtils {
         return new RealmQuery.Builder().keyword(input.contains("*") ? input : 
"*" + input + "*").build();
     }
 
-    public static RealmQuery buildRootQuery() {
-        String base = 
SyncopeConsoleSession.get().getSearchableRealms().isEmpty()
-                || 
SyncopeConsoleSession.get().getSearchableRealms().contains(SyncopeConstants.ROOT_REALM)
-                ? SyncopeConstants.ROOT_REALM
-                : 
getFullPath(SyncopeConsoleSession.get().getSearchableRealms().getFirst());
-        return new RealmQuery.Builder().base(base).build();
+    public static RealmQuery buildBaseQuery() {
+        List<String> realms = 
SyncopeConsoleSession.get().getSearchableRealms();
+
+        if (realms.isEmpty() || realms.contains(SyncopeConstants.ROOT_REALM)) {
+            return new 
RealmQuery.Builder().base(SyncopeConstants.ROOT_REALM).build();
+        }
+
+        return new 
RealmQuery.Builder().bases(realms.stream().map(RealmsUtils::getFullPath).toList()).build();
     }
 
     private RealmsUtils() {
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java
index def5538df9..401fe87677 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java
@@ -454,7 +454,7 @@ public class RealmChoicePanel extends Panel {
 
     protected Map<String, Pair<RealmTO, List<RealmTO>>> reloadRealmParentMap() 
{
         List<RealmTO> realmsToList = realmRestClient.search(fullRealmsTree
-                ? RealmsUtils.buildRootQuery()
+                ? RealmsUtils.buildBaseQuery()
                 : RealmsUtils.buildKeywordQuery(searchQuery)).getResult();
 
         return reloadRealmParentMap(realmsToList.stream().
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java
index dbf73c9eca..370aa053e1 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/tasks/SchedTaskWizardBuilder.java
@@ -123,7 +123,7 @@ public class SchedTaskWizardBuilder<T extends SchedTaskTO> 
extends BaseAjaxWizar
 
     protected List<String> searchRealms(final String realmQuery) {
         return realmRestClient.search(fullRealmsTree
-                ? RealmsUtils.buildRootQuery()
+                ? RealmsUtils.buildBaseQuery()
                 : RealmsUtils.buildKeywordQuery(realmQuery)).
                 
getResult().stream().map(RealmTO::getFullPath).collect(Collectors.toList());
     }
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java
index c2c09a2415..2cf541b1c0 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/any/Details.java
@@ -95,7 +95,7 @@ public class Details<T extends AnyTO> extends WizardStep {
                     return (pageRef.getPage() instanceof Realms
                             ? 
getRealmsFromLinks(Realms.class.cast(pageRef.getPage()).getRealmChoicePanel().getLinks())
                             : (fullRealmsTree
-                                    ? 
realmRestClient.search(RealmsUtils.buildRootQuery())
+                                    ? 
realmRestClient.search(RealmsUtils.buildBaseQuery())
                                     : 
realmRestClient.search(RealmsUtils.buildKeywordQuery(input))).getResult()).
                             stream().map(RealmTO::getFullPath).
                         filter(fullPath -> authRealms.stream().anyMatch(
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/role/RoleWizardBuilder.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/role/RoleWizardBuilder.java
index bab9d537bb..28abc47ef3 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/role/RoleWizardBuilder.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/wizards/role/RoleWizardBuilder.java
@@ -189,7 +189,7 @@ public class RoleWizardBuilder extends 
BaseAjaxWizardBuilder<RoleWrapper> {
                 @Override
                 protected Iterator<String> getChoices(final String input) {
                     return realmRestClient.search(fullRealmsTree
-                            ? RealmsUtils.buildRootQuery()
+                            ? RealmsUtils.buildBaseQuery()
                             : 
RealmsUtils.buildKeywordQuery(input)).getResult().stream().
                             map(RealmTO::getFullPath).iterator();
                 }
diff --git 
a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/RealmQuery.java
 
b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/RealmQuery.java
index 0858b35680..badd154610 100644
--- 
a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/RealmQuery.java
+++ 
b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/RealmQuery.java
@@ -19,6 +19,12 @@
 package org.apache.syncope.common.rest.api.beans;
 
 import jakarta.ws.rs.QueryParam;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.apache.commons.lang3.builder.EqualsBuilder;
 import org.apache.commons.lang3.builder.HashCodeBuilder;
 
@@ -38,15 +44,28 @@ public class RealmQuery extends AbstractQuery {
             return this;
         }
 
-        public Builder base(final String base) {
-            getInstance().setBase(base);
+        public Builder base(final String... bases) {
+            if (bases != null) {
+                Set<String> b = 
Optional.ofNullable(getInstance().getBases()).orElseGet(HashSet::new);
+                b.addAll(Stream.of(bases).collect(Collectors.toSet()));
+                getInstance().setBases(b);
+            }
+            return this;
+        }
+
+        public Builder bases(final Collection<String> bases) {
+            if (bases != null) {
+                Set<String> b = 
Optional.ofNullable(getInstance().getBases()).orElseGet(HashSet::new);
+                b.addAll(bases);
+                getInstance().setBases(b);
+            }
             return this;
         }
     }
 
     private String keyword;
 
-    private String base;
+    private Set<String> bases;
 
     public String getKeyword() {
         return keyword;
@@ -57,13 +76,13 @@ public class RealmQuery extends AbstractQuery {
         this.keyword = keyword;
     }
 
-    public String getBase() {
-        return base;
+    public Set<String> getBases() {
+        return bases;
     }
 
-    @QueryParam("base")
-    public void setBase(final String base) {
-        this.base = base;
+    @QueryParam("bases")
+    public void setBases(final Set<String> bases) {
+        this.bases = bases;
     }
 
     @Override
@@ -81,7 +100,7 @@ public class RealmQuery extends AbstractQuery {
         return new EqualsBuilder().
                 appendSuper(super.equals(obj)).
                 append(keyword, other.keyword).
-                append(base, other.base).
+                append(bases, other.bases).
                 build();
     }
 
@@ -90,7 +109,7 @@ public class RealmQuery extends AbstractQuery {
         return new HashCodeBuilder().
                 appendSuper(super.hashCode()).
                 append(keyword).
-                append(base).
+                append(bases).
                 build();
     }
 }
diff --git 
a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java 
b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java
index 68bcedbf1f..e2f6822cfa 100644
--- 
a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java
+++ 
b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java
@@ -20,6 +20,7 @@ package org.apache.syncope.core.logic;
 
 import java.lang.reflect.Method;
 import java.util.Comparator;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -27,6 +28,7 @@ import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.to.ProvisioningResult;
 import org.apache.syncope.common.lib.to.RealmTO;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
@@ -61,6 +63,7 @@ import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
 
 public class RealmLogic extends AbstractTransactionalLogic<RealmTO> {
 
@@ -120,18 +123,24 @@ public class RealmLogic extends 
AbstractTransactionalLogic<RealmTO> {
     @Transactional(readOnly = true)
     public Page<RealmTO> search(
             final String keyword,
-            final String base,
+            final Set<String> bases,
             final Pageable pageable) {
 
-        Realm baseRealm = base == null
-                ? realmDAO.getRoot()
-                : realmSearchDAO.findByFullPath(base).orElseThrow(() -> new 
NotFoundException("Realm " + base));
+        Set<String> baseRealms = new HashSet<>();
+        if (CollectionUtils.isEmpty(bases)) {
+            baseRealms.add(SyncopeConstants.ROOT_REALM);
+        } else {
+            for (String base : bases) {
+                
baseRealms.add(realmSearchDAO.findByFullPath(base).map(Realm::getFullPath).
+                        orElseThrow(() -> new NotFoundException("Realm " + 
base)));
+            }
+        }
 
-        long count = realmSearchDAO.countDescendants(baseRealm.getFullPath(), 
keyword);
+        long count = realmSearchDAO.countDescendants(baseRealms, keyword);
 
         Set<String> authorizations = AuthContextUtils.getAuthorizations().
-            getOrDefault(IdRepoEntitlement.REALM_SEARCH, Set.of());
-        List<RealmTO> result = 
realmSearchDAO.findDescendants(baseRealm.getFullPath(), keyword, 
pageable).stream().
+                getOrDefault(IdRepoEntitlement.REALM_SEARCH, Set.of());
+        List<RealmTO> result = realmSearchDAO.findDescendants(baseRealms, 
keyword, pageable).stream().
                 map(realm -> binder.getRealmTO(
                 realm, authorizations.stream().
                         anyMatch(auth -> 
realm.getFullPath().startsWith(auth)))).
diff --git 
a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/RealmServiceImpl.java
 
b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/RealmServiceImpl.java
index 44fb4d8b0f..1bf32e6086 100644
--- 
a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/RealmServiceImpl.java
+++ 
b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/RealmServiceImpl.java
@@ -44,7 +44,7 @@ public class RealmServiceImpl extends AbstractService 
implements RealmService {
     public PagedResult<RealmTO> search(final RealmQuery query) {
         Page<RealmTO> result = logic.search(
                 Optional.ofNullable(query.getKeyword()).map(k -> 
k.replace('*', '%')).orElse(null),
-                query.getBase(),
+                query.getBases(),
                 pageable(query));
         return buildPagedResult(result);
     }
diff --git 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmSearchDAO.java
 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmSearchDAO.java
index 9c968c0bf7..3b3a740b5f 100644
--- 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmSearchDAO.java
+++ 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmSearchDAO.java
@@ -21,6 +21,7 @@ package org.apache.syncope.core.persistence.api.dao;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.springframework.data.domain.Pageable;
 
@@ -34,8 +35,12 @@ public interface RealmSearchDAO {
 
     long countDescendants(String base, String keyword);
 
+    long countDescendants(Set<String> bases, String keyword);
+
     List<Realm> findDescendants(String base, String keyword, Pageable 
pageable);
 
+    List<Realm> findDescendants(Set<String> bases, String keyword, Pageable 
pageable);
+
     List<String> findDescendants(String base, String prefix);
 
     default void findAncestors(final List<Realm> result, final Realm realm) {
diff --git 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmSearchDAO.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmSearchDAO.java
index 461f393384..1124660fc9 100644
--- 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmSearchDAO.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmSearchDAO.java
@@ -25,6 +25,8 @@ import jakarta.persistence.TypedQuery;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.core.persistence.api.dao.MalformedPathException;
@@ -46,18 +48,20 @@ public class JPARealmSearchDAO implements RealmSearchDAO {
         return parameters.size();
     }
 
-    protected static StringBuilder buildDescendantQuery(
-            final String base,
+    protected static StringBuilder buildDescendantsQuery(
+            final Set<String> bases,
             final String keyword,
             final List<Object> parameters) {
 
+        String basesClause = bases.stream().
+                map(base -> "e.fullPath=?" + setParameter(parameters, base)
+                + " OR e.fullPath LIKE ?" + setParameter(
+                        parameters, SyncopeConstants.ROOT_REALM.equals(base) ? 
"/%" : base + "/%")).
+                collect(Collectors.joining(" OR "));
+
         StringBuilder queryString = new StringBuilder("SELECT e FROM ").
                 append(JPARealm.class.getSimpleName()).append(" e ").
-                append("WHERE (e.fullPath=?").
-                append(setParameter(parameters, base)).
-                append(" OR e.fullPath LIKE ?").
-                append(setParameter(parameters, 
SyncopeConstants.ROOT_REALM.equals(base) ? "/%" : base + "/%")).
-                append(')');
+                append("WHERE (").append(basesClause).append(')');
 
         if (keyword != null) {
             queryString.append(" AND LOWER(e.name) LIKE ?").
@@ -117,9 +121,14 @@ public class JPARealmSearchDAO implements RealmSearchDAO {
 
     @Override
     public long countDescendants(final String base, final String keyword) {
+        return countDescendants(Set.of(base), keyword);
+    }
+
+    @Override
+    public long countDescendants(final Set<String> bases, final String 
keyword) {
         List<Object> parameters = new ArrayList<>();
 
-        StringBuilder queryString = buildDescendantQuery(base, keyword, 
parameters);
+        StringBuilder queryString = buildDescendantsQuery(bases, keyword, 
parameters);
         Query query = entityManager.createQuery(StringUtils.replaceOnce(
                 queryString.toString(),
                 "SELECT e ",
@@ -134,9 +143,14 @@ public class JPARealmSearchDAO implements RealmSearchDAO {
 
     @Override
     public List<Realm> findDescendants(final String base, final String 
keyword, final Pageable pageable) {
+        return findDescendants(Set.of(base), keyword, pageable);
+    }
+
+    @Override
+    public List<Realm> findDescendants(final Set<String> bases, final String 
keyword, final Pageable pageable) {
         List<Object> parameters = new ArrayList<>();
 
-        StringBuilder queryString = buildDescendantQuery(base, keyword, 
parameters);
+        StringBuilder queryString = buildDescendantsQuery(bases, keyword, 
parameters);
         TypedQuery<Realm> query = entityManager.createQuery(
                 queryString.append(" ORDER BY e.fullPath").toString(), 
Realm.class);
 
@@ -156,7 +170,7 @@ public class JPARealmSearchDAO implements RealmSearchDAO {
     public List<String> findDescendants(final String base, final String 
prefix) {
         List<Object> parameters = new ArrayList<>();
 
-        StringBuilder queryString = buildDescendantQuery(base, null, 
parameters);
+        StringBuilder queryString = buildDescendantsQuery(Set.of(base), null, 
parameters);
         TypedQuery<Realm> query = entityManager.createQuery(queryString.
                 append(" AND (e.fullPath=?").
                 append(setParameter(parameters, prefix)).
diff --git 
a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RealmTest.java
 
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RealmTest.java
index 65dd66d28a..5dddbdb6cb 100644
--- 
a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RealmTest.java
+++ 
b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RealmTest.java
@@ -26,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
 import java.util.List;
+import java.util.Set;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.types.EntityViolationType;
 import 
org.apache.syncope.core.persistence.api.attrvalue.InvalidEntityException;
@@ -103,6 +104,12 @@ public class RealmTest extends AbstractTest {
         list.forEach(Assertions::assertNotNull);
 
         assertEquals(4, realmDAO.findAll(Pageable.unpaged()).stream().count());
+
+        list = realmSearchDAO.findDescendants(Set.of("/even", "/odd"), null, 
Pageable.unpaged());
+        assertEquals(3, list.size());
+        assertNotNull(list.stream().filter(realm -> 
"even".equals(realm.getName())).findFirst().orElseThrow());
+        assertNotNull(list.stream().filter(realm -> 
"two".equals(realm.getName())).findFirst().orElseThrow());
+        assertNotNull(list.stream().filter(realm -> 
"odd".equals(realm.getName())).findFirst().orElseThrow());
     }
 
     @Test
diff --git 
a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java
 
b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java
index f7513d57f4..753879b62d 100644
--- 
a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java
+++ 
b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java
@@ -22,6 +22,9 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
 import javax.cache.Cache;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.SyncopeConstants;
@@ -31,8 +34,6 @@ import 
org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
 import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.persistence.neo4j.entity.EntityCacheKey;
 import org.apache.syncope.core.persistence.neo4j.entity.Neo4jRealm;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.neo4j.core.Neo4jClient;
 import org.springframework.data.neo4j.core.Neo4jTemplate;
@@ -40,17 +41,21 @@ import 
org.springframework.transaction.annotation.Transactional;
 
 public class Neo4jRealmSearchDAO extends AbstractDAO implements RealmSearchDAO 
{
 
-    protected static final Logger LOG = 
LoggerFactory.getLogger(RealmSearchDAO.class);
-
-    protected static StringBuilder buildDescendantQuery(
-            final String base,
+    protected static StringBuilder buildDescendantsQuery(
+            final Set<String> bases,
             final String keyword,
             final Map<String, Object> parameters) {
 
+        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 "));
+
         StringBuilder queryString = new StringBuilder("MATCH 
(n:").append(Neo4jRealm.NODE).append(") ").
-                append("WHERE (n.fullPath = $base OR n.fullPath =~ $like)");
-        parameters.put("base", base);
-        parameters.put("like", SyncopeConstants.ROOT_REALM.equals(base) ? 
"/.*" : base + "/.*");
+                append("WHERE (").append(basesClause).append(')');
 
         if (keyword != null) {
             queryString.append(" AND toLower(n.name) =~ $name");
@@ -103,17 +108,27 @@ public class Neo4jRealmSearchDAO extends AbstractDAO 
implements RealmSearchDAO {
 
     @Override
     public long countDescendants(final String base, final String keyword) {
+        return countDescendants(Set.of(base), keyword);
+    }
+
+    @Override
+    public long countDescendants(final Set<String> bases, final String 
keyword) {
         Map<String, Object> parameters = new HashMap<>();
 
-        StringBuilder queryString = buildDescendantQuery(base, keyword, 
parameters).append(" RETURN COUNT(n)");
+        StringBuilder queryString = buildDescendantsQuery(bases, keyword, 
parameters).append(" RETURN COUNT(n)");
         return neo4jTemplate.count(queryString.toString(), parameters);
     }
 
     @Override
     public List<Realm> findDescendants(final String base, final String 
keyword, final Pageable pageable) {
+        return findDescendants(Set.of(base), keyword, pageable);
+    }
+
+    @Override
+    public List<Realm> findDescendants(final Set<String> bases, final String 
keyword, final Pageable pageable) {
         Map<String, Object> parameters = new HashMap<>();
 
-        StringBuilder queryString = buildDescendantQuery(base, keyword, 
parameters).
+        StringBuilder queryString = buildDescendantsQuery(bases, keyword, 
parameters).
                 append(" RETURN n.id ORDER BY n.fullPath");
         if (pageable.isPaged()) {
             queryString.append(" SKIP ").append(pageable.getPageSize() * 
pageable.getPageNumber()).
@@ -128,7 +143,7 @@ public class Neo4jRealmSearchDAO extends AbstractDAO 
implements RealmSearchDAO {
     public List<String> findDescendants(final String base, final String 
prefix) {
         Map<String, Object> parameters = new HashMap<>();
 
-        StringBuilder queryString = buildDescendantQuery(base, null, 
parameters).
+        StringBuilder queryString = buildDescendantsQuery(Set.of(base), null, 
parameters).
                 append(" AND (n.fullPath = $prefix OR n.fullPath =~ 
$likePrefix)").
                 append(" RETURN n.id ORDER BY n.fullPath");
         parameters.put("prefix", prefix);
diff --git 
a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/RealmTest.java
 
b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/RealmTest.java
index 09b5580753..578cb3f891 100644
--- 
a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/RealmTest.java
+++ 
b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/RealmTest.java
@@ -26,6 +26,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
 import java.util.List;
+import java.util.Set;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.types.EntityViolationType;
 import 
org.apache.syncope.core.persistence.api.attrvalue.InvalidEntityException;
@@ -115,6 +116,12 @@ public class RealmTest extends AbstractTest {
         list.forEach(Assertions::assertNotNull);
 
         assertEquals(4, 
realmDAO.findAll(Pageable.ofSize(100)).stream().count());
+
+        list = realmSearchDAO.findDescendants(Set.of("/even", "/odd"), null, 
Pageable.unpaged());
+        assertEquals(3, list.size());
+        assertNotNull(list.stream().filter(realm -> 
"even".equals(realm.getName())).findFirst().orElseThrow());
+        assertNotNull(list.stream().filter(realm -> 
"two".equals(realm.getName())).findFirst().orElseThrow());
+        assertNotNull(list.stream().filter(realm -> 
"odd".equals(realm.getName())).findFirst().orElseThrow());
     }
 
     @Test
diff --git 
a/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAO.java
 
b/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAO.java
index 2208ad5c6a..265409d09c 100644
--- 
a/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAO.java
+++ 
b/ext/elasticsearch/persistence/src/main/java/org/apache/syncope/core/persistence/elasticsearch/dao/ElasticsearchRealmSearchDAO.java
@@ -29,8 +29,10 @@ import 
co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders;
 import co.elastic.clients.elasticsearch.core.CountRequest;
 import co.elastic.clients.elasticsearch.core.SearchRequest;
 import co.elastic.clients.elasticsearch.core.search.Hit;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.core.persistence.api.dao.MalformedPathException;
@@ -139,13 +141,16 @@ public class ElasticsearchRealmSearchDAO implements 
RealmSearchDAO {
                 flatMap(Optional::stream).map(Realm.class::cast).toList();
     }
 
-    protected Query buildDescendantQuery(final String base, final String 
keyword) {
-        Query prefix = new 
Query.Builder().disMax(QueryBuilders.disMax().queries(
-                new Query.Builder().term(QueryBuilders.term().
-                        field("fullPath").value(base).build()).build(),
-                new Query.Builder().regexp(QueryBuilders.regexp().
-                        
field("fullPath").value(SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base 
+ "/.*").
-                        build()).build()).build()).build();
+    protected Query buildDescendantsQuery(final Set<String> bases, final 
String keyword) {
+        List<Query> basesQueries = new ArrayList<>();
+        bases.forEach(base -> {
+            basesQueries.add(new Query.Builder().term(QueryBuilders.term().
+                    field("fullPath").value(base).build()).build());
+            basesQueries.add(new Query.Builder().regexp(QueryBuilders.regexp().
+                    
field("fullPath").value(SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base 
+ "/.*").
+                    build()).build());
+        });
+        Query prefix = new 
Query.Builder().disMax(QueryBuilders.disMax().queries(basesQueries).build()).build();
 
         if (keyword == null) {
             return prefix;
@@ -175,9 +180,14 @@ public class ElasticsearchRealmSearchDAO implements 
RealmSearchDAO {
 
     @Override
     public long countDescendants(final String base, final String keyword) {
+        return countDescendants(Set.of(base), keyword);
+    }
+
+    @Override
+    public long countDescendants(final Set<String> bases, final String 
keyword) {
         CountRequest request = new CountRequest.Builder().
                 
index(ElasticsearchUtils.getRealmIndex(AuthContextUtils.getDomain())).
-                query(buildDescendantQuery(base, keyword)).
+                query(buildDescendantsQuery(bases, keyword)).
                 build();
 
         try {
@@ -190,10 +200,15 @@ public class ElasticsearchRealmSearchDAO implements 
RealmSearchDAO {
 
     @Override
     public List<Realm> findDescendants(final String base, final String 
keyword, final Pageable pageable) {
+        return findDescendants(Set.of(base), keyword, pageable);
+    }
+
+    @Override
+    public List<Realm> findDescendants(final Set<String> bases, final String 
keyword, final Pageable pageable) {
         SearchRequest request = new SearchRequest.Builder().
                 
index(ElasticsearchUtils.getRealmIndex(AuthContextUtils.getDomain())).
                 searchType(SearchType.QueryThenFetch).
-                query(buildDescendantQuery(base, keyword)).
+                query(buildDescendantsQuery(bases, keyword)).
                 from(pageable.isUnpaged() ? 0 : pageable.getPageSize() * 
pageable.getPageNumber()).
                 size(pageable.isUnpaged() ? indexMaxResultWindow : 
pageable.getPageSize()).
                 sort(REALM_SORT_OPTIONS).
@@ -222,8 +237,7 @@ public class ElasticsearchRealmSearchDAO implements 
RealmSearchDAO {
                         build()).build()).build()).build();
 
         Query query = new Query.Builder().bool(QueryBuilders.bool().must(
-                buildDescendantQuery(base, (String) null),
-                prefixQuery).build()).
+                buildDescendantsQuery(Set.of(base), (String) null), 
prefixQuery).build()).
                 build();
 
         SearchRequest request = new SearchRequest.Builder().
diff --git 
a/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAO.java
 
b/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAO.java
index d882119faa..21e88c9eed 100644
--- 
a/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAO.java
+++ 
b/ext/opensearch/persistence/src/main/java/org/apache/syncope/core/persistence/opensearch/dao/OpenSearchRealmSearchDAO.java
@@ -18,8 +18,10 @@
  */
 package org.apache.syncope.core.persistence.opensearch.dao;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.core.persistence.api.dao.MalformedPathException;
@@ -139,14 +141,16 @@ public class OpenSearchRealmSearchDAO implements 
RealmSearchDAO {
                 flatMap(Optional::stream).map(Realm.class::cast).toList();
     }
 
-    protected Query buildDescendantQuery(final String base, final String 
keyword) {
-        Query prefix = new 
Query.Builder().disMax(QueryBuilders.disMax().queries(
-                new Query.Builder().term(QueryBuilders.term().
-                        
field("fullPath").value(FieldValue.of(base)).build()).build(),
-                new Query.Builder().regexp(QueryBuilders.regexp().
-                        
field("fullPath").value(SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base 
+ "/.*").
-                        build()).build()).build()).build();
-
+    protected Query buildDescendantsQuery(final Set<String> bases, final 
String keyword) {
+        List<Query> basesQueries = new ArrayList<>();
+        bases.forEach(base -> {
+            basesQueries.add(new Query.Builder().term(QueryBuilders.term().
+                    
field("fullPath").value(FieldValue.of(base)).build()).build());
+            basesQueries.add(new Query.Builder().regexp(QueryBuilders.regexp().
+                    
field("fullPath").value(SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base 
+ "/.*").
+                    build()).build());
+        });
+        Query prefix = new 
Query.Builder().disMax(QueryBuilders.disMax().queries(basesQueries).build()).build();
         if (keyword == null) {
             return prefix;
         }
@@ -175,9 +179,14 @@ public class OpenSearchRealmSearchDAO implements 
RealmSearchDAO {
 
     @Override
     public long countDescendants(final String base, final String keyword) {
+        return countDescendants(Set.of(base), keyword);
+    }
+
+    @Override
+    public long countDescendants(final Set<String> bases, final String 
keyword) {
         CountRequest request = new CountRequest.Builder().
                 
index(OpenSearchUtils.getRealmIndex(AuthContextUtils.getDomain())).
-                query(buildDescendantQuery(base, keyword)).
+                query(buildDescendantsQuery(bases, keyword)).
                 build();
 
         try {
@@ -190,10 +199,15 @@ public class OpenSearchRealmSearchDAO implements 
RealmSearchDAO {
 
     @Override
     public List<Realm> findDescendants(final String base, final String 
keyword, final Pageable pageable) {
+        return findDescendants(Set.of(base), keyword, pageable);
+    }
+
+    @Override
+    public List<Realm> findDescendants(final Set<String> bases, final String 
keyword, final Pageable pageable) {
         SearchRequest request = new SearchRequest.Builder().
                 
index(OpenSearchUtils.getRealmIndex(AuthContextUtils.getDomain())).
                 searchType(SearchType.QueryThenFetch).
-                query(buildDescendantQuery(base, keyword)).
+                query(buildDescendantsQuery(bases, keyword)).
                 from(pageable.isUnpaged() ? 0 : pageable.getPageSize() * 
pageable.getPageNumber()).
                 size(pageable.isUnpaged() ? indexMaxResultWindow : 
pageable.getPageSize()).
                 sort(REALM_SORT_OPTIONS).
@@ -222,8 +236,7 @@ public class OpenSearchRealmSearchDAO implements 
RealmSearchDAO {
                         build()).build()).build()).build();
 
         Query query = new Query.Builder().bool(QueryBuilders.bool().must(
-                buildDescendantQuery(base, null),
-                prefixQuery).build()).
+                buildDescendantsQuery(Set.of(base), (String) null), 
prefixQuery).build()).
                 build();
 
         SearchRequest request = new SearchRequest.Builder().
diff --git 
a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestCommand.java
 
b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestCommand.java
index 462570ab80..ae5d83859e 100644
--- 
a/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestCommand.java
+++ 
b/fit/core-reference/src/main/java/org/apache/syncope/fit/core/reference/TestCommand.java
@@ -19,6 +19,7 @@
 package org.apache.syncope.fit.core.reference;
 
 import java.util.Optional;
+import java.util.Set;
 import org.apache.syncope.common.lib.Attr;
 import org.apache.syncope.common.lib.request.AnyObjectCR;
 import org.apache.syncope.common.lib.to.AnyObjectTO;
@@ -44,7 +45,7 @@ public class TestCommand implements Command<TestCommandArgs> {
     private AnyObjectLogic anyObjectLogic;
 
     private Optional<RealmTO> getRealm(final String fullPath) {
-        return realmLogic.search(null, fullPath, Pageable.unpaged()).get().
+        return realmLogic.search(null, Set.of(fullPath), 
Pageable.unpaged()).get().
                 filter(realm -> 
fullPath.equals(realm.getFullPath())).findFirst();
     }
 
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RealmITCase.java 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RealmITCase.java
index 219e82b5df..1c29a6b472 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RealmITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RealmITCase.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.fit.core;
 
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -465,4 +466,15 @@ public class RealmITCase extends AbstractITCase {
             ROLE_SERVICE.delete(role.getKey());
         }
     }
+
+    @Test
+    public void issueSYNCOPE1871() {
+        PagedResult<RealmTO> result = REALM_SERVICE.search(new 
RealmQuery.Builder().base("/odd").base("/even").build());
+        assertDoesNotThrow(() -> result.getResult().stream().
+                filter(r -> 
"odd".equals(r.getName())).findFirst().orElseThrow());
+        assertDoesNotThrow(() -> result.getResult().stream().
+                filter(r -> 
"even".equals(r.getName())).findFirst().orElseThrow());
+        assertDoesNotThrow(() -> result.getResult().stream().
+                filter(r -> 
"two".equals(r.getName())).findFirst().orElseThrow());
+    }
 }


Reply via email to