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

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


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

commit bc135bd76aa40e72223e0c4422de010c3091503c
Author: Francesco Chicchiriccò <[email protected]>
AuthorDate: Tue Apr 1 17:42:50 2025 +0200

    [SYNCOPE-1871] Allow to search for Realms from multiple bases (#1042)
---
 .../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        | 17 +++--
 .../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  | 22 ++++--
 .../core/rest/cxf/service/RealmServiceImpl.java    |  2 +-
 .../syncope/core/persistence/api/dao/RealmDAO.java |  7 +-
 .../core/persistence/jpa/dao/JPARealmDAO.java      | 84 ++++++++++++----------
 .../core/persistence/jpa/inner/RealmTest.java      |  7 ++
 .../persistence/jpa/dao/ElasticsearchRealmDAO.java | 41 ++++++++---
 .../persistence/jpa/dao/OpenSearchRealmDAO.java    | 43 +++++++----
 .../syncope/fit/core/reference/TestCommand.java    |  3 +-
 .../org/apache/syncope/fit/core/RealmITCase.java   | 12 ++++
 19 files changed, 199 insertions(+), 94 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 09c2ab81ac..5276ae85b6 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).collect(Collectors.toList()).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 49be7e0733..f30c6022f0 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).collect(Collectors.toList()).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 42f107c507..a4931c2161 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).collect(Collectors.toList()).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 747bb0ff40..2a9f8011fa 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
@@ -327,7 +327,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 e825642f29..e81664325a 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,8 @@
  */
 package org.apache.syncope.client.console.commons;
 
+import java.util.List;
+import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.client.console.SyncopeConsoleSession;
 import org.apache.syncope.common.lib.SyncopeConstants;
@@ -41,12 +43,15 @@ 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().get(0));
-        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).collect(Collectors.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 412ec72df5..0485b1485e 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
@@ -453,7 +453,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 0a10d24c59..4a01cabc12 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
@@ -119,7 +119,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 e7e83a0cec..13dca07f9e 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
@@ -96,7 +96,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().filter(realm -> 
authRealms.stream().anyMatch(
                             authRealm -> 
realm.getFullPath().startsWith(authRealm))).
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 fc108a2a48..d37a130580 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
@@ -196,7 +196,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).collect(Collectors.toList()).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 9cd16b770e..fe65db7a4b 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
@@ -18,6 +18,12 @@
  */
 package org.apache.syncope.common.rest.api.beans;
 
+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 javax.ws.rs.QueryParam;
 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 70bf5f81a3..2d37f2d0f7 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.Optional;
@@ -29,6 +30,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;
@@ -59,6 +61,7 @@ import 
org.apache.syncope.core.spring.security.DelegatedAdministrationException;
 import org.identityconnectors.framework.common.objects.Attribute;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
 
 public class RealmLogic extends AbstractTransactionalLogic<RealmTO> {
 
@@ -114,16 +117,23 @@ public class RealmLogic extends 
AbstractTransactionalLogic<RealmTO> {
     @Transactional(readOnly = true)
     public Pair<Integer, List<RealmTO>> search(
             final String keyword,
-            final String base,
+            final Set<String> bases,
             final int page,
             final int size) {
 
-        Realm baseRealm = Optional.ofNullable(base == null ? 
realmDAO.getRoot() : realmDAO.findByFullPath(base)).
-                orElseThrow(() -> new NotFoundException(base));
+        Set<String> baseRealms = new HashSet<>();
+        if (CollectionUtils.isEmpty(bases)) {
+            baseRealms.add(SyncopeConstants.ROOT_REALM);
+        } else {
+            for (String base : bases) {
+                
baseRealms.add(Optional.ofNullable(realmDAO.findByFullPath(base)).map(Realm::getFullPath).
+                        orElseThrow(() -> new NotFoundException("Realm " + 
base)));
+            }
+        }
 
-        int count = realmDAO.countDescendants(baseRealm.getFullPath(), 
keyword);
+        int count = realmDAO.countDescendants(baseRealms, keyword);
 
-        List<Realm> result = realmDAO.findDescendants(baseRealm.getFullPath(), 
keyword, page, size);
+        List<Realm> result = realmDAO.findDescendants(baseRealms, keyword, 
page, size);
 
         return Pair.of(
                 count,
@@ -154,7 +164,7 @@ public class RealmLogic extends 
AbstractTransactionalLogic<RealmTO> {
             }
         }
 
-         
securityChecks(AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.REALM_CREATE),
 parent.getFullPath());
+        
securityChecks(AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.REALM_CREATE),
 parent.getFullPath());
 
         String fullPath = StringUtils.appendIfMissing(parent.getFullPath(), 
"/") + realmTO.getName();
         if (realmDAO.findByFullPath(fullPath) != null) {
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 8056149e3c..46e8b01316 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
@@ -47,7 +47,7 @@ public class RealmServiceImpl extends AbstractService 
implements RealmService {
     public PagedResult<RealmTO> search(final RealmQuery query) {
         Pair<Integer, List<RealmTO>> result = logic.search(
                 Optional.ofNullable(query.getKeyword()).map(k -> 
k.replace('*', '%')).orElse(null),
-                query.getBase(),
+                query.getBases(),
                 query.getPage(),
                 query.getSize());
         return buildPagedResult(result.getRight(), 1, 
result.getRight().size(), result.getLeft());
diff --git 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmDAO.java
 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmDAO.java
index a0a573caaa..fb26260149 100644
--- 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmDAO.java
+++ 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/RealmDAO.java
@@ -19,6 +19,7 @@
 package org.apache.syncope.core.persistence.api.dao;
 
 import java.util.List;
+import java.util.Set;
 import java.util.regex.Pattern;
 import org.apache.syncope.core.persistence.api.entity.ExternalResource;
 import org.apache.syncope.core.persistence.api.entity.Implementation;
@@ -43,10 +44,14 @@ public interface RealmDAO extends DAO<Realm> {
 
     int countDescendants(String base, String keyword);
 
+    int countDescendants(Set<String> bases, String keyword);
+
     List<Realm> findDescendants(String base, String keyword, int page, int 
itemsPerPage);
 
+    List<Realm> findDescendants(Set<String> bases, String keyword, int page, 
int itemsPerPage);
+
     List<String> findDescendants(String base, String prefix);
-    
+
     <T extends Policy> List<Realm> findByPolicy(T policy);
 
     List<Realm> findByLogicActions(Implementation logicActions);
diff --git 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java
 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java
index aa1f867155..d4d94ba874 100644
--- 
a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java
+++ 
b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java
@@ -20,6 +20,7 @@ package org.apache.syncope.core.persistence.jpa.dao;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 import java.util.stream.Collectors;
 import javax.persistence.NoResultException;
 import javax.persistence.Query;
@@ -50,6 +51,34 @@ import 
org.springframework.transaction.annotation.Transactional;
 
 public class JPARealmDAO extends AbstractDAO<Realm> implements RealmDAO {
 
+    protected static int setParameter(final List<Object> parameters, final 
Object parameter) {
+        parameters.add(parameter);
+        return parameters.size();
+    }
+
+    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 (").append(basesClause).append(')');
+
+        if (keyword != null) {
+            queryString.append(" AND LOWER(e.name) LIKE ?").
+                    append(setParameter(parameters, "%" + 
keyword.replaceAll("_", "\\\\_").toLowerCase() + "%"));
+        }
+
+        return queryString;
+    }
+
     protected final RoleDAO roleDAO;
 
     protected final ApplicationEventPublisher publisher;
@@ -123,37 +152,16 @@ public class JPARealmDAO extends AbstractDAO<Realm> 
implements RealmDAO {
         return query.getResultList();
     }
 
-    protected int setParameter(final List<Object> parameters, final Object 
parameter) {
-        parameters.add(parameter);
-        return parameters.size();
-    }
-
-    protected StringBuilder buildDescendantQuery(
-            final String base,
-            final String keyword,
-            final List<Object> parameters) {
-
-        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(')');
-
-        if (keyword != null) {
-            queryString.append(" AND LOWER(e.name) LIKE ?").
-                    append(setParameter(parameters, "%" + 
keyword.replaceAll("_", "\\\\_").toLowerCase() + "%"));
-        }
-
-        return queryString;
+    @Override
+    public int countDescendants(final String base, final String keyword) {
+        return countDescendants(Set.of(base), keyword);
     }
 
     @Override
-    public int countDescendants(final String base, final String keyword) {
+    public int 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 ",
@@ -173,9 +181,19 @@ public class JPARealmDAO extends AbstractDAO<Realm> 
implements RealmDAO {
             final int page,
             final int itemsPerPage) {
 
+        return findDescendants(Set.of(base), keyword, page, itemsPerPage);
+    }
+
+    @Override
+    public List<Realm> findDescendants(
+            final Set<String> bases,
+            final String keyword,
+            final int page,
+            final int itemsPerPage) {
+
         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);
 
@@ -196,7 +214,7 @@ public class JPARealmDAO extends AbstractDAO<Realm> 
implements RealmDAO {
     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)).
@@ -298,16 +316,6 @@ public class JPARealmDAO extends AbstractDAO<Realm> 
implements RealmDAO {
         return query.getResultList();
     }
 
-    protected StringBuilder buildDescendantQuery(final String base, final 
List<Object> parameters) {
-        return 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(" ORDER BY e.fullPath");
-    }
-
     protected String buildFullPath(final Realm realm) {
         return realm.getParent() == null
                 ? SyncopeConstants.ROOT_REALM
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 2115cf9250..3f48702336 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
@@ -27,6 +27,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.validation.InvalidEntityException;
@@ -99,6 +100,12 @@ public class RealmTest extends AbstractTest {
         assertNotNull(list);
         assertFalse(list.isEmpty());
         list.forEach(Assertions::assertNotNull);
+
+        list = realmDAO.findDescendants(Set.of("/even", "/odd"), null, -1, -1);
+        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-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchRealmDAO.java
 
b/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchRealmDAO.java
index 326426181d..2dc61a0a22 100644
--- 
a/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchRealmDAO.java
+++ 
b/ext/elasticsearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/ElasticsearchRealmDAO.java
@@ -29,7 +29,9 @@ 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.Set;
 import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.SyncopeConstants;
@@ -131,13 +133,16 @@ public class ElasticsearchRealmDAO extends JPARealmDAO {
         return result.stream().map(this::find).collect(Collectors.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 static 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;
@@ -167,9 +172,14 @@ public class ElasticsearchRealmDAO extends JPARealmDAO {
 
     @Override
     public int countDescendants(final String base, final String keyword) {
+        return countDescendants(Set.of(base), keyword);
+    }
+
+    @Override
+    public int 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 {
@@ -187,10 +197,20 @@ public class ElasticsearchRealmDAO extends JPARealmDAO {
             final int page,
             final int itemsPerPage) {
 
+        return findDescendants(Set.of(base), keyword, page, itemsPerPage);
+    }
+
+    @Override
+    public List<Realm> findDescendants(
+            final Set<String> bases,
+            final String keyword,
+            final int page,
+            final int itemsPerPage) {
+
         SearchRequest request = new SearchRequest.Builder().
                 
index(ElasticsearchUtils.getRealmIndex(AuthContextUtils.getDomain())).
                 searchType(SearchType.QueryThenFetch).
-                query(buildDescendantQuery(base, keyword)).
+                query(buildDescendantsQuery(bases, keyword)).
                 from(itemsPerPage * (page <= 0 ? 0 : page - 1)).
                 size(itemsPerPage < 0 ? indexMaxResultWindow : itemsPerPage).
                 sort(ES_SORT_OPTIONS_REALM).
@@ -218,8 +238,7 @@ public class ElasticsearchRealmDAO extends JPARealmDAO {
                         build()).build()).build()).build();
 
         Query query = new Query.Builder().bool(QueryBuilders.bool().must(
-                buildDescendantQuery(base, (String) null),
-                prefixQuery).build()).
+                buildDescendantsQuery(Set.of(base), null), 
prefixQuery).build()).
                 build();
 
         SearchRequest request = new SearchRequest.Builder().
diff --git 
a/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchRealmDAO.java
 
b/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchRealmDAO.java
index 082af104a2..23e0c88f43 100644
--- 
a/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchRealmDAO.java
+++ 
b/ext/opensearch/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OpenSearchRealmDAO.java
@@ -18,7 +18,9 @@
  */
 package org.apache.syncope.core.persistence.jpa.dao;
 
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.SyncopeConstants;
@@ -131,14 +133,16 @@ public class OpenSearchRealmDAO extends JPARealmDAO {
         return result.stream().map(this::find).collect(Collectors.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;
         }
@@ -167,15 +171,20 @@ public class OpenSearchRealmDAO extends JPARealmDAO {
 
     @Override
     public int countDescendants(final String base, final String keyword) {
+        return countDescendants(Set.of(base), keyword);
+    }
+
+    @Override
+    public int 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 {
             return (int) client.count(request).count();
         } catch (Exception e) {
-            LOG.error("While counting in OpenSearch", e);
+            LOG.error("While counting in Elasticsearch", e);
             return 0;
         }
     }
@@ -187,10 +196,20 @@ public class OpenSearchRealmDAO extends JPARealmDAO {
             final int page,
             final int itemsPerPage) {
 
+        return findDescendants(Set.of(base), keyword, page, itemsPerPage);
+    }
+
+    @Override
+    public List<Realm> findDescendants(
+            final Set<String> bases,
+            final String keyword,
+            final int page,
+            final int itemsPerPage) {
+
         SearchRequest request = new SearchRequest.Builder().
                 
index(OpenSearchUtils.getRealmIndex(AuthContextUtils.getDomain())).
                 searchType(SearchType.QueryThenFetch).
-                query(buildDescendantQuery(base, keyword)).
+                query(buildDescendantsQuery(bases, keyword)).
                 from(itemsPerPage * (page <= 0 ? 0 : page - 1)).
                 size(itemsPerPage < 0 ? indexMaxResultWindow : itemsPerPage).
                 sort(ES_SORT_OPTIONS_REALM).
@@ -218,7 +237,7 @@ public class OpenSearchRealmDAO extends JPARealmDAO {
                         build()).build()).build()).build();
 
         Query query = new Query.Builder().bool(QueryBuilders.bool().must(
-                buildDescendantQuery(base, (String) null),
+                buildDescendantsQuery(Set.of(base), null),
                 prefixQuery).build()).
                 build();
 
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 290b283e8b..6382d770c2 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;
@@ -43,7 +44,7 @@ public class TestCommand implements Command<TestCommandArgs> {
     private AnyObjectLogic anyObjectLogic;
 
     private Optional<RealmTO> getRealm(final String fullPath) {
-        return realmLogic.search(null, fullPath, -1, -1).getRight().stream().
+        return realmLogic.search(null, Set.of(fullPath), -1, 
-1).getRight().stream().
                 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 b7f5f73ff9..358a591e7c 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