From 0311ed78c38fb021108c150d6859154c1fd8e42e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Chicchiricc=C3=B2?= Date: Fri, 17 Jan 2025 19:14:35 +0100 Subject: [PATCH 1/9] Rewriting JPAAnySearchDAO to reduce subqueries --- core/persistence-jpa/pom.xml | 16 +- .../persistence/jpa/PersistenceContext.java | 4 +- .../persistence/jpa/dao/AnySearchNode.java | 151 +++ .../persistence/jpa/dao/AnySearchNodeDAO.java | 1123 +++++++++++++++++ .../persistence/jpa/inner/AnySearchTest.java | 9 +- 5 files changed, 1288 insertions(+), 15 deletions(-) create mode 100644 core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnySearchNode.java create mode 100644 core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnySearchNodeDAO.java diff --git a/core/persistence-jpa/pom.xml b/core/persistence-jpa/pom.xml index 3f18cfc0f50..ba23eb7215e 100644 --- a/core/persistence-jpa/pom.xml +++ b/core/persistence-jpa/pom.xml @@ -118,23 +118,23 @@ under the License. test - org.springframework - spring-test + org.bouncycastle + bcpkix-jdk15on test - org.junit.jupiter - junit-jupiter + org.bouncycastle + bcprov-jdk15on test - org.bouncycastle - bcpkix-jdk15on + org.springframework + spring-test test - org.bouncycastle - bcprov-jdk15on + org.junit.jupiter + junit-jupiter test diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java index 33f1e234c12..fae7fac124e 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java @@ -85,10 +85,10 @@ import org.apache.syncope.core.persistence.jpa.content.KeymasterConfParamLoader; import org.apache.syncope.core.persistence.jpa.content.XMLContentExporter; import org.apache.syncope.core.persistence.jpa.content.XMLContentLoader; +import org.apache.syncope.core.persistence.jpa.dao.AnySearchNodeDAO; import org.apache.syncope.core.persistence.jpa.dao.JPAAccessTokenDAO; import org.apache.syncope.core.persistence.jpa.dao.JPAAnyMatchDAO; import org.apache.syncope.core.persistence.jpa.dao.JPAAnyObjectDAO; -import org.apache.syncope.core.persistence.jpa.dao.JPAAnySearchDAO; import org.apache.syncope.core.persistence.jpa.dao.JPAAnyTypeClassDAO; import org.apache.syncope.core.persistence.jpa.dao.JPAAnyTypeDAO; import org.apache.syncope.core.persistence.jpa.dao.JPAApplicationDAO; @@ -384,7 +384,7 @@ public AnySearchDAO anySearchDAO( final AnyUtilsFactory anyUtilsFactory, final PlainAttrValidationManager validator) { - return new JPAAnySearchDAO( + return new AnySearchNodeDAO( realmDAO, dynRealmDAO, userDAO, diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnySearchNode.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnySearchNode.java new file mode 100644 index 00000000000..404593b7ea5 --- /dev/null +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnySearchNode.java @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.core.persistence.jpa.dao; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +public class AnySearchNode { + + enum Type { + AND, + OR, + LEAF + + } + + public static class Leaf extends AnySearchNode { + + private final SearchSupport.SearchView from; + + private final String clause; + + protected Leaf(final SearchSupport.SearchView from, final String clause) { + super(Type.LEAF); + this.from = from; + this.clause = clause; + } + + public SearchSupport.SearchView getFrom() { + return from; + } + + public String getClause() { + return clause; + } + + @Override + public int hashCode() { + return new HashCodeBuilder(). + appendSuper(super.hashCode()). + append(from). + append(clause). + build(); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Leaf other = (Leaf) obj; + + return new EqualsBuilder(). + appendSuper(super.equals(obj)). + append(from, other.from). + append(clause, other.clause). + build(); + } + + @Override + public String toString() { + return "LeafNode{" + "from=" + from + ", clause=" + clause + '}'; + } + } + + private final Type type; + + private final List children = new ArrayList<>(); + + protected AnySearchNode(final Type type) { + this.type = type; + } + + protected Type getType() { + return type; + } + + protected boolean add(final AnySearchNode child) { + if (type == Type.LEAF) { + throw new IllegalArgumentException("Cannot add children to a leaf node"); + } + return children.add(child); + } + + protected List getChildren() { + return children; + } + + protected Optional asLeaf() { + return type == Type.LEAF + ? Optional.of((Leaf) this) + : Optional.empty(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(). + append(type). + append(children). + build(); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final AnySearchNode other = (AnySearchNode) obj; + + return new EqualsBuilder(). + append(type, other.type). + append(children, other.children). + build(); + } + + @Override + public String toString() { + return "Node{" + "type=" + type + ", children=" + children + '}'; + } +} diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnySearchNodeDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnySearchNodeDAO.java new file mode 100644 index 00000000000..752d8bea53b --- /dev/null +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnySearchNodeDAO.java @@ -0,0 +1,1123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.syncope.core.persistence.jpa.dao; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import javax.persistence.Query; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.lang3.tuple.Triple; +import org.apache.syncope.common.lib.SyncopeClientException; +import org.apache.syncope.common.lib.SyncopeConstants; +import org.apache.syncope.common.lib.types.AnyTypeKind; +import org.apache.syncope.common.lib.types.AttrSchemaType; +import org.apache.syncope.common.lib.types.ClientExceptionType; +import org.apache.syncope.common.rest.api.service.JAXRSService; +import org.apache.syncope.core.persistence.api.attrvalue.validation.PlainAttrValidationManager; +import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; +import org.apache.syncope.core.persistence.api.dao.DynRealmDAO; +import org.apache.syncope.core.persistence.api.dao.GroupDAO; +import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; +import org.apache.syncope.core.persistence.api.dao.RealmDAO; +import org.apache.syncope.core.persistence.api.dao.UserDAO; +import org.apache.syncope.core.persistence.api.dao.search.AnyCond; +import org.apache.syncope.core.persistence.api.dao.search.AnyTypeCond; +import org.apache.syncope.core.persistence.api.dao.search.AttrCond; +import org.apache.syncope.core.persistence.api.dao.search.AuxClassCond; +import org.apache.syncope.core.persistence.api.dao.search.DynRealmCond; +import org.apache.syncope.core.persistence.api.dao.search.MemberCond; +import org.apache.syncope.core.persistence.api.dao.search.MembershipCond; +import org.apache.syncope.core.persistence.api.dao.search.OrderByClause; +import org.apache.syncope.core.persistence.api.dao.search.PrivilegeCond; +import org.apache.syncope.core.persistence.api.dao.search.RelationshipCond; +import org.apache.syncope.core.persistence.api.dao.search.RelationshipTypeCond; +import org.apache.syncope.core.persistence.api.dao.search.ResourceCond; +import org.apache.syncope.core.persistence.api.dao.search.RoleCond; +import org.apache.syncope.core.persistence.api.dao.search.SearchCond; +import org.apache.syncope.core.persistence.api.entity.Any; +import org.apache.syncope.core.persistence.api.entity.AnyUtils; +import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; +import org.apache.syncope.core.persistence.api.entity.DynRealm; +import org.apache.syncope.core.persistence.api.entity.EntityFactory; +import org.apache.syncope.core.persistence.api.entity.PlainAttrValue; +import org.apache.syncope.core.persistence.api.entity.PlainSchema; +import org.apache.syncope.core.persistence.api.entity.Realm; +import org.apache.syncope.core.provisioning.api.utils.RealmUtils; + +/** + * Search engine implementation for users, groups and any objects, based on self-updating SQL views. + */ +public class AnySearchNodeDAO extends AbstractAnySearchDAO { + + protected static final String SELECT_COLS_FROM_VIEW = + "any_id,creationContext,creationDate,creator,lastChangeContext," + + "lastChangeDate,lastModifier,status,changePwdDate,cipherAlgorithm,failedLogins," + + "lastLoginDate,mustChangePassword,suspended,username"; + + protected static int setParameter(final List parameters, final Object parameter) { + parameters.add(parameter); + return parameters.size(); + } + + protected static void fillWithParameters(final Query query, final List parameters) { + for (int i = 0; i < parameters.size(); i++) { + if (parameters.get(i) instanceof Boolean) { + query.setParameter(i + 1, ((Boolean) parameters.get(i)) ? 1 : 0); + } else { + query.setParameter(i + 1, parameters.get(i)); + } + } + } + + protected static String key(final AttrSchemaType schemaType) { + String key; + switch (schemaType) { + case Boolean: + key = "booleanValue"; + break; + + case Date: + key = "dateValue"; + break; + + case Double: + key = "doubleValue"; + break; + + case Long: + key = "longValue"; + break; + + case Binary: + key = "binaryValue"; + break; + + default: + key = "stringValue"; + } + + return key; + } + + protected static void visitLeaf( + final AnySearchNode.Leaf node, + final Set from, + final List where) { + + from.add(node.getFrom()); + where.add(node.getClause()); + } + + protected static void visitNode( + final AnySearchNode node, + final Set from, + final List where) { + + node.asLeaf().ifPresentOrElse( + leaf -> visitLeaf(leaf, from, where), + () -> { + List nodeWhere = new ArrayList<>(); + node.getChildren().forEach(child -> visitNode(child, from, nodeWhere)); + where.add(nodeWhere.stream(). + map(w -> "(" + w + ")"). + collect(Collectors.joining(" " + node.getType().name() + " "))); + }); + } + + protected static String buildFrom(final Set from) { + String fromString; + if (from.size() == 1) { + SearchSupport.SearchView sv = from.iterator().next(); + fromString = sv.name + " " + sv.alias; + } else { + List joins = new ArrayList<>(from); + StringBuilder join = new StringBuilder(joins.get(0).name + " " + joins.get(0).alias); + for (int i = 1; i < joins.size(); i++) { + SearchSupport.SearchView sv = joins.get(i); + join.append(" LEFT JOIN "). + append(sv.name).append(" ").append(sv.alias). + append(" ON "). + append(joins.get(0).alias).append(".any_id=").append(sv.alias).append(".any_id"); + } + fromString = join.toString(); + } + return fromString; + } + + protected static String buildWhere(final List where, final AnySearchNode root) { + return where.stream(). + map(w -> "(" + w + ")"). + collect(Collectors.joining(" " + root.getType().name() + " ")); + } + + protected static Supplier syncopeClientException(final String message) { + return () -> { + SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidSearchParameters); + sce.getElements().add(message); + return sce; + }; + } + + public AnySearchNodeDAO( + final RealmDAO realmDAO, + final DynRealmDAO dynRealmDAO, + final UserDAO userDAO, + final GroupDAO groupDAO, + final AnyObjectDAO anyObjectDAO, + final PlainSchemaDAO plainSchemaDAO, + final EntityFactory entityFactory, + final AnyUtilsFactory anyUtilsFactory, + final PlainAttrValidationManager validator) { + + super( + realmDAO, + dynRealmDAO, + userDAO, + groupDAO, + anyObjectDAO, + plainSchemaDAO, + entityFactory, + anyUtilsFactory, + validator); + } + + protected Optional getQueryForCustomConds( + final SearchCond cond, + final List parameters, + final SearchSupport svs, + final boolean not) { + + // do nothing by default, leave it open for subclasses + return Optional.empty(); + } + + protected Optional>> getQuery( + final SearchCond cond, final List parameters, final SearchSupport svs) { + + boolean not = cond.getType() == SearchCond.Type.NOT_LEAF; + + Optional node = Optional.empty(); + Set involvedPlainAttrs = new HashSet<>(); + + switch (cond.getType()) { + case LEAF: + case NOT_LEAF: + if (node.isEmpty()) { + node = cond.getLeaf(AnyTypeCond.class). + filter(leaf -> AnyTypeKind.ANY_OBJECT == svs.anyTypeKind). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } + + if (node.isEmpty()) { + node = cond.getLeaf(AuxClassCond.class). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } + + if (node.isEmpty()) { + node = cond.getLeaf(RelationshipTypeCond.class). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } + + if (node.isEmpty()) { + node = cond.getLeaf(RelationshipCond.class). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } + + if (node.isEmpty()) { + node = cond.getLeaf(MembershipCond.class). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } + + if (node.isEmpty()) { + node = cond.getLeaf(MemberCond.class). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } + + if (node.isEmpty()) { + node = cond.getLeaf(RoleCond.class). + filter(leaf -> AnyTypeKind.USER == svs.anyTypeKind). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } + + if (node.isEmpty()) { + node = cond.getLeaf(PrivilegeCond.class). + filter(leaf -> AnyTypeKind.USER == svs.anyTypeKind). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } + + if (node.isEmpty()) { + node = cond.getLeaf(DynRealmCond.class). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } + + if (node.isEmpty()) { + node = cond.getLeaf(ResourceCond.class). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } + + if (node.isEmpty()) { + node = cond.getLeaf(AnyCond.class). + map(anyCond -> getQuery(anyCond, not, parameters, svs)). + or(() -> cond.getLeaf(AttrCond.class). + map(attrCond -> { + try { + Pair checked = check(attrCond, svs.anyTypeKind); + involvedPlainAttrs.add(checked.getLeft().getKey()); + return getQuery(attrCond, not, checked, parameters, svs); + } catch (IllegalArgumentException e) { + // ignore + return null; + } + })); + } + + // allow for additional search conditions + if (node.isEmpty()) { + node = getQueryForCustomConds(cond, parameters, svs, not); + } + break; + + case AND: + AnySearchNode andNode = new AnySearchNode(AnySearchNode.Type.AND); + + getQuery(cond.getLeft(), parameters, svs).ifPresent(left -> { + andNode.add(left.getLeft()); + involvedPlainAttrs.addAll(left.getRight()); + }); + + getQuery(cond.getRight(), parameters, svs).ifPresent(right -> { + andNode.add(right.getLeft()); + involvedPlainAttrs.addAll(right.getRight()); + }); + + if (!andNode.getChildren().isEmpty()) { + node = Optional.of(andNode); + } + break; + + case OR: + AnySearchNode orNode = new AnySearchNode(AnySearchNode.Type.OR); + + getQuery(cond.getLeft(), parameters, svs).ifPresent(left -> { + orNode.add(left.getLeft()); + involvedPlainAttrs.addAll(left.getRight()); + }); + + getQuery(cond.getRight(), parameters, svs).ifPresent(right -> { + orNode.add(right.getLeft()); + involvedPlainAttrs.addAll(right.getRight()); + }); + + if (!orNode.getChildren().isEmpty()) { + node = Optional.of(orNode); + } + break; + + default: + } + + return node.map(n -> Pair.of(n, involvedPlainAttrs)); + } + + protected AnySearchNode getQuery( + final AnyTypeCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { + + StringBuilder clause = new StringBuilder("type_id"); + if (not) { + clause.append("<>"); + } else { + clause.append('='); + } + clause.append('?').append(setParameter(parameters, cond.getAnyTypeKey())); + + return new AnySearchNode.Leaf(svs.field(), clause.toString()); + } + + protected AnySearchNode getQuery( + final AuxClassCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { + + StringBuilder clause = new StringBuilder(); + if (not) { + clause.append("sv.any_id NOT IN ("); + } else { + clause.append("sv.any_id IN ("); + } + clause.append("SELECT any_id FROM "). + append(svs.auxClass().name). + append(" WHERE anyTypeClass_id=?"). + append(setParameter(parameters, cond.getAuxClass())). + append(')'); + + return new AnySearchNode.Leaf(svs.field(), clause.toString()); + } + + protected AnySearchNode getQuery( + final RelationshipTypeCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { + + StringBuilder clause = new StringBuilder(); + if (not) { + clause.append("sv.any_id NOT IN ("); + } else { + clause.append("sv.any_id IN ("); + } + clause.append("SELECT any_id ").append("FROM "). + append(svs.relationship().name). + append(" WHERE type=?").append(setParameter(parameters, cond.getRelationshipTypeKey())). + append(" UNION SELECT right_any_id AS any_id FROM "). + append(svs.relationship().name). + append(" WHERE type=?").append(setParameter(parameters, cond.getRelationshipTypeKey())). + append(')'); + + return new AnySearchNode.Leaf(svs.field(), clause.toString()); + } + + protected AnySearchNode getQuery( + final RelationshipCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { + + Set rightAnyObjects = check(cond); + + StringBuilder clause = new StringBuilder(); + if (not) { + clause.append("sv.any_id NOT IN ("); + } else { + clause.append("sv.any_id IN ("); + } + clause.append("SELECT DISTINCT any_id FROM "). + append(svs.relationship().name).append(" WHERE "). + append(rightAnyObjects.stream(). + map(key -> "right_any_id=?" + setParameter(parameters, key)). + collect(Collectors.joining(" OR "))). + append(')'); + + return new AnySearchNode.Leaf(svs.field(), clause.toString()); + } + + protected AnySearchNode getQuery( + final MembershipCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { + + List groupKeys = check(cond); + + String subwhere = groupKeys.stream(). + map(key -> "group_id=?" + setParameter(parameters, key)). + collect(Collectors.joining(" OR ")); + + StringBuilder clause = new StringBuilder("("); + + if (not) { + clause.append("sv.any_id NOT IN ("); + } else { + clause.append("sv.any_id IN ("); + } + clause.append("SELECT DISTINCT any_id FROM "). + append(svs.membership().name).append(" WHERE "). + append(subwhere). + append(") "); + + if (not) { + clause.append("AND sv.any_id NOT IN ("); + } else { + clause.append("OR sv.any_id IN ("); + } + + clause.append("SELECT DISTINCT any_id FROM "). + append(svs.dyngroupmembership().name).append(" WHERE "). + append(subwhere). + append("))"); + + return new AnySearchNode.Leaf(svs.field(), clause.toString()); + } + + protected AnySearchNode getQuery( + final RoleCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { + + StringBuilder clause = new StringBuilder("("); + + if (not) { + clause.append("sv.any_id NOT IN ("); + } else { + clause.append("sv.any_id IN ("); + } + + clause.append("SELECT DISTINCT any_id FROM "). + append(svs.role().name).append(" WHERE "). + append("role_id=?").append(setParameter(parameters, cond.getRole())). + append(") "); + + if (not) { + clause.append("AND sv.any_id NOT IN ("); + } else { + clause.append("OR sv.any_id IN ("); + } + + clause.append("SELECT DISTINCT any_id FROM "). + append(SearchSupport.dynrolemembership().name).append(" WHERE "). + append("role_id=?").append(setParameter(parameters, cond.getRole())). + append("))"); + + return new AnySearchNode.Leaf(svs.field(), clause.toString()); + } + + protected AnySearchNode getQuery( + final PrivilegeCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { + + StringBuilder clause = new StringBuilder("("); + + if (not) { + clause.append("sv.any_id NOT IN ("); + } else { + clause.append("sv.any_id IN ("); + } + + clause.append("SELECT DISTINCT any_id FROM "). + append(svs.priv().name).append(" WHERE "). + append("privilege_id=?").append(setParameter(parameters, cond.getPrivilege())). + append(") "); + + if (not) { + clause.append("AND sv.any_id NOT IN ("); + } else { + clause.append("OR sv.any_id IN ("); + } + + clause.append("SELECT DISTINCT any_id FROM "). + append(svs.dynpriv().name).append(" WHERE "). + append("privilege_id=?").append(setParameter(parameters, cond.getPrivilege())). + append("))"); + + return new AnySearchNode.Leaf(svs.field(), clause.toString()); + } + + protected AnySearchNode getQuery( + final DynRealmCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { + + StringBuilder clause = new StringBuilder("("); + + if (not) { + clause.append("sv.any_id NOT IN ("); + } else { + clause.append("sv.any_id IN ("); + } + + clause.append("SELECT DISTINCT any_id FROM "). + append(SearchSupport.dynrealmmembership().name).append(" WHERE "). + append("dynRealm_id=?").append(setParameter(parameters, cond.getDynRealm())). + append("))"); + + return new AnySearchNode.Leaf(svs.field(), clause.toString()); + } + + protected AnySearchNode getQuery( + final ResourceCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { + + StringBuilder clause = new StringBuilder(); + + if (not) { + clause.append("sv.any_id NOT IN ("); + } else { + clause.append("sv.any_id IN ("); + } + + clause.append("SELECT DISTINCT any_id FROM "). + append(svs.resource().name). + append(" WHERE resource_id=?"). + append(setParameter(parameters, cond.getResource())); + + if (svs.anyTypeKind == AnyTypeKind.USER || svs.anyTypeKind == AnyTypeKind.ANY_OBJECT) { + clause.append(" UNION SELECT DISTINCT any_id FROM "). + append(svs.groupResource().name). + append(" WHERE resource_id=?"). + append(setParameter(parameters, cond.getResource())); + } + + clause.append(')'); + + return new AnySearchNode.Leaf(svs.field(), clause.toString()); + } + + protected AnySearchNode getQuery( + final MemberCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { + + Set members = check(cond); + + StringBuilder clause = new StringBuilder(); + + if (not) { + clause.append("sv.any_id NOT IN ("); + } else { + clause.append("sv.any_id IN ("); + } + + clause.append("SELECT DISTINCT group_id AS any_id FROM "). + append(new SearchSupport(AnyTypeKind.USER).membership().name).append(" WHERE "). + append(members.stream(). + map(key -> "any_id=?" + setParameter(parameters, key)). + collect(Collectors.joining(" OR "))). + append(") "); + + if (not) { + clause.append("AND sv.any_id NOT IN ("); + } else { + clause.append("OR sv.any_id IN ("); + } + + clause.append("SELECT DISTINCT group_id AS any_id FROM "). + append(new SearchSupport(AnyTypeKind.ANY_OBJECT).membership().name).append(" WHERE "). + append(members.stream(). + map(key -> "any_id=?" + setParameter(parameters, key)). + collect(Collectors.joining(" OR "))). + append(')'); + + return new AnySearchNode.Leaf(svs.field(), clause.toString()); + } + + protected AnySearchNode.Leaf fillAttrQuery( + final String column, + final SearchSupport.SearchView from, + final PlainAttrValue attrValue, + final PlainSchema schema, + final AttrCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { + + // activate ignoreCase only for EQ and LIKE operators + boolean ignoreCase = AttrCond.Type.ILIKE == cond.getType() || AttrCond.Type.IEQ == cond.getType(); + + String left = column; + if (ignoreCase && (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum)) { + left = "LOWER(" + left + ')'; + } + + StringBuilder clause = new StringBuilder(left); + switch (cond.getType()) { + + case ILIKE: + case LIKE: + if (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) { + if (not) { + clause.append(" NOT "); + } + clause.append(" LIKE "); + if (ignoreCase) { + clause.append("LOWER(?").append(setParameter(parameters, cond.getExpression())).append(')'); + } else { + clause.append('?').append(setParameter(parameters, cond.getExpression())); + } + // workaround for Oracle DB adding explicit escape, to search for literal _ (underscore) + if (isOracle()) { + clause.append(" ESCAPE '\\' "); + } + } else { + LOG.error("LIKE is only compatible with string or enum schemas"); + return new AnySearchNode.Leaf(svs.field(), "1=2"); + } + break; + + case IEQ: + case EQ: + default: + if (not) { + clause.append("<>"); + } else { + clause.append('='); + } + if ((schema.getType() == AttrSchemaType.String + || schema.getType() == AttrSchemaType.Enum) && ignoreCase) { + clause.append("LOWER(?").append(setParameter(parameters, attrValue.getValue())).append(')'); + } else { + clause.append('?').append(setParameter(parameters, attrValue.getValue())); + } + break; + + case GE: + if (not) { + clause.append('<'); + } else { + clause.append(">="); + } + clause.append('?').append(setParameter(parameters, attrValue.getValue())); + break; + + case GT: + if (not) { + clause.append("<="); + } else { + clause.append('>'); + } + clause.append('?').append(setParameter(parameters, attrValue.getValue())); + break; + + case LE: + if (not) { + clause.append('>'); + } else { + clause.append("<="); + } + clause.append('?').append(setParameter(parameters, attrValue.getValue())); + break; + + case LT: + if (not) { + clause.append(">="); + } else { + clause.append('<'); + } + clause.append('?').append(setParameter(parameters, attrValue.getValue())); + break; + } + + return new AnySearchNode.Leaf( + from, + cond instanceof AnyCond + ? clause.toString() + : from.alias + ".schema_id='" + schema.getKey() + "' AND " + clause); + } + + protected AnySearchNode getQuery( + final AttrCond cond, + final boolean not, + final Pair checked, + final List parameters, + final SearchSupport svs) { + + SearchSupport.SearchView sv = checked.getLeft().isUniqueConstraint() + ? svs.asSearchViewSupport().uniqueAttr() + : svs.asSearchViewSupport().attr(); + + switch (cond.getType()) { + case ISNOTNULL: + return new AnySearchNode.Leaf( + sv, + sv.alias + ".schema_id='" + checked.getLeft().getKey() + "'"); + + case ISNULL: + String clause = new StringBuilder("sv.any_id NOT IN "). + append('('). + append("SELECT DISTINCT any_id FROM "). + append(sv.name). + append(" WHERE schema_id=").append("'").append(checked.getLeft().getKey()).append("'"). + append(')').toString(); + return new AnySearchNode.Leaf(svs.field(), clause); + + default: + AnySearchNode.Leaf node; + if (not && checked.getLeft().isMultivalue()) { + AnySearchNode.Leaf notNode = fillAttrQuery( + sv.alias + "." + key(checked.getLeft().getType()), + sv, + checked.getRight(), + checked.getLeft(), + cond, + false, + parameters, + svs); + node = new AnySearchNode.Leaf( + sv, + "sv.any_id NOT IN (" + + "SELECT any_id FROM " + sv.name + + " WHERE " + notNode.getClause().replace(sv.alias + ".", "") + + ")"); + } else { + node = fillAttrQuery( + sv.alias + "." + key(checked.getLeft().getType()), + sv, + checked.getRight(), + checked.getLeft(), + cond, + not, + parameters, + svs); + } + return node; + } + } + + protected AnySearchNode getQuery( + final AnyCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { + + if (JAXRSService.PARAM_REALM.equals(cond.getSchema()) + && !SyncopeConstants.UUID_PATTERN.matcher(cond.getExpression()).matches()) { + + Realm realm = realmDAO.findByFullPath(cond.getExpression()); + if (realm == null) { + throw new IllegalArgumentException("Invalid Realm full path: " + cond.getExpression()); + } + cond.setExpression(realm.getKey()); + } + + Triple checked = check(cond, svs.anyTypeKind); + + switch (checked.getRight().getType()) { + case ISNULL: + return new AnySearchNode.Leaf( + svs.field(), + checked.getRight().getSchema() + (not ? " IS NOT NULL" : " IS NULL")); + + case ISNOTNULL: + return new AnySearchNode.Leaf( + svs.field(), + checked.getRight().getSchema() + (not ? " IS NULL" : " IS NOT NULL")); + + default: + return fillAttrQuery( + checked.getRight().getSchema(), + svs.field(), + checked.getMiddle(), + checked.getLeft(), + checked.getRight(), + not, + parameters, + svs); + } + } + + protected AnySearchNode.Leaf buildAdminRealmsFilter( + final Set realmKeys, + final SearchSupport svs, + final List parameters) { + + if (realmKeys.isEmpty()) { + return new AnySearchNode.Leaf(svs.field(), "any_id IS NOT NULL"); + } + + String realmKeysArg = realmKeys.stream(). + map(realmKey -> "?" + setParameter(parameters, realmKey)). + collect(Collectors.joining(",")); + return new AnySearchNode.Leaf(svs.field(), "realm_id IN (" + realmKeysArg + ")"); + } + + protected Triple, Set> getAdminRealmsFilter( + final Realm base, + final boolean recursive, + final Set adminRealms, + final SearchSupport svs, + final List parameters) { + + Set realmKeys = new HashSet<>(); + Set dynRealmKeys = new HashSet<>(); + Set groupOwners = new HashSet<>(); + + if (recursive) { + adminRealms.forEach(realmPath -> RealmUtils.parseGroupOwnerRealm(realmPath).ifPresentOrElse( + goRealm -> groupOwners.add(goRealm.getRight()), + () -> { + if (realmPath.startsWith("/")) { + Realm realm = Optional.ofNullable(realmDAO.findByFullPath(realmPath)).orElseThrow(() -> { + SyncopeClientException noRealm = + SyncopeClientException.build(ClientExceptionType.InvalidRealm); + noRealm.getElements().add("Invalid realm specified: " + realmPath); + return noRealm; + }); + + realmKeys.addAll(realmDAO.findDescendants(realm.getFullPath(), base.getFullPath())); + } else { + DynRealm dynRealm = dynRealmDAO.find(realmPath); + if (dynRealm == null) { + LOG.warn("Ignoring invalid dynamic realm {}", realmPath); + } else { + dynRealmKeys.add(dynRealm.getKey()); + } + } + })); + if (!dynRealmKeys.isEmpty()) { + realmKeys.clear(); + } + } else { + if (adminRealms.stream().anyMatch(r -> r.startsWith(base.getFullPath()))) { + realmKeys.add(base.getKey()); + } + } + + return Triple.of(buildAdminRealmsFilter(realmKeys, svs, parameters), dynRealmKeys, groupOwners); + } + + @Override + protected int doCount( + final Realm base, + final boolean recursive, + final Set adminRealms, + final SearchCond cond, + final AnyTypeKind kind) { + + List parameters = new ArrayList<>(); + + SearchSupport svs = new SearchViewSupport(kind); + + Triple, Set> filter = + getAdminRealmsFilter(base, recursive, adminRealms, svs, parameters); + + // 1. get query string from search condition + Pair> queryInfo = getQuery( + buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind), parameters, svs). + orElseThrow(syncopeClientException("Invalid search condition")); + + // 2. take realms into account + AnySearchNode root; + if (queryInfo.getLeft().getType() == AnySearchNode.Type.AND) { + root = queryInfo.getLeft(); + } else { + root = new AnySearchNode(AnySearchNode.Type.AND); + root.add(queryInfo.getLeft()); + } + root.add(filter.getLeft()); + + Set from = new HashSet<>(); + List where = new ArrayList<>(); + + visitNode(root, from, where); + + // 3. generate the query string + String queryString = "SELECT COUNT(DISTINCT sv.any_id)" + + " FROM " + buildFrom(from) + + " WHERE " + buildWhere(where, root); + LOG.debug("Query: {}, parameters: {}", queryString, parameters); + + // 4. populate the search query with parameter values + Query countQuery = entityManager().createNativeQuery(queryString); + fillWithParameters(countQuery, parameters); + + // 5. execute the query and return the result + return ((Number) countQuery.getSingleResult()).intValue(); + } + + protected void parseOrderByForPlainSchema( + final SearchSupport svs, + final OrderBySupport obs, + final OrderBySupport.Item item, + final OrderByClause clause, + final PlainSchema schema, + final String fieldName) { + + // keep track of involvement of non-mandatory schemas in the order by clauses + obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition()); + + if (schema.isUniqueConstraint()) { + obs.views.add(svs.asSearchViewSupport().uniqueAttr()); + + item.select = new StringBuilder(). + append(svs.asSearchViewSupport().uniqueAttr().alias).append('.'). + append(key(schema.getType())). + append(" AS ").append(fieldName).toString(); + item.where = new StringBuilder(). + append(svs.asSearchViewSupport().uniqueAttr().alias). + append(".schema_id='").append(fieldName).append("'").toString(); + item.orderBy = fieldName + ' ' + clause.getDirection().name(); + } else { + obs.views.add(svs.asSearchViewSupport().attr()); + + item.select = new StringBuilder(). + append(svs.asSearchViewSupport().attr().alias).append('.').append(key(schema.getType())). + append(" AS ").append(fieldName).toString(); + item.where = new StringBuilder(). + append(svs.asSearchViewSupport().attr().alias). + append(".schema_id='").append(fieldName).append("'").toString(); + item.orderBy = fieldName + ' ' + clause.getDirection().name(); + } + } + + protected void parseOrderByForField( + final SearchSupport svs, + final OrderBySupport.Item item, + final String fieldName, + final OrderByClause clause) { + + item.select = svs.field().alias + '.' + fieldName; + item.where = StringUtils.EMPTY; + item.orderBy = svs.field().alias + '.' + fieldName + ' ' + clause.getDirection().name(); + } + + protected void parseOrderByForCustom( + final SearchSupport svs, + final OrderByClause clause, + final OrderBySupport.Item item, + final OrderBySupport obs) { + + // do nothing by default, meant for subclasses + } + + protected OrderBySupport parseOrderBy( + final SearchSupport svs, + final List orderBy) { + + AnyUtils anyUtils = anyUtilsFactory.getInstance(svs.anyTypeKind); + + OrderBySupport obs = new OrderBySupport(); + + Set orderByUniquePlainSchemas = new HashSet<>(); + Set orderByNonUniquePlainSchemas = new HashSet<>(); + orderBy.forEach(clause -> { + OrderBySupport.Item item = new OrderBySupport.Item(); + + parseOrderByForCustom(svs, clause, item, obs); + + if (item.isEmpty()) { + if (anyUtils.getField(clause.getField()) == null) { + PlainSchema schema = plainSchemaDAO.find(clause.getField()); + if (schema != null) { + if (schema.isUniqueConstraint()) { + orderByUniquePlainSchemas.add(schema.getKey()); + } else { + orderByNonUniquePlainSchemas.add(schema.getKey()); + } + if (orderByUniquePlainSchemas.size() > 1 || orderByNonUniquePlainSchemas.size() > 1) { + throw syncopeClientException("Order by more than one attribute is not allowed; " + + "remove one from " + (orderByUniquePlainSchemas.size() > 1 + ? orderByUniquePlainSchemas : orderByNonUniquePlainSchemas)).get(); + } + parseOrderByForPlainSchema(svs, obs, item, clause, schema, clause.getField()); + } + } else { + // Manage difference among external key attribute and internal JPA @Id + String fieldName = "key".equals(clause.getField()) ? "id" : clause.getField(); + + // Adjust field name to column name + if (ArrayUtils.contains(RELATIONSHIP_FIELDS, fieldName)) { + fieldName += "_id"; + } + + obs.views.add(svs.field()); + + parseOrderByForField(svs, item, fieldName, clause); + } + } + + if (item.isEmpty()) { + LOG.warn("Cannot build any valid clause from {}", clause); + } else { + obs.items.add(item); + } + }); + + return obs; + } + + @Override + @SuppressWarnings("unchecked") + protected > List doSearch( + final Realm base, + final boolean recursive, + final Set adminRealms, + final SearchCond cond, + final int page, + final int itemsPerPage, + final List orderBy, + final AnyTypeKind kind) { + + List parameters = new ArrayList<>(); + + SearchSupport svs = new SearchViewSupport(kind); + + Triple, Set> filter = + getAdminRealmsFilter(base, recursive, adminRealms, svs, parameters); + + // 1. get query string from search condition + Pair> queryInfo = getQuery( + buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind), parameters, svs). + orElseThrow(syncopeClientException("Invalid search condition")); + + // 2. take realms into account + AnySearchNode root; + if (queryInfo.getLeft().getType() == AnySearchNode.Type.AND) { + root = queryInfo.getLeft(); + } else { + root = new AnySearchNode(AnySearchNode.Type.AND); + root.add(queryInfo.getLeft()); + } + root.add(filter.getLeft()); + + Set from = new HashSet<>(); + List where = new ArrayList<>(); + + visitNode(root, from, where); + + // 3. take ordering into account + OrderBySupport obs = parseOrderBy(svs, orderBy); + + // 4. generate the query string + StringBuilder queryString = new StringBuilder("SELECT DISTINCT sv.any_id"); + obs.items.forEach(item -> queryString.append(',').append(item.select)); + + queryString.append(" FROM ").append(buildFrom(from)); + + queryString.append(" WHERE ").append(buildWhere(where, root)); + + if (!obs.items.isEmpty()) { + queryString.append(" ORDER BY "). + append(obs.items.stream().map(item -> item.orderBy).collect(Collectors.joining(","))); + } + + LOG.debug("Query: {}, parameters: {}", queryString, parameters); + + // 5. prepare the search query + Query query = entityManager().createNativeQuery(queryString.toString()); + + // 6. page starts from 1, while setFirtResult() starts from 0 + query.setFirstResult(itemsPerPage * (page <= 0 ? 0 : page - 1)); + + if (itemsPerPage >= 0) { + query.setMaxResults(itemsPerPage); + } + + // 7. populate the search query with parameter values + fillWithParameters(query, parameters); + + // 8. prepare the result (avoiding duplicates) + return buildResult(query.getResultList(), kind); + } +} diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java index 0ace0bd5505..d514181735b 100644 --- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java +++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java @@ -758,8 +758,7 @@ public void issueSYNCOPE433() { likeCond.setSchema("username"); likeCond.setExpression("%ossin%"); - SearchCond searchCond = SearchCond.getOr( - SearchCond.getLeaf(isNullCond), SearchCond.getLeaf(likeCond)); + SearchCond searchCond = SearchCond.getOr(SearchCond.getLeaf(isNullCond), SearchCond.getLeaf(likeCond)); int count = searchDAO.count( realmDAO.getRoot(), true, SyncopeConstants.FULL_ADMIN_REALMS, searchCond, AnyTypeKind.USER); @@ -778,9 +777,9 @@ public void issueSYNCOPE929() { SearchCond orCond = SearchCond.getOr(SearchCond.getLeaf(rossiniCond), SearchCond.getLeaf(genderCond)); - AttrCond belliniCond = new AttrCond(AttrCond.Type.EQ); - belliniCond.setSchema("surname"); - belliniCond.setExpression("Bellini"); + AnyCond belliniCond = new AnyCond(AttrCond.Type.EQ); + belliniCond.setSchema("username"); + belliniCond.setExpression("bellini"); SearchCond searchCond = SearchCond.getAnd(orCond, SearchCond.getLeaf(belliniCond)); From 83c8a53d09ee3cb62c7ba55b80099b04f16920e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Chicchiricc=C3=B2?= Date: Sat, 18 Jan 2025 07:30:16 +0100 Subject: [PATCH 2/9] Fixes --- .../core/persistence/jpa/dao/AnySearchNodeDAO.java | 11 ++++++++--- .../core/persistence/jpa/dao/SearchSupport.java | 5 +++++ .../org/apache/syncope/fit/core/SearchITCase.java | 2 +- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnySearchNodeDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnySearchNodeDAO.java index 752d8bea53b..d96fae6b5ad 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnySearchNodeDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnySearchNodeDAO.java @@ -1067,9 +1067,13 @@ protected > List doSearch( getAdminRealmsFilter(base, recursive, adminRealms, svs, parameters); // 1. get query string from search condition - Pair> queryInfo = getQuery( - buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind), parameters, svs). - orElseThrow(syncopeClientException("Invalid search condition")); + Optional>> optionalQueryInfo = getQuery( + buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind), parameters, svs); + if (optionalQueryInfo.isEmpty()) { + LOG.error("Invalid search condition: {}", cond); + return List.of(); + } + Pair> queryInfo = optionalQueryInfo.get(); // 2. take realms into account AnySearchNode root; @@ -1093,6 +1097,7 @@ protected > List doSearch( StringBuilder queryString = new StringBuilder("SELECT DISTINCT sv.any_id"); obs.items.forEach(item -> queryString.append(',').append(item.select)); + from.addAll(obs.views); queryString.append(" FROM ").append(buildFrom(from)); queryString.append(" WHERE ").append(buildWhere(where, root)); diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/SearchSupport.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/SearchSupport.java index 936315574d9..7235f6a81f1 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/SearchSupport.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/SearchSupport.java @@ -63,6 +63,11 @@ public boolean equals(final Object obj) { append(name, other.name). build(); } + + @Override + public String toString() { + return "SearchView{" + "alias=" + alias + ", name=" + name + '}'; + } } protected final AnyTypeKind anyTypeKind; diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java index 93919052c5c..0369060318f 100644 --- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java +++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java @@ -736,7 +736,7 @@ public void issueSYNCOPE768() { public void issueSYNCOPE929() { PagedResult matchingUsers = USER_SERVICE.search( new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM). - fiql("(surname==Rossini,gender==M);surname==Bellini").build()); + fiql("(surname==Rossini,gender==M);username==bellini").build()); assertNotNull(matchingUsers); From b913c94995be5f81424e4bcca63fa3d7bd73edd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Chicchiricc=C3=B2?= Date: Sun, 19 Jan 2025 09:23:09 +0100 Subject: [PATCH 3/9] myjson and majson should work, ojson has flows, pgjsonb still to do --- .../jpa/dao/MaJPAJSONAnySearchDAO.java | 48 +- .../jpa/dao/MyJPAJSONAnySearchDAO.java | 285 ++- .../jpa/dao/OJPAJSONAnySearchDAO.java | 272 ++- .../jpa/dao/PGJPAJSONAnySearchDAO.java | 901 ++------- .../persistence/jpa/PersistenceContext.java | 4 +- .../persistence/jpa/dao/AnySearchNodeDAO.java | 1128 ----------- .../persistence/jpa/dao/JPAAnySearchDAO.java | 1646 ++++++++--------- 7 files changed, 1171 insertions(+), 3113 deletions(-) delete mode 100644 core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnySearchNodeDAO.java diff --git a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MaJPAJSONAnySearchDAO.java b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MaJPAJSONAnySearchDAO.java index 8ec7e900703..00fcbdce4d9 100644 --- a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MaJPAJSONAnySearchDAO.java +++ b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MaJPAJSONAnySearchDAO.java @@ -29,7 +29,6 @@ import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; import org.apache.syncope.core.persistence.api.dao.RealmDAO; import org.apache.syncope.core.persistence.api.dao.UserDAO; -import org.apache.syncope.core.persistence.api.dao.search.AnyCond; import org.apache.syncope.core.persistence.api.dao.search.AttrCond; import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; import org.apache.syncope.core.persistence.api.entity.EntityFactory; @@ -66,14 +65,13 @@ public MaJPAJSONAnySearchDAO( } @Override - protected String getQuery( + protected AnySearchNode getQuery( final AttrCond cond, final boolean not, + final Pair checked, final List parameters, final SearchSupport svs) { - Pair checked = check(cond, svs.anyTypeKind); - // normalize NULL / NOT NULL checks if (not) { if (cond.getType() == AttrCond.Type.ISNULL) { @@ -83,20 +81,20 @@ protected String getQuery( } } - StringBuilder query = - new StringBuilder("SELECT DISTINCT any_id FROM ").append(svs.field().name).append(" WHERE "); switch (cond.getType()) { case ISNOTNULL: - query.append("JSON_SEARCH(plainAttrs, 'one', '"). - append(checked.getLeft().getKey()). - append("', NULL, '$[*].schema') IS NOT NULL"); - break; + return new AnySearchNode.Leaf( + svs.field(), + "JSON_SEARCH(" + + "plainAttrs, 'one', '" + checked.getLeft().getKey() + "', NULL, '$[*].schema'" + + ") IS NOT NULL"); case ISNULL: - query.append("JSON_SEARCH(plainAttrs, 'one', '"). - append(checked.getLeft().getKey()). - append("', NULL, '$[*].schema') IS NULL"); - break; + return new AnySearchNode.Leaf( + svs.field(), + "JSON_SEARCH(" + + "plainAttrs, 'one', '" + checked.getLeft().getKey() + "', NULL, '$[*].schema'" + + ") IS NULL"); default: if (!not && cond.getType() == AttrCond.Type.EQ) { @@ -108,20 +106,12 @@ protected String getQuery( ((JSONPlainAttr) container).add(checked.getRight()); } - query.append("JSON_CONTAINS(plainAttrs, '"). - append(POJOHelper.serialize(List.of(container)).replace("'", "''")). - append("')"); + return new AnySearchNode.Leaf( + svs.field(), + "JSON_CONTAINS(" + + "plainAttrs, '" + POJOHelper.serialize(List.of(container)).replace("'", "''") + + "')"); } else { - query = new StringBuilder("SELECT DISTINCT any_id FROM "); - if (not && !(cond instanceof AnyCond) && checked.getLeft().isMultivalue()) { - query.append(svs.field().name).append(" WHERE "); - } else { - query.append((checked.getLeft().isUniqueConstraint() - ? svs.asSearchViewSupport().uniqueAttr().name - : svs.asSearchViewSupport().attr().name)). - append(" WHERE schema_id='").append(checked.getLeft().getKey()); - } - Optional.ofNullable(checked.getRight().getDateValue()). map(DateTimeFormatter.ISO_OFFSET_DATE_TIME::format). ifPresent(formatted -> { @@ -129,10 +119,8 @@ protected String getQuery( checked.getRight().setStringValue(formatted); }); - fillAttrQuery(query, checked.getRight(), checked.getLeft(), cond, not, parameters, svs); + return super.getQuery(cond, not, checked, parameters, svs); } } - - return query.toString(); } } diff --git a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MyJPAJSONAnySearchDAO.java b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MyJPAJSONAnySearchDAO.java index 40f4c1cf91c..ec86a34bddb 100644 --- a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MyJPAJSONAnySearchDAO.java +++ b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MyJPAJSONAnySearchDAO.java @@ -21,8 +21,6 @@ import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; import org.apache.commons.lang3.tuple.Pair; import org.apache.syncope.common.lib.types.AttrSchemaType; import org.apache.syncope.core.persistence.api.attrvalue.validation.PlainAttrValidationManager; @@ -32,10 +30,8 @@ import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; import org.apache.syncope.core.persistence.api.dao.RealmDAO; import org.apache.syncope.core.persistence.api.dao.UserDAO; -import org.apache.syncope.core.persistence.api.dao.search.AnyCond; import org.apache.syncope.core.persistence.api.dao.search.AttrCond; import org.apache.syncope.core.persistence.api.dao.search.OrderByClause; -import org.apache.syncope.core.persistence.api.entity.AnyUtils; import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; import org.apache.syncope.core.persistence.api.entity.EntityFactory; import org.apache.syncope.core.persistence.api.entity.JSONPlainAttr; @@ -70,62 +66,6 @@ public MyJPAJSONAnySearchDAO( validator); } - @Override - protected void processOBS( - final SearchSupport svs, - final OrderBySupport obs, - final StringBuilder where) { - - Set attrs = obs.items.stream(). - map(item -> item.orderBy.substring(0, item.orderBy.indexOf(" "))).collect(Collectors.toSet()); - - obs.views.forEach(searchView -> { - boolean searchViewAddedToWhere = false; - if (searchView.name.equals(svs.field().name)) { - StringBuilder attrWhere = new StringBuilder(); - StringBuilder nullAttrWhere = new StringBuilder(); - - if (svs.nonMandatorySchemas || obs.nonMandatorySchemas) { - where.append(", (SELECT ").append(SELECT_COLS_FROM_VIEW).append(",plainSchema," - + "binaryValue,booleanValue,dateValue,doubleValue,longValue,stringValue,attrUniqueValue " - + "FROM ").append(searchView.name); - searchViewAddedToWhere = true; - - attrs.forEach(field -> { - if (attrWhere.length() == 0) { - attrWhere.append(" WHERE "); - } else { - attrWhere.append(" OR "); - } - attrWhere.append("JSON_CONTAINS(plainAttrs, '[{\"schema\":\"").append(field).append("\"}]')"); - - nullAttrWhere.append(" UNION SELECT DISTINCT ").append(SELECT_COLS_FROM_VIEW).append(", "). - append("'").append(field).append("'").append(" AS plainSchema, "). - append("null AS binaryValue, "). - append("null AS booleanValue, "). - append("null AS dateValue, "). - append("null AS doubleValue, "). - append("null AS longValue, "). - append("null AS stringValue, "). - append("null AS attrUniqueValue "). - append("FROM ").append(svs.field().name). - append(" WHERE any_id NOT IN "). - append("(SELECT DISTINCT any_id FROM "). - append(svs.field().name). - append(" WHERE "). - append("JSON_CONTAINS(plainAttrs, '[{\"schema\":\"").append(field).append("\"}]'))"); - }); - where.append(attrWhere).append(nullAttrWhere).append(')'); - } - } - if (!searchViewAddedToWhere) { - where.append(',').append(searchView.name); - } - - where.append(' ').append(searchView.alias); - }); - } - @Override protected void parseOrderByForPlainSchema( final SearchSupport svs, @@ -147,122 +87,96 @@ protected void parseOrderByForPlainSchema( item.orderBy = fieldName + ' ' + clause.getDirection().name(); } - protected void fillAttrQuery( - final AnyUtils anyUtils, - final StringBuilder query, + protected AnySearchNode.Leaf filJSONAttrQuery( + final SearchSupport.SearchView from, final PlainAttrValue attrValue, final PlainSchema schema, final AttrCond cond, final boolean not, - final List parameters, - final SearchSupport svs) { + final List parameters) { - // This first branch is required for handling with not conditions given on multivalue fields (SYNCOPE-1419) - if (not && schema.isMultivalue() - && !(cond instanceof AnyCond) - && cond.getType() != AttrCond.Type.ISNULL && cond.getType() != AttrCond.Type.ISNOTNULL) { + String key = key(schema.getType()); - query.append("id NOT IN (SELECT DISTINCT any_id FROM "); - query.append(svs.field().name).append(" WHERE "); - fillAttrQuery(anyUtils, query, attrValue, schema, cond, false, parameters, svs); - query.append(')'); - } else { - if (!not && cond.getType() == AttrCond.Type.EQ) { - PlainAttr container = anyUtils.newPlainAttr(); - container.setSchema(schema); - if (attrValue instanceof PlainAttrUniqueValue) { - container.setUniqueValue((PlainAttrUniqueValue) attrValue); - } else { - ((JSONPlainAttr) container).add(attrValue); - } + String value = Optional.ofNullable(attrValue.getDateValue()). + map(DateTimeFormatter.ISO_OFFSET_DATE_TIME::format). + orElse(cond.getExpression()); - query.append("JSON_CONTAINS(plainAttrs, '"). - append(POJOHelper.serialize(List.of(container)).replace("'", "''")). - append("')"); - } else { - String key = key(schema.getType()); + boolean lower = (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) + && (cond.getType() == AttrCond.Type.IEQ || cond.getType() == AttrCond.Type.ILIKE); - String value = Optional.ofNullable(attrValue.getDateValue()). - map(DateTimeFormatter.ISO_OFFSET_DATE_TIME::format). - orElse(cond.getExpression()); + StringBuilder clause = new StringBuilder("plainSchema=?").append(setParameter(parameters, cond.getSchema())). + append(" AND "). + append(lower ? "LOWER(" : ""). + append(schema.isUniqueConstraint() + ? "attrUniqueValue ->> '$." + key + '\'' + : key). + append(lower ? ')' : ""); - boolean lower = (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) - && (cond.getType() == AttrCond.Type.IEQ || cond.getType() == AttrCond.Type.ILIKE); - - query.append("plainSchema=?").append(setParameter(parameters, cond.getSchema())). - append(" AND "). - append(lower ? "LOWER(" : ""). - append(schema.isUniqueConstraint() - ? "attrUniqueValue ->> '$." + key + '\'' - : key). - append(lower ? ')' : ""); - - switch (cond.getType()) { - case LIKE: - case ILIKE: - if (not) { - query.append("NOT "); - } - query.append(" LIKE "); - break; - - case GE: - if (not) { - query.append('<'); - } else { - query.append(">="); - } - break; + switch (cond.getType()) { + case LIKE: + case ILIKE: + if (not) { + clause.append("NOT "); + } + clause.append(" LIKE "); + break; - case GT: - if (not) { - query.append("<="); - } else { - query.append('>'); - } - break; + case GE: + if (not) { + clause.append('<'); + } else { + clause.append(">="); + } + break; - case LE: - if (not) { - query.append('>'); - } else { - query.append("<="); - } - break; + case GT: + if (not) { + clause.append("<="); + } else { + clause.append('>'); + } + break; - case LT: - if (not) { - query.append(">="); - } else { - query.append('<'); - } - break; + case LE: + if (not) { + clause.append('>'); + } else { + clause.append("<="); + } + break; - case EQ: - case IEQ: - default: - if (not) { - query.append('!'); - } - query.append('='); + case LT: + if (not) { + clause.append(">="); + } else { + clause.append('<'); } + break; - query.append(lower ? "LOWER(" : ""). - append('?').append(setParameter(parameters, value)). - append(lower ? ")" : ""); - } + case EQ: + case IEQ: + default: + if (not) { + clause.append('!'); + } + clause.append('='); } + + clause.append(lower ? "LOWER(" : ""). + append('?').append(setParameter(parameters, value)). + append(lower ? ")" : ""); + + return new AnySearchNode.Leaf(from, clause.toString()); } @Override - protected String getQuery( + protected AnySearchNode getQuery( final AttrCond cond, final boolean not, + final Pair checked, final List parameters, final SearchSupport svs) { - Pair checked = check(cond, svs.anyTypeKind); - // normalize NULL / NOT NULL checks if (not) { if (cond.getType() == AttrCond.Type.ISNULL) { @@ -272,30 +186,63 @@ protected String getQuery( } } - StringBuilder query = - new StringBuilder("SELECT DISTINCT any_id FROM ").append(svs.field().name).append(" WHERE "); switch (cond.getType()) { case ISNOTNULL: - query.append("JSON_SEARCH(plainAttrs, 'one', '"). - append(checked.getLeft().getKey()). - append("', NULL, '$[*].schema') IS NOT NULL"); - break; + return new AnySearchNode.Leaf( + svs.field(), + "JSON_SEARCH(" + + "plainAttrs, 'one', '" + checked.getLeft().getKey() + "', NULL, '$[*].schema'" + + ") IS NOT NULL"); case ISNULL: - query.append("JSON_SEARCH(plainAttrs, 'one', '"). - append(checked.getLeft().getKey()). - append("', NULL, '$[*].schema') IS NULL"); - break; + return new AnySearchNode.Leaf( + svs.field(), + "JSON_SEARCH(" + + "plainAttrs, 'one', '" + checked.getLeft().getKey() + "', NULL, '$[*].schema'" + + ") IS NULL"); default: - if (not && !(cond instanceof AnyCond) && checked.getLeft().isMultivalue()) { - query = new StringBuilder("SELECT DISTINCT id AS any_id FROM ").append(svs.table().name). - append(" WHERE "); + if (!not && cond.getType() == AttrCond.Type.EQ) { + PlainAttr container = anyUtilsFactory.getInstance(svs.anyTypeKind).newPlainAttr(); + container.setSchema(checked.getLeft()); + if (checked.getRight() instanceof PlainAttrUniqueValue) { + container.setUniqueValue((PlainAttrUniqueValue) checked.getRight()); + } else { + ((JSONPlainAttr) container).add(checked.getRight()); + } + + return new AnySearchNode.Leaf( + svs.field(), + "JSON_CONTAINS(" + + "plainAttrs, '" + POJOHelper.serialize(List.of(container)).replace("'", "''") + + "')"); + } else { + AnySearchNode.Leaf node; + if (not && checked.getLeft().isMultivalue()) { + AnySearchNode.Leaf notNode = filJSONAttrQuery( + svs.field(), + checked.getRight(), + checked.getLeft(), + cond, + false, + parameters); + node = new AnySearchNode.Leaf( + notNode.getFrom(), + "sv.any_id NOT IN (" + + "SELECT any_id FROM " + notNode.getFrom().name + + " WHERE " + notNode.getClause().replace(notNode.getFrom().alias + ".", "") + + ")"); + } else { + node = filJSONAttrQuery( + svs.field(), + checked.getRight(), + checked.getLeft(), + cond, + not, + parameters); + } + return node; } - fillAttrQuery(anyUtilsFactory.getInstance(svs.anyTypeKind), - query, checked.getRight(), checked.getLeft(), cond, not, parameters, svs); } - - return query.toString(); } } diff --git a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OJPAJSONAnySearchDAO.java b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OJPAJSONAnySearchDAO.java index 67216ee3588..d08a4acb42c 100644 --- a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OJPAJSONAnySearchDAO.java +++ b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OJPAJSONAnySearchDAO.java @@ -21,8 +21,6 @@ import java.time.format.DateTimeFormatter; import java.util.List; import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.syncope.common.lib.types.AttrSchemaType; @@ -33,10 +31,8 @@ import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; import org.apache.syncope.core.persistence.api.dao.RealmDAO; import org.apache.syncope.core.persistence.api.dao.UserDAO; -import org.apache.syncope.core.persistence.api.dao.search.AnyCond; import org.apache.syncope.core.persistence.api.dao.search.AttrCond; import org.apache.syncope.core.persistence.api.dao.search.OrderByClause; -import org.apache.syncope.core.persistence.api.entity.AnyUtils; import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; import org.apache.syncope.core.persistence.api.entity.EntityFactory; import org.apache.syncope.core.persistence.api.entity.PlainAttrValue; @@ -67,68 +63,6 @@ public OJPAJSONAnySearchDAO( validator); } - @Override - protected void processOBS( - final SearchSupport svs, - final OrderBySupport obs, - final StringBuilder where) { - - Set attrs = obs.items.stream(). - map(item -> item.orderBy.substring(0, item.orderBy.indexOf(" "))).collect(Collectors.toSet()); - - obs.views.forEach(searchView -> { - boolean searchViewAddedToWhere = false; - if (searchView.name.equals(svs.field().name)) { - StringBuilder attrWhere = new StringBuilder(); - StringBuilder nullAttrWhere = new StringBuilder(); - - if (svs.nonMandatorySchemas || obs.nonMandatorySchemas) { - where.append(", (SELECT ").append(SELECT_COLS_FROM_VIEW).append(",plainSchema," - + "ubinaryValue,ubooleanValue,udateValue,udoubleValue,ulongValue,ustringValue," - + "binaryValue,booleanValue,dateValue,doubleValue,longValue,stringValue FROM "). - append(searchView.name); - searchViewAddedToWhere = true; - - attrs.forEach(field -> { - if (attrWhere.length() == 0) { - attrWhere.append(" WHERE "); - } else { - attrWhere.append(" OR "); - } - attrWhere.append("JSON_EXISTS(plainAttrs, '$[*]?(@.schema == \"").append(field).append("\")')"); - - nullAttrWhere.append(" UNION SELECT DISTINCT ").append(SELECT_COLS_FROM_VIEW).append(","). - append("'").append(field).append("'").append(" AS plainSchema, "). - append("null AS ubinaryValue, "). - append("null AS ubooleanValue, "). - append("null AS udateValue, "). - append("null AS udoubleValue, "). - append("null AS ulongValue, "). - append("null AS ustringValue, "). - append("null AS binaryValue, "). - append("null AS booleanValue, "). - append("null AS dateValue, "). - append("null AS doubleValue, "). - append("null AS longValue, "). - append("null AS stringValue "). - append("FROM ").append(svs.field().name). - append(" WHERE any_id NOT IN "). - append("(SELECT DISTINCT any_id FROM "). - append(svs.field().name). - append(" WHERE "). - append("JSON_EXISTS(plainAttrs, '$[*]?(@.schema == \"").append(field).append("\")'))"); - }); - where.append(attrWhere).append(nullAttrWhere).append(')'); - } - } - if (!searchViewAddedToWhere) { - where.append(',').append(searchView.name); - } - - where.append(' ').append(searchView.alias); - }); - } - @Override protected void parseOrderByForPlainSchema( final SearchSupport svs, @@ -150,117 +84,106 @@ protected void parseOrderByForPlainSchema( item.orderBy = fieldName + ' ' + clause.getDirection().name(); } - protected void fillAttrQuery( - final AnyUtils anyUtils, - final StringBuilder query, + protected AnySearchNode.Leaf filJSONAttrQuery( + final SearchSupport.SearchView from, final PlainAttrValue attrValue, final PlainSchema schema, final AttrCond cond, final boolean not, - final List parameters, - final SearchSupport svs) { + final List parameters) { - // This first branch is required for handling with not conditions given on multivalue fields (SYNCOPE-1419) - if (not && schema.isMultivalue() - && !(cond instanceof AnyCond) - && cond.getType() != AttrCond.Type.ISNULL && cond.getType() != AttrCond.Type.ISNOTNULL) { + String key = key(schema.getType()); - query.append("id NOT IN (SELECT DISTINCT any_id FROM "); - query.append(svs.field().name).append(" WHERE "); - fillAttrQuery(anyUtils, query, attrValue, schema, cond, false, parameters, svs); - query.append(')'); - } else { - String key = key(schema.getType()); + String value = Optional.ofNullable(attrValue.getDateValue()). + map(DateTimeFormatter.ISO_OFFSET_DATE_TIME::format). + orElseGet(() -> schema.getType() == AttrSchemaType.Boolean + ? BooleanUtils.toStringTrueFalse(attrValue.getBooleanValue()) + : cond.getExpression()); - String value = Optional.ofNullable(attrValue.getDateValue()). - map(DateTimeFormatter.ISO_OFFSET_DATE_TIME::format). - orElseGet(() -> schema.getType() == AttrSchemaType.Boolean - ? BooleanUtils.toStringTrueFalse(attrValue.getBooleanValue()) - : cond.getExpression()); + boolean lower = (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) + && (cond.getType() == AttrCond.Type.IEQ || cond.getType() == AttrCond.Type.ILIKE); - boolean lower = (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) - && (cond.getType() == AttrCond.Type.IEQ || cond.getType() == AttrCond.Type.ILIKE); + StringBuilder clause = new StringBuilder("plainSchema=?").append(setParameter(parameters, cond.getSchema())). + append(" AND "). + append(lower ? "LOWER(" : ""); + if (schema.isUniqueConstraint()) { + clause.append("u").append(key); + } else { + clause.append("JSON_VALUE(").append(key).append(", '$[*]')"); + } + clause.append(lower ? ')' : ""); - query.append("plainSchema=?").append(setParameter(parameters, cond.getSchema())). - append(" AND "). - append(lower ? "LOWER(" : ""); - if (schema.isUniqueConstraint()) { - query.append("u").append(key); - } else { - query.append("JSON_VALUE(").append(key).append(", '$[*]')"); - } - query.append(lower ? ')' : ""); + switch (cond.getType()) { + case LIKE: + case ILIKE: + if (not) { + clause.append("NOT "); + } + clause.append(" LIKE "); + break; - switch (cond.getType()) { - case LIKE: - case ILIKE: - if (not) { - query.append("NOT "); - } - query.append(" LIKE "); - break; + case GE: + if (not) { + clause.append('<'); + } else { + clause.append(">="); + } + break; - case GE: - if (not) { - query.append('<'); - } else { - query.append(">="); - } - break; + case GT: + if (not) { + clause.append("<="); + } else { + clause.append('>'); + } + break; - case GT: - if (not) { - query.append("<="); - } else { - query.append('>'); - } - break; + case LE: + if (not) { + clause.append('>'); + } else { + clause.append("<="); + } + break; - case LE: - if (not) { - query.append('>'); - } else { - query.append("<="); - } - break; + case LT: + if (not) { + clause.append(">="); + } else { + clause.append('<'); + } + break; - case LT: - if (not) { - query.append(">="); - } else { - query.append('<'); - } - break; + case EQ: + case IEQ: + default: + if (not) { + clause.append('!'); + } + clause.append('='); + } - case EQ: - case IEQ: - default: - if (not) { - query.append('!'); - } - query.append('='); - } + clause.append(lower ? "LOWER(" : ""). + append('?').append(setParameter(parameters, value)). + append(lower ? ")" : ""); - query.append(lower ? "LOWER(" : ""). - append('?').append(setParameter(parameters, value)). - append(lower ? ")" : ""); - // workaround for Oracle DB adding explicit escaping string, to search - // for literal _ (underscore) (SYNCOPE-1779) - if (cond.getType() == AttrCond.Type.ILIKE || cond.getType() == AttrCond.Type.LIKE) { - query.append(" ESCAPE '\\' "); - } + // workaround for Oracle DB adding explicit escaping string, to search + // for literal _ (underscore) (SYNCOPE-1779) + if (cond.getType() == AttrCond.Type.ILIKE || cond.getType() == AttrCond.Type.LIKE) { + clause.append(" ESCAPE '\\' "); } + + return new AnySearchNode.Leaf(from, clause.toString()); } @Override - protected String getQuery( + protected AnySearchNode getQuery( final AttrCond cond, final boolean not, + final Pair checked, final List parameters, final SearchSupport svs) { - Pair checked = check(cond, svs.anyTypeKind); - // normalize NULL / NOT NULL checks if (not) { if (cond.getType() == AttrCond.Type.ISNULL) { @@ -270,28 +193,43 @@ protected String getQuery( } } - StringBuilder query = - new StringBuilder("SELECT DISTINCT any_id FROM ").append(svs.field().name).append(" WHERE "); switch (cond.getType()) { case ISNOTNULL: - query.append("JSON_EXISTS(plainAttrs, '$[*]?(@.schema == \""). - append(checked.getLeft().getKey()).append("\")')"); - break; + return new AnySearchNode.Leaf( + svs.field(), + "JSON_EXISTS(plainAttrs, '$[*]?(@.schema == \"" + checked.getLeft().getKey() + "\")')"); case ISNULL: - query.append("NOT JSON_EXISTS(plainAttrs, '$[*]?(@.schema == \""). - append(checked.getLeft().getKey()).append("\")')"); - break; + return new AnySearchNode.Leaf( + svs.field(), + "NOT JSON_EXISTS(plainAttrs, '$[*]?(@.schema == \"" + checked.getLeft().getKey() + "\")')"); default: - if (not && !(cond instanceof AnyCond) && checked.getLeft().isMultivalue()) { - query = new StringBuilder("SELECT DISTINCT id AS any_id FROM ").append(svs.table().name). - append(" WHERE "); + AnySearchNode.Leaf node; + if (not && checked.getLeft().isMultivalue()) { + AnySearchNode.Leaf notNode = filJSONAttrQuery( + svs.field(), + checked.getRight(), + checked.getLeft(), + cond, + false, + parameters); + node = new AnySearchNode.Leaf( + notNode.getFrom(), + "sv.any_id NOT IN (" + + "SELECT any_id FROM " + notNode.getFrom().name + + " WHERE " + notNode.getClause().replace(notNode.getFrom().alias + ".", "") + + ")"); + } else { + node = filJSONAttrQuery( + svs.field(), + checked.getRight(), + checked.getLeft(), + cond, + not, + parameters); } - fillAttrQuery(anyUtilsFactory.getInstance(svs.anyTypeKind), - query, checked.getRight(), checked.getLeft(), cond, not, parameters, svs); + return node; } - - return query.toString(); } } diff --git a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAJSONAnySearchDAO.java b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAJSONAnySearchDAO.java index 2a829bc4618..47896db5eba 100644 --- a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAJSONAnySearchDAO.java +++ b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAJSONAnySearchDAO.java @@ -19,20 +19,11 @@ package org.apache.syncope.core.persistence.jpa.dao; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; -import javax.persistence.Query; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; -import org.apache.commons.lang3.tuple.Triple; -import org.apache.syncope.common.lib.SyncopeClientException; -import org.apache.syncope.common.lib.SyncopeConstants; -import org.apache.syncope.common.lib.types.AnyTypeKind; import org.apache.syncope.common.lib.types.AttrSchemaType; -import org.apache.syncope.common.rest.api.service.JAXRSService; import org.apache.syncope.core.persistence.api.attrvalue.validation.PlainAttrValidationManager; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.DynRealmDAO; @@ -40,32 +31,15 @@ import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; import org.apache.syncope.core.persistence.api.dao.RealmDAO; import org.apache.syncope.core.persistence.api.dao.UserDAO; -import org.apache.syncope.core.persistence.api.dao.search.AnyCond; -import org.apache.syncope.core.persistence.api.dao.search.AnyTypeCond; import org.apache.syncope.core.persistence.api.dao.search.AttrCond; -import org.apache.syncope.core.persistence.api.dao.search.AuxClassCond; -import org.apache.syncope.core.persistence.api.dao.search.DynRealmCond; -import org.apache.syncope.core.persistence.api.dao.search.MemberCond; -import org.apache.syncope.core.persistence.api.dao.search.MembershipCond; import org.apache.syncope.core.persistence.api.dao.search.OrderByClause; -import org.apache.syncope.core.persistence.api.dao.search.PrivilegeCond; -import org.apache.syncope.core.persistence.api.dao.search.RelationshipCond; -import org.apache.syncope.core.persistence.api.dao.search.RelationshipTypeCond; -import org.apache.syncope.core.persistence.api.dao.search.ResourceCond; -import org.apache.syncope.core.persistence.api.dao.search.RoleCond; -import org.apache.syncope.core.persistence.api.dao.search.SearchCond; -import org.apache.syncope.core.persistence.api.entity.Any; -import org.apache.syncope.core.persistence.api.entity.AnyUtils; import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; import org.apache.syncope.core.persistence.api.entity.EntityFactory; import org.apache.syncope.core.persistence.api.entity.PlainAttrValue; import org.apache.syncope.core.persistence.api.entity.PlainSchema; -import org.apache.syncope.core.persistence.api.entity.Realm; public class PGJPAJSONAnySearchDAO extends JPAAnySearchDAO { - protected static final String ALWAYS_FALSE_ASSERTION = "1=2"; - protected static final String POSTGRESQL_REGEX_CHARS = "!$()*+.:<=>?[\\]^{|}-"; protected static String escapeForLikeRegex(final String input) { @@ -136,808 +110,159 @@ protected void parseOrderByForField( item.orderBy = svs.table().alias + '.' + fieldName + ' ' + clause.getDirection().name(); } - protected void fillAttrQuery( - final AnyUtils anyUtils, - final StringBuilder query, + protected AnySearchNode.Leaf filJSONAttrQuery( + final SearchSupport.SearchView from, final PlainAttrValue attrValue, final PlainSchema schema, final AttrCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { + final boolean not) { + + String key = key(schema.getType()); + + String value = Optional.ofNullable(attrValue.getDateValue()). + map(DateTimeFormatter.ISO_OFFSET_DATE_TIME::format). + orElse(cond.getExpression()); + + boolean isStr = true; + boolean lower = false; + if (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) { + 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); + break; + + case Double: + Double.valueOf(value); + break; + + case Boolean: + if (!("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) { + throw new IllegalArgumentException(); + } + break; - if (not && cond.getType() == AttrCond.Type.ISNULL) { - cond.setType(AttrCond.Type.ISNOTNULL); - fillAttrQuery(anyUtils, query, attrValue, schema, cond, true, parameters, svs); - } else if (not) { - query.append("NOT ("); - fillAttrQuery(anyUtils, query, attrValue, schema, cond, false, parameters, svs); - query.append(')'); - } else { - String key = key(schema.getType()); - - String value = Optional.ofNullable(attrValue.getDateValue()). - map(DateTimeFormatter.ISO_OFFSET_DATE_TIME::format). - orElse(cond.getExpression()); - - boolean isStr = true; - boolean lower = false; - if (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) { - 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); - break; - - case Double: - Double.valueOf(value); - break; - - case Boolean: - if (!("true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value))) { - throw new IllegalArgumentException(); - } - break; - - default: - } - - isStr = false; - } catch (Exception nfe) { - // ignore + default: } - } - switch (cond.getType()) { - case ISNULL: - // shouldn't occour: processed before - break; - - case ISNOTNULL: - query.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*]')"); - break; - - case ILIKE: - case LIKE: - // jsonb_path_exists(Nome, '$[*] ? (@.stringValue like_regex "EL.*" flag "i")') - if (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) { - query.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? "). - append("(@.").append(key).append(" like_regex \""). - append(escapeForLikeRegex(value).replace("%", ".*")). - append("\""). - append(lower ? " flag \"i\"" : "").append(")')"); - } else { - query.append(' ').append(ALWAYS_FALSE_ASSERTION); - LOG.error("LIKE is only compatible with string or enum schemas"); - } - break; - - case IEQ: - case EQ: - query.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? "). - append("(@.").append(key); - - if (StringUtils.containsAny(value, POSTGRESQL_REGEX_CHARS) || lower) { - query.append(" like_regex \"^"). - append(escapeForLikeRegex(value).replace("'", "''")). - append("$\""); - } else { - query.append(" == ").append(escapeIfString(value, isStr)); - } - - query.append(lower ? " flag \"i\"" : "").append(")')"); - break; - - case GE: - query.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? "). - append("(@.").append(key).append(" >= "). - append(escapeIfString(value, isStr)).append(")')"); - break; - - case GT: - query.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? "). - append("(@.").append(key).append(" > "). - append(escapeIfString(value, isStr)).append(")')"); - break; - - case LE: - query.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? "). - append("(@.").append(key).append(" <= "). - append(escapeIfString(value, isStr)).append(")')"); - break; - - case LT: - query.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? "). - append("(@.").append(key).append(" < "). - append(escapeIfString(value, isStr)).append(")')"); - break; - - default: + isStr = false; + } catch (Exception nfe) { + // ignore } } - } - - @Override - protected String getQuery( - final AttrCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - Pair checked = check(cond, svs.anyTypeKind); - - StringBuilder query = new StringBuilder(); + StringBuilder clause = new StringBuilder(); switch (cond.getType()) { + case ISNULL: + // shouldn't occour: processed before + break; + case ISNOTNULL: - query.append(not ? " NOT " : ' '). - append("jsonb_path_exists(").append(checked.getLeft().getKey()).append(",'$[*]')"); + clause.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*]')"); break; - case ISNULL: - query.append(not ? ' ' : " NOT "). - append("jsonb_path_exists(").append(checked.getLeft().getKey()).append(",'$[*]')"); + case ILIKE: + case LIKE: + // jsonb_path_exists(Nome, '$[*] ? (@.stringValue like_regex "EL.*" flag "i")') + if (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) { + clause.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? "). + append("(@.").append(key).append(" like_regex \""). + append(escapeForLikeRegex(value).replace("%", ".*")). + append("\""). + append(lower ? " flag \"i\"" : "").append(")')"); + } else { + clause.append(' ').append(ALWAYS_FALSE_CLAUSE); + LOG.error("LIKE is only compatible with string or enum schemas"); + } break; + case IEQ: + case EQ: default: - fillAttrQuery(anyUtilsFactory.getInstance(svs.anyTypeKind), - query, checked.getRight(), checked.getLeft(), cond, not, parameters, svs); - } - - return query.toString(); - } - - @Override - protected String getQuery( - final AnyTypeCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - StringBuilder query = new StringBuilder("type_id"); - - if (not) { - query.append("<>"); - } else { - query.append('='); - } - - query.append('?').append(setParameter(parameters, cond.getAnyTypeKey())); - - return query.toString(); - } - - @Override - protected String getQuery( - final AuxClassCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - StringBuilder query = new StringBuilder(); - - if (not) { - query.append("id NOT IN ("); - } else { - query.append("id IN ("); - } - - query.append("SELECT DISTINCT any_id FROM "). - append(svs.auxClass().name). - append(" WHERE anyTypeClass_id=?"). - append(setParameter(parameters, cond.getAuxClass())). - append(')'); - - return query.toString(); - } - - @Override - protected String getQuery( - final RoleCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - StringBuilder query = new StringBuilder().append('('); - - if (not) { - query.append("id NOT IN ("); - } else { - query.append("id IN ("); - } - - query.append("SELECT DISTINCT any_id FROM "). - append(svs.role().name).append(" WHERE "). - append("role_id=?").append(setParameter(parameters, cond.getRole())). - append(") "); - - if (not) { - query.append("AND id NOT IN ("); - } else { - query.append("OR id IN ("); - } - - query.append("SELECT DISTINCT any_id FROM "). - append(SearchSupport.dynrolemembership().name).append(" WHERE "). - append("role_id=?").append(setParameter(parameters, cond.getRole())). - append(')'); - - query.append(')'); - - return query.toString(); - } - - @Override - protected String getQuery( - final PrivilegeCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - StringBuilder query = new StringBuilder().append('('); - - if (not) { - query.append("id NOT IN ("); - } else { - query.append("id IN ("); - } - - query.append("SELECT DISTINCT any_id FROM "). - append(svs.priv().name).append(" WHERE "). - append("privilege_id=?").append(setParameter(parameters, cond.getPrivilege())). - append(") "); - - if (not) { - query.append("AND id NOT IN ("); - } else { - query.append("OR id IN ("); - } - - query.append("SELECT DISTINCT any_id FROM "). - append(svs.dynpriv().name).append(" WHERE "). - append("privilege_id=?").append(setParameter(parameters, cond.getPrivilege())). - append(')'); - - query.append(')'); - - return query.toString(); - } - - @Override - protected String getQuery( - final DynRealmCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - StringBuilder query = new StringBuilder(); - - if (not) { - query.append("id NOT IN ("); - } else { - query.append("id IN ("); - } - - query.append("SELECT DISTINCT any_id FROM "). - append(SearchSupport.dynrealmmembership().name).append(" WHERE "). - append("dynRealm_id=?").append(setParameter(parameters, cond.getDynRealm())). - append(')'); - - return query.toString(); - } - - @Override - protected String getQuery( - final ResourceCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - StringBuilder query = new StringBuilder(); - - if (not) { - query.append("id NOT IN ("); - } else { - query.append("id IN ("); - } - - query.append("SELECT DISTINCT any_id FROM "). - append(svs.resource().name). - append(" WHERE resource_id=?"). - append(setParameter(parameters, cond.getResource())); - - if (svs.anyTypeKind == AnyTypeKind.USER || svs.anyTypeKind == AnyTypeKind.ANY_OBJECT) { - query.append(" UNION SELECT DISTINCT any_id FROM "). - append(svs.groupResource().name). - append(" WHERE resource_id=?"). - append(setParameter(parameters, cond.getResource())); - } - - query.append(')'); - - return query.toString(); - } - - @Override - protected String getQuery( - final MemberCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - Set members = check(cond); - - StringBuilder query = new StringBuilder().append('('); - - if (not) { - query.append("id NOT IN ("); - } else { - query.append("id IN ("); - } - - query.append("SELECT DISTINCT group_id AS any_id FROM "). - append(new SearchSupport(AnyTypeKind.USER).membership().name).append(" WHERE "). - append(members.stream(). - map(key -> "any_id=?" + setParameter(parameters, key)). - collect(Collectors.joining(" OR "))). - append(") "); - - if (not) { - query.append("AND id NOT IN ("); - } else { - query.append("OR id IN ("); - } - - query.append("SELECT DISTINCT group_id AS any_id FROM "). - append(new SearchSupport(AnyTypeKind.ANY_OBJECT).membership().name).append(" WHERE "). - append(members.stream(). - map(key -> "any_id=?" + setParameter(parameters, key)). - collect(Collectors.joining(" OR "))). - append(')'); - - query.append(')'); - - return query.toString(); - } + clause.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? "). + append("(@.").append(key); - @Override - protected String getQuery( - final RelationshipTypeCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - StringBuilder query = new StringBuilder().append('('); - - if (not) { - query.append("id NOT IN ("); - } else { - query.append("id IN ("); - } - - query.append("SELECT any_id ").append("FROM "). - append(svs.relationship().name). - append(" WHERE type=?").append(setParameter(parameters, cond.getRelationshipTypeKey())). - append(" UNION SELECT right_any_id AS any_id FROM "). - append(svs.relationship().name). - append(" WHERE type=?").append(setParameter(parameters, cond.getRelationshipTypeKey())). - append(')'); - - query.append(')'); - - return query.toString(); - } - - @Override - protected String getQuery( - final RelationshipCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - Set rightAnyObjectKeys = check(cond); - - StringBuilder query = new StringBuilder().append('('); - - if (not) { - query.append("id NOT IN ("); - } else { - query.append("id IN ("); - } - - query.append("SELECT DISTINCT any_id FROM "). - append(svs.relationship().name).append(" WHERE "). - append(rightAnyObjectKeys.stream(). - map(key -> "right_any_id=?" + setParameter(parameters, key)). - collect(Collectors.joining(" OR "))). - append(')'); - - query.append(')'); - - return query.toString(); - } + if (StringUtils.containsAny(value, POSTGRESQL_REGEX_CHARS) || lower) { + clause.append(" like_regex \"^"). + append(escapeForLikeRegex(value).replace("'", "''")). + append("$\""); + } else { + clause.append(" == ").append(escapeIfString(value, isStr)); + } - @Override - protected String getQuery( - final MembershipCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { + clause.append(lower ? " flag \"i\"" : "").append(")')"); + break; - List groupKeys = check(cond); + case GE: + clause.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? "). + append("(@.").append(key).append(" >= "). + append(escapeIfString(value, isStr)).append(")')"); + break; - String where = groupKeys.stream(). - map(key -> "group_id=?" + setParameter(parameters, key)). - collect(Collectors.joining(" OR ")); + case GT: + clause.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? "). + append("(@.").append(key).append(" > "). + append(escapeIfString(value, isStr)).append(")')"); + break; - StringBuilder query = new StringBuilder().append('('); + case LE: + clause.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? "). + append("(@.").append(key).append(" <= "). + append(escapeIfString(value, isStr)).append(")')"); + break; - if (not) { - query.append("id NOT IN ("); - } else { - query.append("id IN ("); + case LT: + clause.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? "). + append("(@.").append(key).append(" < "). + append(escapeIfString(value, isStr)).append(")')"); + break; } - query.append("SELECT DISTINCT any_id FROM "). - append(svs.membership().name).append(" WHERE "). - append('(').append(where).append(')'). - append(") "); - if (not) { - query.append("AND id NOT IN ("); - } else { - query.append("OR id IN ("); - } - - query.append("SELECT DISTINCT any_id FROM "). - append(svs.dyngroupmembership().name).append(" WHERE "). - append('(').append(where).append(')'). - append(')'); - - query.append(')'); - - return query.toString(); - } - - @Override - protected String getQuery( - final AnyCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - if (JAXRSService.PARAM_REALM.equals(cond.getSchema()) - && !SyncopeConstants.UUID_PATTERN.matcher(cond.getExpression()).matches()) { - - Realm realm = realmDAO.findByFullPath(cond.getExpression()); - if (realm == null) { - throw new IllegalArgumentException("Invalid Realm full path: " + cond.getExpression()); - } - cond.setExpression(realm.getKey()); - } - - Triple checked = check(cond, svs.anyTypeKind); - - StringBuilder query = new StringBuilder(); - - PlainSchema schema = plainSchemaDAO.find(cond.getSchema()); - if (schema == null) { - fillAttrQuery(query, checked.getMiddle(), checked.getLeft(), checked.getRight(), not, parameters, svs); - } else { - fillAttrQuery(anyUtilsFactory.getInstance(svs.anyTypeKind), - query, checked.getMiddle(), checked.getLeft(), checked.getRight(), not, parameters, svs); - } - - return query.toString(); - } - - @Override - protected String buildAdminRealmsFilter( - final Set realmKeys, - final SearchSupport svs, - final List parameters) { - - if (realmKeys.isEmpty()) { - return "realm_id IS NOT NULL"; + clause.insert(0, "NOT "); } - String realmKeysArg = realmKeys.stream(). - map(realmKey -> "?" + setParameter(parameters, realmKey)). - collect(Collectors.joining(",")); - return "realm_id IN (" + realmKeysArg + ')'; + return new AnySearchNode.Leaf(from, clause.toString()); } @Override - protected int doCount( - final Realm base, - final boolean recursive, - final Set adminRealms, - final SearchCond cond, - final AnyTypeKind kind) { - - List parameters = new ArrayList<>(); - - SearchSupport svs = buildSearchSupport(kind); - - Triple, Set> filter = - getAdminRealmsFilter(base, recursive, adminRealms, svs, parameters); - - Pair> queryInfo = - getQuery(buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind), parameters, svs); - - StringBuilder queryString = - new StringBuilder("SELECT count(").append(svs.table().alias).append(".id").append(')'); - - buildFromAndWhere(queryString, queryInfo, filter.getLeft(), svs, null); - - Query countQuery = entityManager().createNativeQuery(queryString.toString()); - fillWithParameters(countQuery, parameters); - - return ((Number) countQuery.getSingleResult()).intValue(); - } - - @Override - @SuppressWarnings("unchecked") - protected > List doSearch( - final Realm base, - final boolean recursive, - final Set adminRealms, - final SearchCond cond, - final int page, - final int itemsPerPage, - final List orderBy, - final AnyTypeKind kind) { - - try { - List parameters = new ArrayList<>(); - - SearchSupport svs = buildSearchSupport(kind); - - Triple, Set> filter = - getAdminRealmsFilter(base, recursive, adminRealms, svs, parameters); - - SearchCond effectiveCond = buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind); - - // 1. get the query string from the search condition - Pair> queryInfo = getQuery(effectiveCond, parameters, svs); - - // 2. take into account realms and ordering - OrderBySupport obs = parseOrderBy(svs, orderBy); - - StringBuilder queryString = new StringBuilder("SELECT ").append(svs.table().alias).append(".id"); - obs.items.forEach(item -> queryString.append(',').append(item.select)); - - buildFromAndWhere(queryString, queryInfo, filter.getLeft(), svs, obs); - - LOG.debug("Query: {}, parameters: {}", queryString, parameters); - - queryString.append(buildOrderBy(obs)); - - LOG.debug("Query with auth and order by statements: {}, parameters: {}", queryString, parameters); - - // 3. prepare the search query - Query query = entityManager().createNativeQuery(queryString.toString()); - - // 4. page starts from 1, while setFirtResult() starts from 0 - query.setFirstResult(itemsPerPage * (page <= 0 ? 0 : page - 1)); - - if (itemsPerPage >= 0) { - query.setMaxResults(itemsPerPage); - } - - // 5. populate the search query with parameter values - fillWithParameters(query, parameters); - - // 6. Prepare the result (avoiding duplicates) - return buildResult(query.getResultList(), kind); - } catch (SyncopeClientException e) { - throw e; - } catch (Exception e) { - LOG.error("While searching for {}", kind, e); - } - - return List.of(); - } - - @Override - protected void queryOp( - final StringBuilder query, - final String op, - final Pair> leftInfo, - final Pair> rightInfo) { - - query.append('('). - append(leftInfo.getKey()). - append(' ').append(op).append(' '). - append(rightInfo.getKey()). - append(')'); - } - - @Override - protected void fillAttrQuery( - final StringBuilder query, - final PlainAttrValue attrValue, - final PlainSchema schema, + protected AnySearchNode getQuery( final AttrCond cond, final boolean not, + final Pair checked, final List parameters, final SearchSupport svs) { - if (not && cond.getType() == AttrCond.Type.ISNULL) { - cond.setType(AttrCond.Type.ISNOTNULL); - fillAttrQuery(query, attrValue, schema, cond, true, parameters, svs); - } else if (not) { - query.append("NOT ("); - fillAttrQuery(query, attrValue, schema, cond, false, parameters, svs); - query.append(')'); - } else if (not && cond.getType() == AttrCond.Type.ISNULL) { - cond.setType(AttrCond.Type.ISNOTNULL); - fillAttrQuery(query, attrValue, schema, cond, true, parameters, svs); - } else { - boolean lower = (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) - && (cond.getType() == AttrCond.Type.IEQ || cond.getType() == AttrCond.Type.ILIKE); - - String column = cond.getSchema(); - if ((schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) && lower) { - column = "LOWER (" + column + ')'; - } - - switch (cond.getType()) { - - case ISNULL: - query.append(column).append(" IS NULL"); - break; - - case ISNOTNULL: - query.append(column).append(" IS NOT NULL"); - break; - - case ILIKE: - case LIKE: - if (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) { - query.append(column); - query.append(" LIKE "); - if (lower) { - query.append("LOWER(?").append(setParameter(parameters, cond.getExpression())).append(')'); - } else { - query.append('?').append(setParameter(parameters, cond.getExpression())); - } - } else { - query.append(' ').append(ALWAYS_FALSE_ASSERTION); - LOG.error("LIKE is only compatible with string or enum schemas"); - } - break; - - case IEQ: - case EQ: - query.append(column); - query.append('='); - - if (lower - && (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum)) { - - query.append("LOWER(?").append(setParameter(parameters, attrValue.getValue())).append(')'); - } else { - query.append('?').append(setParameter(parameters, attrValue.getValue())); - } - break; - - case GE: - query.append(column); - if (not) { - query.append('<'); - } else { - query.append(">="); - } - query.append('?').append(setParameter(parameters, attrValue.getValue())); - break; - - case GT: - query.append(column); - if (not) { - query.append("<="); - } else { - query.append('>'); - } - query.append('?').append(setParameter(parameters, attrValue.getValue())); - break; - - case LE: - query.append(column); - if (not) { - query.append('>'); - } else { - query.append("<="); - } - query.append('?').append(setParameter(parameters, attrValue.getValue())); - break; - - case LT: - query.append(column); - if (not) { - query.append(">="); - } else { - query.append('<'); - } - query.append('?').append(setParameter(parameters, attrValue.getValue())); - break; - - default: + // normalize NULL / NOT NULL checks + if (not) { + if (cond.getType() == AttrCond.Type.ISNULL) { + cond.setType(AttrCond.Type.ISNOTNULL); + } else if (cond.getType() == AttrCond.Type.ISNOTNULL) { + cond.setType(AttrCond.Type.ISNULL); } } - } - - protected void buildFromAndWhere( - final StringBuilder queryString, - final Pair> queryInfo, - final String realmsFilter, - final SearchSupport svs, - final OrderBySupport obs) { - - queryString.append(" FROM ").append(svs.table().name).append(' ').append(svs.table().alias); - - Set schemas = queryInfo.getRight(); - - if (obs != null) { - obs.views.stream(). - filter(view -> !svs.field().name.equals(view.name) && !svs.table().name.equals(view.name)). - map(view -> view.name + ' ' + view.alias). - forEach(view -> queryString.append(',').append(view)); - obs.items.forEach(item -> { - String schema = StringUtils.substringBefore(item.orderBy, ' '); - if (StringUtils.isNotBlank(schema)) { - schemas.add(schema); - } - }); - } - - schemas.forEach(schema -> { - // i.e jsonb_path_query(plainattrs, '$[*] ? (@.schema=="Nome")."values"') AS Nome - PlainSchema pschema = plainSchemaDAO.find(schema); - if (pschema == null) { - // just to be sure - LOG.warn("Ignoring invalid schema '{}'", schema); - } else { - queryString.append(','). - append("jsonb_path_query_array(plainattrs, '$[*] ? (@.schema==\""). - append(schema).append("\")."). - append("\"").append(pschema.isUniqueConstraint() ? "uniqueValue" : "values").append("\"')"). - append(" AS ").append(schema); - } - }); - - StringBuilder where = new StringBuilder(); + switch (cond.getType()) { + case ISNOTNULL: + return new AnySearchNode.Leaf( + svs.table(), + "jsonb_path_exists(" + checked.getLeft().getKey() + ",'$[*]')"); - if (queryInfo.getLeft().length() > 0) { - where.append(" WHERE ").append(queryInfo.getLeft()); - } + case ISNULL: + return new AnySearchNode.Leaf( + svs.table(), + "NOT jsonb_path_exists(" + checked.getLeft().getKey() + ",'$[*]')"); - if (queryInfo.getLeft().length() == 0) { - where.append(" WHERE "); - } else { - where.append(" AND "); - } - where.append(realmsFilter); - - if (obs != null) { - String obsWhere = obs.views.stream(). - filter(view -> !svs.field().name.equals(view.name) && !svs.table().name.equals(view.name)). - map(view -> "t.id=" + view.alias + ".any_id"). - collect(Collectors.joining(" AND ")); - if (!obsWhere.isEmpty()) { - if (where.length() == 0) { - where.append(" WHERE "); - } else { - where.append(" AND "); - } - where.append(obsWhere); - } + default: + return filJSONAttrQuery( + svs.table(), + checked.getRight(), + checked.getLeft(), + cond, + not); } - - queryString.append(where); } } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java index fae7fac124e..33f1e234c12 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java @@ -85,10 +85,10 @@ import org.apache.syncope.core.persistence.jpa.content.KeymasterConfParamLoader; import org.apache.syncope.core.persistence.jpa.content.XMLContentExporter; import org.apache.syncope.core.persistence.jpa.content.XMLContentLoader; -import org.apache.syncope.core.persistence.jpa.dao.AnySearchNodeDAO; import org.apache.syncope.core.persistence.jpa.dao.JPAAccessTokenDAO; import org.apache.syncope.core.persistence.jpa.dao.JPAAnyMatchDAO; import org.apache.syncope.core.persistence.jpa.dao.JPAAnyObjectDAO; +import org.apache.syncope.core.persistence.jpa.dao.JPAAnySearchDAO; import org.apache.syncope.core.persistence.jpa.dao.JPAAnyTypeClassDAO; import org.apache.syncope.core.persistence.jpa.dao.JPAAnyTypeDAO; import org.apache.syncope.core.persistence.jpa.dao.JPAApplicationDAO; @@ -384,7 +384,7 @@ public AnySearchDAO anySearchDAO( final AnyUtilsFactory anyUtilsFactory, final PlainAttrValidationManager validator) { - return new AnySearchNodeDAO( + return new JPAAnySearchDAO( realmDAO, dynRealmDAO, userDAO, diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnySearchNodeDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnySearchNodeDAO.java deleted file mode 100644 index d96fae6b5ad..00000000000 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnySearchNodeDAO.java +++ /dev/null @@ -1,1128 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.syncope.core.persistence.jpa.dao; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.function.Supplier; -import java.util.stream.Collectors; -import javax.persistence.Query; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.commons.lang3.tuple.Triple; -import org.apache.syncope.common.lib.SyncopeClientException; -import org.apache.syncope.common.lib.SyncopeConstants; -import org.apache.syncope.common.lib.types.AnyTypeKind; -import org.apache.syncope.common.lib.types.AttrSchemaType; -import org.apache.syncope.common.lib.types.ClientExceptionType; -import org.apache.syncope.common.rest.api.service.JAXRSService; -import org.apache.syncope.core.persistence.api.attrvalue.validation.PlainAttrValidationManager; -import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; -import org.apache.syncope.core.persistence.api.dao.DynRealmDAO; -import org.apache.syncope.core.persistence.api.dao.GroupDAO; -import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; -import org.apache.syncope.core.persistence.api.dao.RealmDAO; -import org.apache.syncope.core.persistence.api.dao.UserDAO; -import org.apache.syncope.core.persistence.api.dao.search.AnyCond; -import org.apache.syncope.core.persistence.api.dao.search.AnyTypeCond; -import org.apache.syncope.core.persistence.api.dao.search.AttrCond; -import org.apache.syncope.core.persistence.api.dao.search.AuxClassCond; -import org.apache.syncope.core.persistence.api.dao.search.DynRealmCond; -import org.apache.syncope.core.persistence.api.dao.search.MemberCond; -import org.apache.syncope.core.persistence.api.dao.search.MembershipCond; -import org.apache.syncope.core.persistence.api.dao.search.OrderByClause; -import org.apache.syncope.core.persistence.api.dao.search.PrivilegeCond; -import org.apache.syncope.core.persistence.api.dao.search.RelationshipCond; -import org.apache.syncope.core.persistence.api.dao.search.RelationshipTypeCond; -import org.apache.syncope.core.persistence.api.dao.search.ResourceCond; -import org.apache.syncope.core.persistence.api.dao.search.RoleCond; -import org.apache.syncope.core.persistence.api.dao.search.SearchCond; -import org.apache.syncope.core.persistence.api.entity.Any; -import org.apache.syncope.core.persistence.api.entity.AnyUtils; -import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; -import org.apache.syncope.core.persistence.api.entity.DynRealm; -import org.apache.syncope.core.persistence.api.entity.EntityFactory; -import org.apache.syncope.core.persistence.api.entity.PlainAttrValue; -import org.apache.syncope.core.persistence.api.entity.PlainSchema; -import org.apache.syncope.core.persistence.api.entity.Realm; -import org.apache.syncope.core.provisioning.api.utils.RealmUtils; - -/** - * Search engine implementation for users, groups and any objects, based on self-updating SQL views. - */ -public class AnySearchNodeDAO extends AbstractAnySearchDAO { - - protected static final String SELECT_COLS_FROM_VIEW = - "any_id,creationContext,creationDate,creator,lastChangeContext," - + "lastChangeDate,lastModifier,status,changePwdDate,cipherAlgorithm,failedLogins," - + "lastLoginDate,mustChangePassword,suspended,username"; - - protected static int setParameter(final List parameters, final Object parameter) { - parameters.add(parameter); - return parameters.size(); - } - - protected static void fillWithParameters(final Query query, final List parameters) { - for (int i = 0; i < parameters.size(); i++) { - if (parameters.get(i) instanceof Boolean) { - query.setParameter(i + 1, ((Boolean) parameters.get(i)) ? 1 : 0); - } else { - query.setParameter(i + 1, parameters.get(i)); - } - } - } - - protected static String key(final AttrSchemaType schemaType) { - String key; - switch (schemaType) { - case Boolean: - key = "booleanValue"; - break; - - case Date: - key = "dateValue"; - break; - - case Double: - key = "doubleValue"; - break; - - case Long: - key = "longValue"; - break; - - case Binary: - key = "binaryValue"; - break; - - default: - key = "stringValue"; - } - - return key; - } - - protected static void visitLeaf( - final AnySearchNode.Leaf node, - final Set from, - final List where) { - - from.add(node.getFrom()); - where.add(node.getClause()); - } - - protected static void visitNode( - final AnySearchNode node, - final Set from, - final List where) { - - node.asLeaf().ifPresentOrElse( - leaf -> visitLeaf(leaf, from, where), - () -> { - List nodeWhere = new ArrayList<>(); - node.getChildren().forEach(child -> visitNode(child, from, nodeWhere)); - where.add(nodeWhere.stream(). - map(w -> "(" + w + ")"). - collect(Collectors.joining(" " + node.getType().name() + " "))); - }); - } - - protected static String buildFrom(final Set from) { - String fromString; - if (from.size() == 1) { - SearchSupport.SearchView sv = from.iterator().next(); - fromString = sv.name + " " + sv.alias; - } else { - List joins = new ArrayList<>(from); - StringBuilder join = new StringBuilder(joins.get(0).name + " " + joins.get(0).alias); - for (int i = 1; i < joins.size(); i++) { - SearchSupport.SearchView sv = joins.get(i); - join.append(" LEFT JOIN "). - append(sv.name).append(" ").append(sv.alias). - append(" ON "). - append(joins.get(0).alias).append(".any_id=").append(sv.alias).append(".any_id"); - } - fromString = join.toString(); - } - return fromString; - } - - protected static String buildWhere(final List where, final AnySearchNode root) { - return where.stream(). - map(w -> "(" + w + ")"). - collect(Collectors.joining(" " + root.getType().name() + " ")); - } - - protected static Supplier syncopeClientException(final String message) { - return () -> { - SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidSearchParameters); - sce.getElements().add(message); - return sce; - }; - } - - public AnySearchNodeDAO( - final RealmDAO realmDAO, - final DynRealmDAO dynRealmDAO, - final UserDAO userDAO, - final GroupDAO groupDAO, - final AnyObjectDAO anyObjectDAO, - final PlainSchemaDAO plainSchemaDAO, - final EntityFactory entityFactory, - final AnyUtilsFactory anyUtilsFactory, - final PlainAttrValidationManager validator) { - - super( - realmDAO, - dynRealmDAO, - userDAO, - groupDAO, - anyObjectDAO, - plainSchemaDAO, - entityFactory, - anyUtilsFactory, - validator); - } - - protected Optional getQueryForCustomConds( - final SearchCond cond, - final List parameters, - final SearchSupport svs, - final boolean not) { - - // do nothing by default, leave it open for subclasses - return Optional.empty(); - } - - protected Optional>> getQuery( - final SearchCond cond, final List parameters, final SearchSupport svs) { - - boolean not = cond.getType() == SearchCond.Type.NOT_LEAF; - - Optional node = Optional.empty(); - Set involvedPlainAttrs = new HashSet<>(); - - switch (cond.getType()) { - case LEAF: - case NOT_LEAF: - if (node.isEmpty()) { - node = cond.getLeaf(AnyTypeCond.class). - filter(leaf -> AnyTypeKind.ANY_OBJECT == svs.anyTypeKind). - map(leaf -> getQuery(leaf, not, parameters, svs)); - } - - if (node.isEmpty()) { - node = cond.getLeaf(AuxClassCond.class). - map(leaf -> getQuery(leaf, not, parameters, svs)); - } - - if (node.isEmpty()) { - node = cond.getLeaf(RelationshipTypeCond.class). - map(leaf -> getQuery(leaf, not, parameters, svs)); - } - - if (node.isEmpty()) { - node = cond.getLeaf(RelationshipCond.class). - map(leaf -> getQuery(leaf, not, parameters, svs)); - } - - if (node.isEmpty()) { - node = cond.getLeaf(MembershipCond.class). - map(leaf -> getQuery(leaf, not, parameters, svs)); - } - - if (node.isEmpty()) { - node = cond.getLeaf(MemberCond.class). - map(leaf -> getQuery(leaf, not, parameters, svs)); - } - - if (node.isEmpty()) { - node = cond.getLeaf(RoleCond.class). - filter(leaf -> AnyTypeKind.USER == svs.anyTypeKind). - map(leaf -> getQuery(leaf, not, parameters, svs)); - } - - if (node.isEmpty()) { - node = cond.getLeaf(PrivilegeCond.class). - filter(leaf -> AnyTypeKind.USER == svs.anyTypeKind). - map(leaf -> getQuery(leaf, not, parameters, svs)); - } - - if (node.isEmpty()) { - node = cond.getLeaf(DynRealmCond.class). - map(leaf -> getQuery(leaf, not, parameters, svs)); - } - - if (node.isEmpty()) { - node = cond.getLeaf(ResourceCond.class). - map(leaf -> getQuery(leaf, not, parameters, svs)); - } - - if (node.isEmpty()) { - node = cond.getLeaf(AnyCond.class). - map(anyCond -> getQuery(anyCond, not, parameters, svs)). - or(() -> cond.getLeaf(AttrCond.class). - map(attrCond -> { - try { - Pair checked = check(attrCond, svs.anyTypeKind); - involvedPlainAttrs.add(checked.getLeft().getKey()); - return getQuery(attrCond, not, checked, parameters, svs); - } catch (IllegalArgumentException e) { - // ignore - return null; - } - })); - } - - // allow for additional search conditions - if (node.isEmpty()) { - node = getQueryForCustomConds(cond, parameters, svs, not); - } - break; - - case AND: - AnySearchNode andNode = new AnySearchNode(AnySearchNode.Type.AND); - - getQuery(cond.getLeft(), parameters, svs).ifPresent(left -> { - andNode.add(left.getLeft()); - involvedPlainAttrs.addAll(left.getRight()); - }); - - getQuery(cond.getRight(), parameters, svs).ifPresent(right -> { - andNode.add(right.getLeft()); - involvedPlainAttrs.addAll(right.getRight()); - }); - - if (!andNode.getChildren().isEmpty()) { - node = Optional.of(andNode); - } - break; - - case OR: - AnySearchNode orNode = new AnySearchNode(AnySearchNode.Type.OR); - - getQuery(cond.getLeft(), parameters, svs).ifPresent(left -> { - orNode.add(left.getLeft()); - involvedPlainAttrs.addAll(left.getRight()); - }); - - getQuery(cond.getRight(), parameters, svs).ifPresent(right -> { - orNode.add(right.getLeft()); - involvedPlainAttrs.addAll(right.getRight()); - }); - - if (!orNode.getChildren().isEmpty()) { - node = Optional.of(orNode); - } - break; - - default: - } - - return node.map(n -> Pair.of(n, involvedPlainAttrs)); - } - - protected AnySearchNode getQuery( - final AnyTypeCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - StringBuilder clause = new StringBuilder("type_id"); - if (not) { - clause.append("<>"); - } else { - clause.append('='); - } - clause.append('?').append(setParameter(parameters, cond.getAnyTypeKey())); - - return new AnySearchNode.Leaf(svs.field(), clause.toString()); - } - - protected AnySearchNode getQuery( - final AuxClassCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - StringBuilder clause = new StringBuilder(); - if (not) { - clause.append("sv.any_id NOT IN ("); - } else { - clause.append("sv.any_id IN ("); - } - clause.append("SELECT any_id FROM "). - append(svs.auxClass().name). - append(" WHERE anyTypeClass_id=?"). - append(setParameter(parameters, cond.getAuxClass())). - append(')'); - - return new AnySearchNode.Leaf(svs.field(), clause.toString()); - } - - protected AnySearchNode getQuery( - final RelationshipTypeCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - StringBuilder clause = new StringBuilder(); - if (not) { - clause.append("sv.any_id NOT IN ("); - } else { - clause.append("sv.any_id IN ("); - } - clause.append("SELECT any_id ").append("FROM "). - append(svs.relationship().name). - append(" WHERE type=?").append(setParameter(parameters, cond.getRelationshipTypeKey())). - append(" UNION SELECT right_any_id AS any_id FROM "). - append(svs.relationship().name). - append(" WHERE type=?").append(setParameter(parameters, cond.getRelationshipTypeKey())). - append(')'); - - return new AnySearchNode.Leaf(svs.field(), clause.toString()); - } - - protected AnySearchNode getQuery( - final RelationshipCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - Set rightAnyObjects = check(cond); - - StringBuilder clause = new StringBuilder(); - if (not) { - clause.append("sv.any_id NOT IN ("); - } else { - clause.append("sv.any_id IN ("); - } - clause.append("SELECT DISTINCT any_id FROM "). - append(svs.relationship().name).append(" WHERE "). - append(rightAnyObjects.stream(). - map(key -> "right_any_id=?" + setParameter(parameters, key)). - collect(Collectors.joining(" OR "))). - append(')'); - - return new AnySearchNode.Leaf(svs.field(), clause.toString()); - } - - protected AnySearchNode getQuery( - final MembershipCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - List groupKeys = check(cond); - - String subwhere = groupKeys.stream(). - map(key -> "group_id=?" + setParameter(parameters, key)). - collect(Collectors.joining(" OR ")); - - StringBuilder clause = new StringBuilder("("); - - if (not) { - clause.append("sv.any_id NOT IN ("); - } else { - clause.append("sv.any_id IN ("); - } - clause.append("SELECT DISTINCT any_id FROM "). - append(svs.membership().name).append(" WHERE "). - append(subwhere). - append(") "); - - if (not) { - clause.append("AND sv.any_id NOT IN ("); - } else { - clause.append("OR sv.any_id IN ("); - } - - clause.append("SELECT DISTINCT any_id FROM "). - append(svs.dyngroupmembership().name).append(" WHERE "). - append(subwhere). - append("))"); - - return new AnySearchNode.Leaf(svs.field(), clause.toString()); - } - - protected AnySearchNode getQuery( - final RoleCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - StringBuilder clause = new StringBuilder("("); - - if (not) { - clause.append("sv.any_id NOT IN ("); - } else { - clause.append("sv.any_id IN ("); - } - - clause.append("SELECT DISTINCT any_id FROM "). - append(svs.role().name).append(" WHERE "). - append("role_id=?").append(setParameter(parameters, cond.getRole())). - append(") "); - - if (not) { - clause.append("AND sv.any_id NOT IN ("); - } else { - clause.append("OR sv.any_id IN ("); - } - - clause.append("SELECT DISTINCT any_id FROM "). - append(SearchSupport.dynrolemembership().name).append(" WHERE "). - append("role_id=?").append(setParameter(parameters, cond.getRole())). - append("))"); - - return new AnySearchNode.Leaf(svs.field(), clause.toString()); - } - - protected AnySearchNode getQuery( - final PrivilegeCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - StringBuilder clause = new StringBuilder("("); - - if (not) { - clause.append("sv.any_id NOT IN ("); - } else { - clause.append("sv.any_id IN ("); - } - - clause.append("SELECT DISTINCT any_id FROM "). - append(svs.priv().name).append(" WHERE "). - append("privilege_id=?").append(setParameter(parameters, cond.getPrivilege())). - append(") "); - - if (not) { - clause.append("AND sv.any_id NOT IN ("); - } else { - clause.append("OR sv.any_id IN ("); - } - - clause.append("SELECT DISTINCT any_id FROM "). - append(svs.dynpriv().name).append(" WHERE "). - append("privilege_id=?").append(setParameter(parameters, cond.getPrivilege())). - append("))"); - - return new AnySearchNode.Leaf(svs.field(), clause.toString()); - } - - protected AnySearchNode getQuery( - final DynRealmCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - StringBuilder clause = new StringBuilder("("); - - if (not) { - clause.append("sv.any_id NOT IN ("); - } else { - clause.append("sv.any_id IN ("); - } - - clause.append("SELECT DISTINCT any_id FROM "). - append(SearchSupport.dynrealmmembership().name).append(" WHERE "). - append("dynRealm_id=?").append(setParameter(parameters, cond.getDynRealm())). - append("))"); - - return new AnySearchNode.Leaf(svs.field(), clause.toString()); - } - - protected AnySearchNode getQuery( - final ResourceCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - StringBuilder clause = new StringBuilder(); - - if (not) { - clause.append("sv.any_id NOT IN ("); - } else { - clause.append("sv.any_id IN ("); - } - - clause.append("SELECT DISTINCT any_id FROM "). - append(svs.resource().name). - append(" WHERE resource_id=?"). - append(setParameter(parameters, cond.getResource())); - - if (svs.anyTypeKind == AnyTypeKind.USER || svs.anyTypeKind == AnyTypeKind.ANY_OBJECT) { - clause.append(" UNION SELECT DISTINCT any_id FROM "). - append(svs.groupResource().name). - append(" WHERE resource_id=?"). - append(setParameter(parameters, cond.getResource())); - } - - clause.append(')'); - - return new AnySearchNode.Leaf(svs.field(), clause.toString()); - } - - protected AnySearchNode getQuery( - final MemberCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - Set members = check(cond); - - StringBuilder clause = new StringBuilder(); - - if (not) { - clause.append("sv.any_id NOT IN ("); - } else { - clause.append("sv.any_id IN ("); - } - - clause.append("SELECT DISTINCT group_id AS any_id FROM "). - append(new SearchSupport(AnyTypeKind.USER).membership().name).append(" WHERE "). - append(members.stream(). - map(key -> "any_id=?" + setParameter(parameters, key)). - collect(Collectors.joining(" OR "))). - append(") "); - - if (not) { - clause.append("AND sv.any_id NOT IN ("); - } else { - clause.append("OR sv.any_id IN ("); - } - - clause.append("SELECT DISTINCT group_id AS any_id FROM "). - append(new SearchSupport(AnyTypeKind.ANY_OBJECT).membership().name).append(" WHERE "). - append(members.stream(). - map(key -> "any_id=?" + setParameter(parameters, key)). - collect(Collectors.joining(" OR "))). - append(')'); - - return new AnySearchNode.Leaf(svs.field(), clause.toString()); - } - - protected AnySearchNode.Leaf fillAttrQuery( - final String column, - final SearchSupport.SearchView from, - final PlainAttrValue attrValue, - final PlainSchema schema, - final AttrCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - // activate ignoreCase only for EQ and LIKE operators - boolean ignoreCase = AttrCond.Type.ILIKE == cond.getType() || AttrCond.Type.IEQ == cond.getType(); - - String left = column; - if (ignoreCase && (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum)) { - left = "LOWER(" + left + ')'; - } - - StringBuilder clause = new StringBuilder(left); - switch (cond.getType()) { - - case ILIKE: - case LIKE: - if (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) { - if (not) { - clause.append(" NOT "); - } - clause.append(" LIKE "); - if (ignoreCase) { - clause.append("LOWER(?").append(setParameter(parameters, cond.getExpression())).append(')'); - } else { - clause.append('?').append(setParameter(parameters, cond.getExpression())); - } - // workaround for Oracle DB adding explicit escape, to search for literal _ (underscore) - if (isOracle()) { - clause.append(" ESCAPE '\\' "); - } - } else { - LOG.error("LIKE is only compatible with string or enum schemas"); - return new AnySearchNode.Leaf(svs.field(), "1=2"); - } - break; - - case IEQ: - case EQ: - default: - if (not) { - clause.append("<>"); - } else { - clause.append('='); - } - if ((schema.getType() == AttrSchemaType.String - || schema.getType() == AttrSchemaType.Enum) && ignoreCase) { - clause.append("LOWER(?").append(setParameter(parameters, attrValue.getValue())).append(')'); - } else { - clause.append('?').append(setParameter(parameters, attrValue.getValue())); - } - break; - - case GE: - if (not) { - clause.append('<'); - } else { - clause.append(">="); - } - clause.append('?').append(setParameter(parameters, attrValue.getValue())); - break; - - case GT: - if (not) { - clause.append("<="); - } else { - clause.append('>'); - } - clause.append('?').append(setParameter(parameters, attrValue.getValue())); - break; - - case LE: - if (not) { - clause.append('>'); - } else { - clause.append("<="); - } - clause.append('?').append(setParameter(parameters, attrValue.getValue())); - break; - - case LT: - if (not) { - clause.append(">="); - } else { - clause.append('<'); - } - clause.append('?').append(setParameter(parameters, attrValue.getValue())); - break; - } - - return new AnySearchNode.Leaf( - from, - cond instanceof AnyCond - ? clause.toString() - : from.alias + ".schema_id='" + schema.getKey() + "' AND " + clause); - } - - protected AnySearchNode getQuery( - final AttrCond cond, - final boolean not, - final Pair checked, - final List parameters, - final SearchSupport svs) { - - SearchSupport.SearchView sv = checked.getLeft().isUniqueConstraint() - ? svs.asSearchViewSupport().uniqueAttr() - : svs.asSearchViewSupport().attr(); - - switch (cond.getType()) { - case ISNOTNULL: - return new AnySearchNode.Leaf( - sv, - sv.alias + ".schema_id='" + checked.getLeft().getKey() + "'"); - - case ISNULL: - String clause = new StringBuilder("sv.any_id NOT IN "). - append('('). - append("SELECT DISTINCT any_id FROM "). - append(sv.name). - append(" WHERE schema_id=").append("'").append(checked.getLeft().getKey()).append("'"). - append(')').toString(); - return new AnySearchNode.Leaf(svs.field(), clause); - - default: - AnySearchNode.Leaf node; - if (not && checked.getLeft().isMultivalue()) { - AnySearchNode.Leaf notNode = fillAttrQuery( - sv.alias + "." + key(checked.getLeft().getType()), - sv, - checked.getRight(), - checked.getLeft(), - cond, - false, - parameters, - svs); - node = new AnySearchNode.Leaf( - sv, - "sv.any_id NOT IN (" - + "SELECT any_id FROM " + sv.name - + " WHERE " + notNode.getClause().replace(sv.alias + ".", "") - + ")"); - } else { - node = fillAttrQuery( - sv.alias + "." + key(checked.getLeft().getType()), - sv, - checked.getRight(), - checked.getLeft(), - cond, - not, - parameters, - svs); - } - return node; - } - } - - protected AnySearchNode getQuery( - final AnyCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - if (JAXRSService.PARAM_REALM.equals(cond.getSchema()) - && !SyncopeConstants.UUID_PATTERN.matcher(cond.getExpression()).matches()) { - - Realm realm = realmDAO.findByFullPath(cond.getExpression()); - if (realm == null) { - throw new IllegalArgumentException("Invalid Realm full path: " + cond.getExpression()); - } - cond.setExpression(realm.getKey()); - } - - Triple checked = check(cond, svs.anyTypeKind); - - switch (checked.getRight().getType()) { - case ISNULL: - return new AnySearchNode.Leaf( - svs.field(), - checked.getRight().getSchema() + (not ? " IS NOT NULL" : " IS NULL")); - - case ISNOTNULL: - return new AnySearchNode.Leaf( - svs.field(), - checked.getRight().getSchema() + (not ? " IS NULL" : " IS NOT NULL")); - - default: - return fillAttrQuery( - checked.getRight().getSchema(), - svs.field(), - checked.getMiddle(), - checked.getLeft(), - checked.getRight(), - not, - parameters, - svs); - } - } - - protected AnySearchNode.Leaf buildAdminRealmsFilter( - final Set realmKeys, - final SearchSupport svs, - final List parameters) { - - if (realmKeys.isEmpty()) { - return new AnySearchNode.Leaf(svs.field(), "any_id IS NOT NULL"); - } - - String realmKeysArg = realmKeys.stream(). - map(realmKey -> "?" + setParameter(parameters, realmKey)). - collect(Collectors.joining(",")); - return new AnySearchNode.Leaf(svs.field(), "realm_id IN (" + realmKeysArg + ")"); - } - - protected Triple, Set> getAdminRealmsFilter( - final Realm base, - final boolean recursive, - final Set adminRealms, - final SearchSupport svs, - final List parameters) { - - Set realmKeys = new HashSet<>(); - Set dynRealmKeys = new HashSet<>(); - Set groupOwners = new HashSet<>(); - - if (recursive) { - adminRealms.forEach(realmPath -> RealmUtils.parseGroupOwnerRealm(realmPath).ifPresentOrElse( - goRealm -> groupOwners.add(goRealm.getRight()), - () -> { - if (realmPath.startsWith("/")) { - Realm realm = Optional.ofNullable(realmDAO.findByFullPath(realmPath)).orElseThrow(() -> { - SyncopeClientException noRealm = - SyncopeClientException.build(ClientExceptionType.InvalidRealm); - noRealm.getElements().add("Invalid realm specified: " + realmPath); - return noRealm; - }); - - realmKeys.addAll(realmDAO.findDescendants(realm.getFullPath(), base.getFullPath())); - } else { - DynRealm dynRealm = dynRealmDAO.find(realmPath); - if (dynRealm == null) { - LOG.warn("Ignoring invalid dynamic realm {}", realmPath); - } else { - dynRealmKeys.add(dynRealm.getKey()); - } - } - })); - if (!dynRealmKeys.isEmpty()) { - realmKeys.clear(); - } - } else { - if (adminRealms.stream().anyMatch(r -> r.startsWith(base.getFullPath()))) { - realmKeys.add(base.getKey()); - } - } - - return Triple.of(buildAdminRealmsFilter(realmKeys, svs, parameters), dynRealmKeys, groupOwners); - } - - @Override - protected int doCount( - final Realm base, - final boolean recursive, - final Set adminRealms, - final SearchCond cond, - final AnyTypeKind kind) { - - List parameters = new ArrayList<>(); - - SearchSupport svs = new SearchViewSupport(kind); - - Triple, Set> filter = - getAdminRealmsFilter(base, recursive, adminRealms, svs, parameters); - - // 1. get query string from search condition - Pair> queryInfo = getQuery( - buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind), parameters, svs). - orElseThrow(syncopeClientException("Invalid search condition")); - - // 2. take realms into account - AnySearchNode root; - if (queryInfo.getLeft().getType() == AnySearchNode.Type.AND) { - root = queryInfo.getLeft(); - } else { - root = new AnySearchNode(AnySearchNode.Type.AND); - root.add(queryInfo.getLeft()); - } - root.add(filter.getLeft()); - - Set from = new HashSet<>(); - List where = new ArrayList<>(); - - visitNode(root, from, where); - - // 3. generate the query string - String queryString = "SELECT COUNT(DISTINCT sv.any_id)" - + " FROM " + buildFrom(from) - + " WHERE " + buildWhere(where, root); - LOG.debug("Query: {}, parameters: {}", queryString, parameters); - - // 4. populate the search query with parameter values - Query countQuery = entityManager().createNativeQuery(queryString); - fillWithParameters(countQuery, parameters); - - // 5. execute the query and return the result - return ((Number) countQuery.getSingleResult()).intValue(); - } - - protected void parseOrderByForPlainSchema( - final SearchSupport svs, - final OrderBySupport obs, - final OrderBySupport.Item item, - final OrderByClause clause, - final PlainSchema schema, - final String fieldName) { - - // keep track of involvement of non-mandatory schemas in the order by clauses - obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition()); - - if (schema.isUniqueConstraint()) { - obs.views.add(svs.asSearchViewSupport().uniqueAttr()); - - item.select = new StringBuilder(). - append(svs.asSearchViewSupport().uniqueAttr().alias).append('.'). - append(key(schema.getType())). - append(" AS ").append(fieldName).toString(); - item.where = new StringBuilder(). - append(svs.asSearchViewSupport().uniqueAttr().alias). - append(".schema_id='").append(fieldName).append("'").toString(); - item.orderBy = fieldName + ' ' + clause.getDirection().name(); - } else { - obs.views.add(svs.asSearchViewSupport().attr()); - - item.select = new StringBuilder(). - append(svs.asSearchViewSupport().attr().alias).append('.').append(key(schema.getType())). - append(" AS ").append(fieldName).toString(); - item.where = new StringBuilder(). - append(svs.asSearchViewSupport().attr().alias). - append(".schema_id='").append(fieldName).append("'").toString(); - item.orderBy = fieldName + ' ' + clause.getDirection().name(); - } - } - - protected void parseOrderByForField( - final SearchSupport svs, - final OrderBySupport.Item item, - final String fieldName, - final OrderByClause clause) { - - item.select = svs.field().alias + '.' + fieldName; - item.where = StringUtils.EMPTY; - item.orderBy = svs.field().alias + '.' + fieldName + ' ' + clause.getDirection().name(); - } - - protected void parseOrderByForCustom( - final SearchSupport svs, - final OrderByClause clause, - final OrderBySupport.Item item, - final OrderBySupport obs) { - - // do nothing by default, meant for subclasses - } - - protected OrderBySupport parseOrderBy( - final SearchSupport svs, - final List orderBy) { - - AnyUtils anyUtils = anyUtilsFactory.getInstance(svs.anyTypeKind); - - OrderBySupport obs = new OrderBySupport(); - - Set orderByUniquePlainSchemas = new HashSet<>(); - Set orderByNonUniquePlainSchemas = new HashSet<>(); - orderBy.forEach(clause -> { - OrderBySupport.Item item = new OrderBySupport.Item(); - - parseOrderByForCustom(svs, clause, item, obs); - - if (item.isEmpty()) { - if (anyUtils.getField(clause.getField()) == null) { - PlainSchema schema = plainSchemaDAO.find(clause.getField()); - if (schema != null) { - if (schema.isUniqueConstraint()) { - orderByUniquePlainSchemas.add(schema.getKey()); - } else { - orderByNonUniquePlainSchemas.add(schema.getKey()); - } - if (orderByUniquePlainSchemas.size() > 1 || orderByNonUniquePlainSchemas.size() > 1) { - throw syncopeClientException("Order by more than one attribute is not allowed; " - + "remove one from " + (orderByUniquePlainSchemas.size() > 1 - ? orderByUniquePlainSchemas : orderByNonUniquePlainSchemas)).get(); - } - parseOrderByForPlainSchema(svs, obs, item, clause, schema, clause.getField()); - } - } else { - // Manage difference among external key attribute and internal JPA @Id - String fieldName = "key".equals(clause.getField()) ? "id" : clause.getField(); - - // Adjust field name to column name - if (ArrayUtils.contains(RELATIONSHIP_FIELDS, fieldName)) { - fieldName += "_id"; - } - - obs.views.add(svs.field()); - - parseOrderByForField(svs, item, fieldName, clause); - } - } - - if (item.isEmpty()) { - LOG.warn("Cannot build any valid clause from {}", clause); - } else { - obs.items.add(item); - } - }); - - return obs; - } - - @Override - @SuppressWarnings("unchecked") - protected > List doSearch( - final Realm base, - final boolean recursive, - final Set adminRealms, - final SearchCond cond, - final int page, - final int itemsPerPage, - final List orderBy, - final AnyTypeKind kind) { - - List parameters = new ArrayList<>(); - - SearchSupport svs = new SearchViewSupport(kind); - - Triple, Set> filter = - getAdminRealmsFilter(base, recursive, adminRealms, svs, parameters); - - // 1. get query string from search condition - Optional>> optionalQueryInfo = getQuery( - buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind), parameters, svs); - if (optionalQueryInfo.isEmpty()) { - LOG.error("Invalid search condition: {}", cond); - return List.of(); - } - Pair> queryInfo = optionalQueryInfo.get(); - - // 2. take realms into account - AnySearchNode root; - if (queryInfo.getLeft().getType() == AnySearchNode.Type.AND) { - root = queryInfo.getLeft(); - } else { - root = new AnySearchNode(AnySearchNode.Type.AND); - root.add(queryInfo.getLeft()); - } - root.add(filter.getLeft()); - - Set from = new HashSet<>(); - List where = new ArrayList<>(); - - visitNode(root, from, where); - - // 3. take ordering into account - OrderBySupport obs = parseOrderBy(svs, orderBy); - - // 4. generate the query string - StringBuilder queryString = new StringBuilder("SELECT DISTINCT sv.any_id"); - obs.items.forEach(item -> queryString.append(',').append(item.select)); - - from.addAll(obs.views); - queryString.append(" FROM ").append(buildFrom(from)); - - queryString.append(" WHERE ").append(buildWhere(where, root)); - - if (!obs.items.isEmpty()) { - queryString.append(" ORDER BY "). - append(obs.items.stream().map(item -> item.orderBy).collect(Collectors.joining(","))); - } - - LOG.debug("Query: {}, parameters: {}", queryString, parameters); - - // 5. prepare the search query - Query query = entityManager().createNativeQuery(queryString.toString()); - - // 6. page starts from 1, while setFirtResult() starts from 0 - query.setFirstResult(itemsPerPage * (page <= 0 ? 0 : page - 1)); - - if (itemsPerPage >= 0) { - query.setMaxResults(itemsPerPage); - } - - // 7. populate the search query with parameter values - fillWithParameters(query, parameters); - - // 8. prepare the result (avoiding duplicates) - return buildResult(query.getResultList(), kind); - } -} diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnySearchDAO.java index 55f9c2ef50b..43ad473bf28 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnySearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnySearchDAO.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.function.Supplier; import java.util.stream.Collectors; import javax.persistence.Query; import org.apache.commons.lang3.ArrayUtils; @@ -76,6 +77,106 @@ public class JPAAnySearchDAO extends AbstractAnySearchDAO { + "lastChangeDate,lastModifier,status,changePwdDate,cipherAlgorithm,failedLogins," + "lastLoginDate,mustChangePassword,suspended,username"; + protected static final String ALWAYS_FALSE_CLAUSE = "1=2"; + + protected static int setParameter(final List parameters, final Object parameter) { + parameters.add(parameter); + return parameters.size(); + } + + protected static void fillWithParameters(final Query query, final List parameters) { + for (int i = 0; i < parameters.size(); i++) { + if (parameters.get(i) instanceof Boolean) { + query.setParameter(i + 1, ((Boolean) parameters.get(i)) ? 1 : 0); + } else { + query.setParameter(i + 1, parameters.get(i)); + } + } + } + + protected static String key(final AttrSchemaType schemaType) { + String key; + switch (schemaType) { + case Boolean: + key = "booleanValue"; + break; + + case Date: + key = "dateValue"; + break; + + case Double: + key = "doubleValue"; + break; + + case Long: + key = "longValue"; + break; + + case Binary: + key = "binaryValue"; + break; + + default: + key = "stringValue"; + } + + return key; + } + + protected static void visitNode( + final AnySearchNode node, + final Set from, + final List where) { + + node.asLeaf().ifPresentOrElse( + leaf -> { + from.add(leaf.getFrom()); + where.add(leaf.getClause()); + }, + () -> { + List nodeWhere = new ArrayList<>(); + node.getChildren().forEach(child -> visitNode(child, from, nodeWhere)); + where.add(nodeWhere.stream(). + map(w -> "(" + w + ")"). + collect(Collectors.joining(" " + node.getType().name() + " "))); + }); + } + + protected static String buildFrom(final Set from) { + String fromString; + if (from.size() == 1) { + SearchSupport.SearchView sv = from.iterator().next(); + fromString = sv.name + " " + sv.alias; + } else { + List joins = new ArrayList<>(from); + StringBuilder join = new StringBuilder(joins.get(0).name + " " + joins.get(0).alias); + for (int i = 1; i < joins.size(); i++) { + SearchSupport.SearchView sv = joins.get(i); + join.append(" LEFT JOIN "). + append(sv.name).append(" ").append(sv.alias). + append(" ON "). + append(joins.get(0).alias).append(".any_id=").append(sv.alias).append(".any_id"); + } + fromString = join.toString(); + } + return fromString; + } + + protected static String buildWhere(final List where, final AnySearchNode root) { + return where.stream(). + map(w -> "(" + w + ")"). + collect(Collectors.joining(" " + root.getType().name() + " ")); + } + + protected static Supplier syncopeClientException(final String message) { + return () -> { + SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidSearchParameters); + sce.getElements().add(message); + return sce; + }; + } + public JPAAnySearchDAO( final RealmDAO realmDAO, final DynRealmDAO dynRealmDAO, @@ -99,1043 +200,930 @@ public JPAAnySearchDAO( validator); } - protected String buildAdminRealmsFilter( - final Set realmKeys, + protected Optional getQueryForCustomConds( + final SearchCond cond, + final List parameters, final SearchSupport svs, - final List parameters) { - - if (realmKeys.isEmpty()) { - return "u.any_id IS NOT NULL"; - } + final boolean not) { - String realmKeysArg = realmKeys.stream(). - map(realmKey -> "?" + setParameter(parameters, realmKey)). - collect(Collectors.joining(",")); - return "u.any_id IN (SELECT any_id FROM " + svs.field().name - + " WHERE realm_id IN (" + realmKeysArg + "))"; + // do nothing by default, leave it open for subclasses + return Optional.empty(); } - protected Triple, Set> getAdminRealmsFilter( - final Realm base, - final boolean recursive, - final Set adminRealms, - final SearchSupport svs, - final List parameters) { - - Set realmKeys = new HashSet<>(); - Set dynRealmKeys = new HashSet<>(); - Set groupOwners = new HashSet<>(); - - if (recursive) { - adminRealms.forEach(realmPath -> RealmUtils.parseGroupOwnerRealm(realmPath).ifPresentOrElse( - goRealm -> groupOwners.add(goRealm.getRight()), - () -> { - if (realmPath.startsWith("/")) { - Realm realm = Optional.ofNullable(realmDAO.findByFullPath(realmPath)).orElseThrow(() -> { - SyncopeClientException noRealm = - SyncopeClientException.build(ClientExceptionType.InvalidRealm); - noRealm.getElements().add("Invalid realm specified: " + realmPath); - return noRealm; - }); - - realmKeys.addAll(realmDAO.findDescendants(realm.getFullPath(), base.getFullPath())); - } else { - DynRealm dynRealm = dynRealmDAO.find(realmPath); - if (dynRealm == null) { - LOG.warn("Ignoring invalid dynamic realm {}", realmPath); - } else { - dynRealmKeys.add(dynRealm.getKey()); - } - } - })); - if (!dynRealmKeys.isEmpty()) { - realmKeys.clear(); - } - } else { - if (adminRealms.stream().anyMatch(r -> r.startsWith(base.getFullPath()))) { - realmKeys.add(base.getKey()); - } - } + protected Optional>> getQuery( + final SearchCond cond, final List parameters, final SearchSupport svs) { - return Triple.of(buildAdminRealmsFilter(realmKeys, svs, parameters), dynRealmKeys, groupOwners); - } + boolean not = cond.getType() == SearchCond.Type.NOT_LEAF; - SearchSupport buildSearchSupport(final AnyTypeKind kind) { - return new SearchViewSupport(kind); - } + Optional node = Optional.empty(); + Set involvedPlainAttrs = new HashSet<>(); - @Override - protected int doCount( - final Realm base, - final boolean recursive, - final Set adminRealms, - final SearchCond cond, - final AnyTypeKind kind) { + switch (cond.getType()) { + case LEAF: + case NOT_LEAF: + if (node.isEmpty()) { + node = cond.getLeaf(AnyTypeCond.class). + filter(leaf -> AnyTypeKind.ANY_OBJECT == svs.anyTypeKind). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } - List parameters = new ArrayList<>(); + if (node.isEmpty()) { + node = cond.getLeaf(AuxClassCond.class). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } - SearchSupport svs = buildSearchSupport(kind); + if (node.isEmpty()) { + node = cond.getLeaf(RelationshipTypeCond.class). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } - Triple, Set> filter = - getAdminRealmsFilter(base, recursive, adminRealms, svs, parameters); + if (node.isEmpty()) { + node = cond.getLeaf(RelationshipCond.class). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } - // 1. get the query string from the search condition - Pair> queryInfo = - getQuery(buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind), parameters, svs); + if (node.isEmpty()) { + node = cond.getLeaf(MembershipCond.class). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } - StringBuilder queryString = queryInfo.getLeft(); + if (node.isEmpty()) { + node = cond.getLeaf(MemberCond.class). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } - // 2. take realms into account - queryString.insert(0, "SELECT u.any_id FROM ("); - queryString.append(") u WHERE ").append(filter.getLeft()); + if (node.isEmpty()) { + node = cond.getLeaf(RoleCond.class). + filter(leaf -> AnyTypeKind.USER == svs.anyTypeKind). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } - // 3. prepare the COUNT query - queryString.insert(0, "SELECT COUNT(any_id) FROM ("); - queryString.append(") count_any_id"); + if (node.isEmpty()) { + node = cond.getLeaf(PrivilegeCond.class). + filter(leaf -> AnyTypeKind.USER == svs.anyTypeKind). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } - Query countQuery = entityManager().createNativeQuery(queryString.toString()); - fillWithParameters(countQuery, parameters); + if (node.isEmpty()) { + node = cond.getLeaf(DynRealmCond.class). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } - return ((Number) countQuery.getSingleResult()).intValue(); - } + if (node.isEmpty()) { + node = cond.getLeaf(ResourceCond.class). + map(leaf -> getQuery(leaf, not, parameters, svs)); + } - @Override - @SuppressWarnings("unchecked") - protected > List doSearch( - final Realm base, - final boolean recursive, - final Set adminRealms, - final SearchCond cond, - final int page, - final int itemsPerPage, - final List orderBy, - final AnyTypeKind kind) { + if (node.isEmpty()) { + node = cond.getLeaf(AnyCond.class). + map(anyCond -> getQuery(anyCond, not, parameters, svs)). + or(() -> cond.getLeaf(AttrCond.class). + map(attrCond -> { + try { + Pair checked = check(attrCond, svs.anyTypeKind); + involvedPlainAttrs.add(checked.getLeft().getKey()); + return getQuery(attrCond, not, checked, parameters, svs); + } catch (IllegalArgumentException e) { + // ignore + return null; + } + })); + } - try { - List parameters = new ArrayList<>(); + // allow for additional search conditions + if (node.isEmpty()) { + node = getQueryForCustomConds(cond, parameters, svs, not); + } + break; - SearchSupport svs = buildSearchSupport(kind); + case AND: + AnySearchNode andNode = new AnySearchNode(AnySearchNode.Type.AND); - Triple, Set> filter = - getAdminRealmsFilter(base, recursive, adminRealms, svs, parameters); + getQuery(cond.getLeft(), parameters, svs).ifPresent(left -> { + andNode.add(left.getLeft()); + involvedPlainAttrs.addAll(left.getRight()); + }); - // 1. get the query string from the search condition - Pair> queryInfo = - getQuery(buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind), parameters, svs); + getQuery(cond.getRight(), parameters, svs).ifPresent(right -> { + andNode.add(right.getLeft()); + involvedPlainAttrs.addAll(right.getRight()); + }); - StringBuilder queryString = queryInfo.getLeft(); + if (!andNode.getChildren().isEmpty()) { + node = Optional.of(andNode); + } + break; - LOG.debug("Query: {}, parameters: {}", queryString, parameters); + case OR: + AnySearchNode orNode = new AnySearchNode(AnySearchNode.Type.OR); - // 2. take into account realms and ordering - OrderBySupport obs = parseOrderBy(svs, orderBy); - if (queryString.charAt(0) == '(') { - queryString.insert(0, buildSelect(obs)); - } else { - queryString.insert(0, buildSelect(obs).append('(')); - queryString.append(')'); - } - queryString. - append(buildWhere(svs, obs)). - append(filter.getLeft()). - append(buildOrderBy(obs)); + getQuery(cond.getLeft(), parameters, svs).ifPresent(left -> { + orNode.add(left.getLeft()); + involvedPlainAttrs.addAll(left.getRight()); + }); - LOG.debug("Query with auth and order by statements: {}, parameters: {}", queryString, parameters); + getQuery(cond.getRight(), parameters, svs).ifPresent(right -> { + orNode.add(right.getLeft()); + involvedPlainAttrs.addAll(right.getRight()); + }); - // 3. prepare the search query - Query query = entityManager().createNativeQuery(queryString.toString()); + if (!orNode.getChildren().isEmpty()) { + node = Optional.of(orNode); + } + break; - // 4. page starts from 1, while setFirtResult() starts from 0 - query.setFirstResult(itemsPerPage * (page <= 0 ? 0 : page - 1)); + default: + } - if (itemsPerPage >= 0) { - query.setMaxResults(itemsPerPage); - } + return node.map(n -> Pair.of(n, involvedPlainAttrs)); + } - // 5. populate the search query with parameter values - fillWithParameters(query, parameters); + protected AnySearchNode getQuery( + final AnyTypeCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { - // 6. Prepare the result (avoiding duplicates) - return buildResult(query.getResultList(), kind); - } catch (SyncopeClientException e) { - throw e; - } catch (Exception e) { - LOG.error("While searching for {}", kind, e); + StringBuilder clause = new StringBuilder("type_id"); + if (not) { + clause.append("<>"); + } else { + clause.append('='); } + clause.append('?').append(setParameter(parameters, cond.getAnyTypeKey())); - return List.of(); + return new AnySearchNode.Leaf(svs.field(), clause.toString()); } - protected int setParameter(final List parameters, final Object parameter) { - parameters.add(parameter); - return parameters.size(); - } + protected AnySearchNode getQuery( + final AuxClassCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { - protected void fillWithParameters(final Query query, final List parameters) { - for (int i = 0; i < parameters.size(); i++) { - if (parameters.get(i) instanceof Boolean) { - query.setParameter(i + 1, ((Boolean) parameters.get(i)) ? 1 : 0); - } else { - query.setParameter(i + 1, parameters.get(i)); - } + StringBuilder clause = new StringBuilder(); + if (not) { + clause.append("sv.any_id NOT IN ("); + } else { + clause.append("sv.any_id IN ("); } + clause.append("SELECT any_id FROM "). + append(svs.auxClass().name). + append(" WHERE anyTypeClass_id=?"). + append(setParameter(parameters, cond.getAuxClass())). + append(')'); + + return new AnySearchNode.Leaf(svs.field(), clause.toString()); } - protected StringBuilder buildSelect(final OrderBySupport obs) { - StringBuilder select = new StringBuilder("SELECT DISTINCT u.any_id"); + protected AnySearchNode getQuery( + final RelationshipTypeCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { - obs.items.forEach(item -> select.append(',').append(item.select)); - select.append(" FROM "); + StringBuilder clause = new StringBuilder(); + if (not) { + clause.append("sv.any_id NOT IN ("); + } else { + clause.append("sv.any_id IN ("); + } + clause.append("SELECT any_id ").append("FROM "). + append(svs.relationship().name). + append(" WHERE type=?").append(setParameter(parameters, cond.getRelationshipTypeKey())). + append(" UNION SELECT right_any_id AS any_id FROM "). + append(svs.relationship().name). + append(" WHERE type=?").append(setParameter(parameters, cond.getRelationshipTypeKey())). + append(')'); - return select; + return new AnySearchNode.Leaf(svs.field(), clause.toString()); } - protected void processOBS( - final SearchSupport svs, - final OrderBySupport obs, - final StringBuilder where) { + protected AnySearchNode getQuery( + final RelationshipCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { - Set attrs = obs.items.stream(). - map(item -> item.orderBy.substring(0, item.orderBy.indexOf(' '))).collect(Collectors.toSet()); + Set rightAnyObjects = check(cond); - obs.views.forEach(searchView -> { - where.append(','); + StringBuilder clause = new StringBuilder(); + if (not) { + clause.append("sv.any_id NOT IN ("); + } else { + clause.append("sv.any_id IN ("); + } + clause.append("SELECT DISTINCT any_id FROM "). + append(svs.relationship().name).append(" WHERE "). + append(rightAnyObjects.stream(). + map(key -> "right_any_id=?" + setParameter(parameters, key)). + collect(Collectors.joining(" OR "))). + append(')'); - boolean searchViewAddedToWhere = false; - if (searchView.name.equals(svs.asSearchViewSupport().attr().name)) { - StringBuilder attrWhere = new StringBuilder(); - StringBuilder nullAttrWhere = new StringBuilder(); + return new AnySearchNode.Leaf(svs.field(), clause.toString()); + } - if (svs.nonMandatorySchemas || obs.nonMandatorySchemas) { - where.append(" (SELECT * FROM ").append(searchView.name); - searchViewAddedToWhere = true; + protected AnySearchNode getQuery( + final MembershipCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { - attrs.forEach(field -> { - if (attrWhere.length() == 0) { - attrWhere.append(" WHERE "); - } else { - attrWhere.append(" OR "); - } - attrWhere.append("schema_id='").append(field).append("'"); - - nullAttrWhere.append(" UNION SELECT any_id, "). - append("'"). - append(field). - append("' AS schema_id, "). - append("null AS booleanvalue, "). - append("null AS datevalue, "). - append("null AS doublevalue, "). - append("null AS longvalue, "). - append("null AS stringvalue FROM ").append(svs.field().name). - append(" WHERE "). - append("any_id NOT IN ("). - append("SELECT any_id FROM "). - append(svs.asSearchViewSupport().attr().name).append(' ').append(searchView.alias). - append(" WHERE ").append("schema_id='").append(field).append("')"); - }); - where.append(attrWhere).append(nullAttrWhere).append(')'); - } - } - if (!searchViewAddedToWhere) { - where.append(searchView.name); - } + List groupKeys = check(cond); - where.append(' ').append(searchView.alias); - }); - } + String subwhere = groupKeys.stream(). + map(key -> "group_id=?" + setParameter(parameters, key)). + collect(Collectors.joining(" OR ")); - protected StringBuilder buildWhere( - final SearchSupport svs, - final OrderBySupport obs) { + StringBuilder clause = new StringBuilder("("); - StringBuilder where = new StringBuilder(" u"); - processOBS(svs, obs, where); - where.append(" WHERE "); + if (not) { + clause.append("sv.any_id NOT IN ("); + } else { + clause.append("sv.any_id IN ("); + } + clause.append("SELECT DISTINCT any_id FROM "). + append(svs.membership().name).append(" WHERE "). + append(subwhere). + append(") "); - obs.views.forEach(searchView -> where.append("u.any_id=").append(searchView.alias).append(".any_id AND ")); + if (not) { + clause.append("AND sv.any_id NOT IN ("); + } else { + clause.append("OR sv.any_id IN ("); + } - obs.items.stream(). - filter(item -> StringUtils.isNotBlank(item.where)). - forEach(item -> where.append(item.where).append(" AND ")); + clause.append("SELECT DISTINCT any_id FROM "). + append(svs.dyngroupmembership().name).append(" WHERE "). + append(subwhere). + append("))"); - return where; + return new AnySearchNode.Leaf(svs.field(), clause.toString()); } - protected StringBuilder buildOrderBy(final OrderBySupport obs) { - StringBuilder orderBy = new StringBuilder(); + protected AnySearchNode getQuery( + final RoleCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { - if (!obs.items.isEmpty()) { - obs.items.forEach(item -> orderBy.append(item.orderBy).append(',')); + StringBuilder clause = new StringBuilder("("); - orderBy.insert(0, " ORDER BY "); - orderBy.deleteCharAt(orderBy.length() - 1); + if (not) { + clause.append("sv.any_id NOT IN ("); + } else { + clause.append("sv.any_id IN ("); } - return orderBy; - } - - protected String key(final AttrSchemaType schemaType) { - String key; - switch (schemaType) { - case Boolean: - key = "booleanValue"; - break; - - case Date: - key = "dateValue"; - break; - - case Double: - key = "doubleValue"; - break; - - case Long: - key = "longValue"; - break; - - case Binary: - key = "binaryValue"; - break; - - default: - key = "stringValue"; - } - - return key; - } - - protected void parseOrderByForPlainSchema( - final SearchSupport svs, - final OrderBySupport obs, - final OrderBySupport.Item item, - final OrderByClause clause, - final PlainSchema schema, - final String fieldName) { - - // keep track of involvement of non-mandatory schemas in the order by clauses - obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition()); - - if (schema.isUniqueConstraint()) { - obs.views.add(svs.asSearchViewSupport().uniqueAttr()); + clause.append("SELECT DISTINCT any_id FROM "). + append(svs.role().name).append(" WHERE "). + append("role_id=?").append(setParameter(parameters, cond.getRole())). + append(") "); - item.select = new StringBuilder(). - append(svs.asSearchViewSupport().uniqueAttr().alias).append('.'). - append(key(schema.getType())). - append(" AS ").append(fieldName).toString(); - item.where = new StringBuilder(). - append(svs.asSearchViewSupport().uniqueAttr().alias). - append(".schema_id='").append(fieldName).append("'").toString(); - item.orderBy = fieldName + ' ' + clause.getDirection().name(); + if (not) { + clause.append("AND sv.any_id NOT IN ("); } else { - obs.views.add(svs.asSearchViewSupport().attr()); - - item.select = new StringBuilder(). - append(svs.asSearchViewSupport().attr().alias).append('.').append(key(schema.getType())). - append(" AS ").append(fieldName).toString(); - item.where = new StringBuilder(). - append(svs.asSearchViewSupport().attr().alias). - append(".schema_id='").append(fieldName).append("'").toString(); - item.orderBy = fieldName + ' ' + clause.getDirection().name(); + clause.append("OR sv.any_id IN ("); } - } - - protected void parseOrderByForField( - final SearchSupport svs, - final OrderBySupport.Item item, - final String fieldName, - final OrderByClause clause) { - - item.select = svs.field().alias + '.' + fieldName; - item.where = StringUtils.EMPTY; - item.orderBy = svs.field().alias + '.' + fieldName + ' ' + clause.getDirection().name(); - } - - protected void parseOrderByForCustom( - final SearchSupport svs, - final OrderByClause clause, - final OrderBySupport.Item item, - final OrderBySupport obs) { - - // do nothing by default, meant for subclasses - } - - protected OrderBySupport parseOrderBy( - final SearchSupport svs, - final List orderBy) { - - AnyUtils anyUtils = anyUtilsFactory.getInstance(svs.anyTypeKind); - - OrderBySupport obs = new OrderBySupport(); - - Set orderByUniquePlainSchemas = new HashSet<>(); - Set orderByNonUniquePlainSchemas = new HashSet<>(); - orderBy.forEach(clause -> { - OrderBySupport.Item item = new OrderBySupport.Item(); - - parseOrderByForCustom(svs, clause, item, obs); - - if (item.isEmpty()) { - if (anyUtils.getField(clause.getField()) == null) { - PlainSchema schema = plainSchemaDAO.find(clause.getField()); - if (schema != null) { - if (schema.isUniqueConstraint()) { - orderByUniquePlainSchemas.add(schema.getKey()); - } else { - orderByNonUniquePlainSchemas.add(schema.getKey()); - } - if (orderByUniquePlainSchemas.size() > 1 || orderByNonUniquePlainSchemas.size() > 1) { - SyncopeClientException invalidSearch = - SyncopeClientException.build(ClientExceptionType.InvalidSearchParameters); - invalidSearch.getElements().add("Order by more than one attribute is not allowed; " - + "remove one from " + (orderByUniquePlainSchemas.size() > 1 - ? orderByUniquePlainSchemas : orderByNonUniquePlainSchemas)); - throw invalidSearch; - } - parseOrderByForPlainSchema(svs, obs, item, clause, schema, clause.getField()); - } - } else { - // Manage difference among external key attribute and internal JPA @Id - String fieldName = "key".equals(clause.getField()) ? "id" : clause.getField(); - - // Adjust field name to column name - if (ArrayUtils.contains(RELATIONSHIP_FIELDS, fieldName)) { - fieldName += "_id"; - } - - obs.views.add(svs.field()); - - parseOrderByForField(svs, item, fieldName, clause); - } - } - - if (item.isEmpty()) { - LOG.warn("Cannot build any valid clause from {}", clause); - } else { - obs.items.add(item); - } - }); - - return obs; - } - - protected void getQueryForCustomConds( - final SearchCond cond, - final List parameters, - final SearchSupport svs, - final boolean not, - final StringBuilder query) { - - // do nothing by default, leave it open for subclasses - } - - protected void queryOp( - final StringBuilder query, - final String op, - final Pair> leftInfo, - final Pair> rightInfo) { - - String subQuery = leftInfo.getKey().toString(); - // Add extra parentheses - subQuery = subQuery.replaceFirst("WHERE ", "WHERE ("); - query.append(subQuery). - append(' ').append(op).append(" any_id IN ( ").append(rightInfo.getKey()).append("))"); - } - - protected Pair> getQuery( - final SearchCond cond, final List parameters, final SearchSupport svs) { - - boolean not = cond.getType() == SearchCond.Type.NOT_LEAF; - - StringBuilder query = new StringBuilder(); - Set involvedPlainAttrs = new HashSet<>(); - - switch (cond.getType()) { - case LEAF: - case NOT_LEAF: - cond.getLeaf(AnyTypeCond.class). - filter(leaf -> AnyTypeKind.ANY_OBJECT == svs.anyTypeKind). - ifPresent(leaf -> query.append(getQuery(leaf, not, parameters, svs))); - - cond.getLeaf(AuxClassCond.class). - ifPresent(leaf -> query.append(getQuery(leaf, not, parameters, svs))); - - cond.getLeaf(RelationshipTypeCond.class). - filter(leaf -> AnyTypeKind.GROUP != svs.anyTypeKind). - ifPresent(leaf -> query.append(getQuery(leaf, not, parameters, svs))); - - cond.getLeaf(RelationshipCond.class). - filter(leaf -> AnyTypeKind.GROUP != svs.anyTypeKind). - ifPresent(leaf -> query.append(getQuery(leaf, not, parameters, svs))); - - cond.getLeaf(MembershipCond.class). - filter(leaf -> AnyTypeKind.GROUP != svs.anyTypeKind). - ifPresent(leaf -> query.append(getQuery(leaf, not, parameters, svs))); - - cond.getLeaf(MemberCond.class). - filter(leaf -> AnyTypeKind.GROUP == svs.anyTypeKind). - ifPresent(leaf -> query.append(getQuery(leaf, not, parameters, svs))); - - cond.getLeaf(RoleCond.class). - filter(leaf -> AnyTypeKind.USER == svs.anyTypeKind). - ifPresent(leaf -> query.append(getQuery(leaf, not, parameters, svs))); - - cond.getLeaf(PrivilegeCond.class). - filter(leaf -> AnyTypeKind.USER == svs.anyTypeKind). - ifPresent(leaf -> query.append(getQuery(leaf, not, parameters, svs))); - - cond.getLeaf(DynRealmCond.class). - ifPresent(leaf -> query.append(getQuery(leaf, not, parameters, svs))); - - cond.getLeaf(ResourceCond.class). - ifPresent(leaf -> query.append(getQuery(leaf, not, parameters, svs))); - - cond.getLeaf(AnyCond.class).ifPresentOrElse( - anyCond -> { - query.append(getQuery(anyCond, not, parameters, svs)); - }, - () -> { - cond.getLeaf(AttrCond.class).ifPresent(leaf -> { - query.append(getQuery(leaf, not, parameters, svs)); - try { - involvedPlainAttrs.add(check(leaf, svs.anyTypeKind).getLeft().getKey()); - } catch (IllegalArgumentException e) { - // ignore - } - }); - }); - - // allow for additional search conditions - getQueryForCustomConds(cond, parameters, svs, not, query); - break; - - case AND: - Pair> leftAndInfo = getQuery(cond.getLeft(), parameters, svs); - involvedPlainAttrs.addAll(leftAndInfo.getRight()); - - Pair> rigthAndInfo = getQuery(cond.getRight(), parameters, svs); - involvedPlainAttrs.addAll(rigthAndInfo.getRight()); - - queryOp(query, "AND", leftAndInfo, rigthAndInfo); - break; - - case OR: - Pair> leftOrInfo = getQuery(cond.getLeft(), parameters, svs); - involvedPlainAttrs.addAll(leftOrInfo.getRight()); - - Pair> rigthOrInfo = getQuery(cond.getRight(), parameters, svs); - involvedPlainAttrs.addAll(rigthOrInfo.getRight()); - queryOp(query, "OR", leftOrInfo, rigthOrInfo); - break; - - default: - } + clause.append("SELECT DISTINCT any_id FROM "). + append(SearchSupport.dynrolemembership().name).append(" WHERE "). + append("role_id=?").append(setParameter(parameters, cond.getRole())). + append("))"); - return Pair.of(query, involvedPlainAttrs); + return new AnySearchNode.Leaf(svs.field(), clause.toString()); } - protected String getQuery( - final AnyTypeCond cond, + protected AnySearchNode getQuery( + final PrivilegeCond cond, final boolean not, final List parameters, final SearchSupport svs) { - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE type_id"); + StringBuilder clause = new StringBuilder("("); if (not) { - query.append("<>"); + clause.append("sv.any_id NOT IN ("); } else { - query.append('='); + clause.append("sv.any_id IN ("); } - query.append('?').append(setParameter(parameters, cond.getAnyTypeKey())); - - return query.toString(); - } - - protected String getQuery( - final AuxClassCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { - - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE "); + clause.append("SELECT DISTINCT any_id FROM "). + append(svs.priv().name).append(" WHERE "). + append("privilege_id=?").append(setParameter(parameters, cond.getPrivilege())). + append(") "); if (not) { - query.append("any_id NOT IN ("); + clause.append("AND sv.any_id NOT IN ("); } else { - query.append("any_id IN ("); + clause.append("OR sv.any_id IN ("); } - query.append("SELECT DISTINCT any_id FROM "). - append(svs.auxClass().name). - append(" WHERE anyTypeClass_id=?"). - append(setParameter(parameters, cond.getAuxClass())). - append(')'); + clause.append("SELECT DISTINCT any_id FROM "). + append(svs.dynpriv().name).append(" WHERE "). + append("privilege_id=?").append(setParameter(parameters, cond.getPrivilege())). + append("))"); - return query.toString(); + return new AnySearchNode.Leaf(svs.field(), clause.toString()); } - protected String getQuery( - final RelationshipTypeCond cond, + protected AnySearchNode getQuery( + final DynRealmCond cond, final boolean not, final List parameters, final SearchSupport svs) { - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE "); + StringBuilder clause = new StringBuilder("("); if (not) { - query.append("any_id NOT IN ("); + clause.append("sv.any_id NOT IN ("); } else { - query.append("any_id IN ("); + clause.append("sv.any_id IN ("); } - query.append("SELECT any_id ").append("FROM "). - append(svs.relationship().name). - append(" WHERE type=?").append(setParameter(parameters, cond.getRelationshipTypeKey())). - append(" UNION SELECT right_any_id AS any_id FROM "). - append(svs.relationship().name). - append(" WHERE type=?").append(setParameter(parameters, cond.getRelationshipTypeKey())). - append(')'); + clause.append("SELECT DISTINCT any_id FROM "). + append(SearchSupport.dynrealmmembership().name).append(" WHERE "). + append("dynRealm_id=?").append(setParameter(parameters, cond.getDynRealm())). + append("))"); - return query.toString(); + return new AnySearchNode.Leaf(svs.field(), clause.toString()); } - protected String getQuery( - final RelationshipCond cond, + protected AnySearchNode getQuery( + final ResourceCond cond, final boolean not, final List parameters, final SearchSupport svs) { - Set rightAnyObjects = check(cond); - - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE "); + StringBuilder clause = new StringBuilder(); if (not) { - query.append("any_id NOT IN ("); + clause.append("sv.any_id NOT IN ("); } else { - query.append("any_id IN ("); + clause.append("sv.any_id IN ("); } - query.append("SELECT DISTINCT any_id FROM "). - append(svs.relationship().name).append(" WHERE "). - append(rightAnyObjects.stream(). - map(key -> "right_any_id=?" + setParameter(parameters, key)). - collect(Collectors.joining(" OR "))). - append(')'); + clause.append("SELECT DISTINCT any_id FROM "). + append(svs.resource().name). + append(" WHERE resource_id=?"). + append(setParameter(parameters, cond.getResource())); + + if (svs.anyTypeKind == AnyTypeKind.USER || svs.anyTypeKind == AnyTypeKind.ANY_OBJECT) { + clause.append(" UNION SELECT DISTINCT any_id FROM "). + append(svs.groupResource().name). + append(" WHERE resource_id=?"). + append(setParameter(parameters, cond.getResource())); + } - return query.toString(); + clause.append(')'); + + return new AnySearchNode.Leaf(svs.field(), clause.toString()); } - protected String getQuery( - final MembershipCond cond, + protected AnySearchNode getQuery( + final MemberCond cond, final boolean not, final List parameters, final SearchSupport svs) { - List groupKeys = check(cond); - - String where = groupKeys.stream(). - map(key -> "group_id=?" + setParameter(parameters, key)). - collect(Collectors.joining(" OR ")); + Set members = check(cond); - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE ("); + StringBuilder clause = new StringBuilder(); if (not) { - query.append("any_id NOT IN ("); + clause.append("sv.any_id NOT IN ("); } else { - query.append("any_id IN ("); + clause.append("sv.any_id IN ("); } - query.append("SELECT DISTINCT any_id FROM "). - append(svs.membership().name).append(" WHERE "). - append(where). + clause.append("SELECT DISTINCT group_id AS any_id FROM "). + append(new SearchSupport(AnyTypeKind.USER).membership().name).append(" WHERE "). + append(members.stream(). + map(key -> "any_id=?" + setParameter(parameters, key)). + collect(Collectors.joining(" OR "))). append(") "); if (not) { - query.append("AND any_id NOT IN ("); + clause.append("AND sv.any_id NOT IN ("); } else { - query.append("OR any_id IN ("); + clause.append("OR sv.any_id IN ("); } - query.append("SELECT DISTINCT any_id FROM "). - append(svs.dyngroupmembership().name).append(" WHERE "). - append(where). - append("))"); + clause.append("SELECT DISTINCT group_id AS any_id FROM "). + append(new SearchSupport(AnyTypeKind.ANY_OBJECT).membership().name).append(" WHERE "). + append(members.stream(). + map(key -> "any_id=?" + setParameter(parameters, key)). + collect(Collectors.joining(" OR "))). + append(')'); - return query.toString(); + return new AnySearchNode.Leaf(svs.field(), clause.toString()); } - protected String getQuery( - final RoleCond cond, + protected AnySearchNode.Leaf fillAttrQuery( + final String column, + final SearchSupport.SearchView from, + final PlainAttrValue attrValue, + final PlainSchema schema, + final AttrCond cond, final boolean not, - final List parameters, - final SearchSupport svs) { + final List parameters) { - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE ("); + // activate ignoreCase only for EQ and LIKE operators + boolean ignoreCase = AttrCond.Type.ILIKE == cond.getType() || AttrCond.Type.IEQ == cond.getType(); - if (not) { - query.append("any_id NOT IN ("); - } else { - query.append("any_id IN ("); + String left = column; + if (ignoreCase && (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum)) { + left = "LOWER(" + left + ')'; } - query.append("SELECT DISTINCT any_id FROM "). - append(svs.role().name).append(" WHERE "). - append("role_id=?").append(setParameter(parameters, cond.getRole())). - append(") "); + StringBuilder clause = new StringBuilder(left); + switch (cond.getType()) { - if (not) { - query.append("AND any_id NOT IN ("); - } else { - query.append("OR any_id IN ("); - } + case ILIKE: + case LIKE: + if (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) { + if (not) { + clause.append(" NOT "); + } + clause.append(" LIKE "); + if (ignoreCase) { + clause.append("LOWER(?").append(setParameter(parameters, cond.getExpression())).append(')'); + } else { + clause.append('?').append(setParameter(parameters, cond.getExpression())); + } + // workaround for Oracle DB adding explicit escape, to search for literal _ (underscore) + if (isOracle()) { + clause.append(" ESCAPE '\\' "); + } + } else { + LOG.error("LIKE is only compatible with string or enum schemas"); + return new AnySearchNode.Leaf(from, ALWAYS_FALSE_CLAUSE); + } + break; - query.append("SELECT DISTINCT any_id FROM "). - append(SearchSupport.dynrolemembership().name).append(" WHERE "). - append("role_id=?").append(setParameter(parameters, cond.getRole())). - append("))"); + case IEQ: + case EQ: + default: + if (not) { + clause.append("<>"); + } else { + clause.append('='); + } + if ((schema.getType() == AttrSchemaType.String + || schema.getType() == AttrSchemaType.Enum) && ignoreCase) { + clause.append("LOWER(?").append(setParameter(parameters, attrValue.getValue())).append(')'); + } else { + clause.append('?').append(setParameter(parameters, attrValue.getValue())); + } + break; + + case GE: + if (not) { + clause.append('<'); + } else { + clause.append(">="); + } + clause.append('?').append(setParameter(parameters, attrValue.getValue())); + break; + + case GT: + if (not) { + clause.append("<="); + } else { + clause.append('>'); + } + clause.append('?').append(setParameter(parameters, attrValue.getValue())); + break; + + case LE: + if (not) { + clause.append('>'); + } else { + clause.append("<="); + } + clause.append('?').append(setParameter(parameters, attrValue.getValue())); + break; + + case LT: + if (not) { + clause.append(">="); + } else { + clause.append('<'); + } + clause.append('?').append(setParameter(parameters, attrValue.getValue())); + break; + } - return query.toString(); + return new AnySearchNode.Leaf( + from, + cond instanceof AnyCond + ? clause.toString() + : from.alias + ".schema_id='" + schema.getKey() + "' AND " + clause); } - protected String getQuery( - final PrivilegeCond cond, + protected AnySearchNode getQuery( + final AttrCond cond, final boolean not, + final Pair checked, final List parameters, final SearchSupport svs) { - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE ("); - + // normalize NULL / NOT NULL checks if (not) { - query.append("any_id NOT IN ("); - } else { - query.append("any_id IN ("); + if (cond.getType() == AttrCond.Type.ISNULL) { + cond.setType(AttrCond.Type.ISNOTNULL); + } else if (cond.getType() == AttrCond.Type.ISNOTNULL) { + cond.setType(AttrCond.Type.ISNULL); + } } - query.append("SELECT DISTINCT any_id FROM "). - append(svs.priv().name).append(" WHERE "). - append("privilege_id=?").append(setParameter(parameters, cond.getPrivilege())). - append(") "); + SearchSupport.SearchView sv = checked.getLeft().isUniqueConstraint() + ? svs.asSearchViewSupport().uniqueAttr() + : svs.asSearchViewSupport().attr(); - if (not) { - query.append("AND any_id NOT IN ("); - } else { - query.append("OR any_id IN ("); - } + switch (cond.getType()) { + case ISNOTNULL: + return new AnySearchNode.Leaf( + sv, + sv.alias + ".schema_id='" + checked.getLeft().getKey() + "'"); - query.append("SELECT DISTINCT any_id FROM "). - append(svs.dynpriv().name).append(" WHERE "). - append("privilege_id=?").append(setParameter(parameters, cond.getPrivilege())). - append("))"); + case ISNULL: + String clause = new StringBuilder("sv.any_id NOT IN "). + append('('). + append("SELECT DISTINCT any_id FROM "). + append(sv.name). + append(" WHERE schema_id=").append("'").append(checked.getLeft().getKey()).append("'"). + append(')').toString(); + return new AnySearchNode.Leaf(svs.field(), clause); - return query.toString(); + default: + AnySearchNode.Leaf node; + if (not && checked.getLeft().isMultivalue()) { + AnySearchNode.Leaf notNode = fillAttrQuery( + sv.alias + "." + key(checked.getLeft().getType()), + sv, + checked.getRight(), + checked.getLeft(), + cond, + false, + parameters); + node = new AnySearchNode.Leaf( + sv, + "sv.any_id NOT IN (" + + "SELECT any_id FROM " + sv.name + + " WHERE " + notNode.getClause().replace(sv.alias + ".", "") + + ")"); + } else { + node = fillAttrQuery( + sv.alias + "." + key(checked.getLeft().getType()), + sv, + checked.getRight(), + checked.getLeft(), + cond, + not, + parameters); + } + return node; + } } - protected String getQuery( - final DynRealmCond cond, + protected AnySearchNode getQuery( + final AnyCond cond, final boolean not, final List parameters, final SearchSupport svs) { - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE ("); + if (JAXRSService.PARAM_REALM.equals(cond.getSchema()) + && !SyncopeConstants.UUID_PATTERN.matcher(cond.getExpression()).matches()) { - if (not) { - query.append("any_id NOT IN ("); - } else { - query.append("any_id IN ("); + Realm realm = realmDAO.findByFullPath(cond.getExpression()); + if (realm == null) { + throw new IllegalArgumentException("Invalid Realm full path: " + cond.getExpression()); + } + cond.setExpression(realm.getKey()); } - query.append("SELECT DISTINCT any_id FROM "). - append(SearchSupport.dynrealmmembership().name).append(" WHERE "). - append("dynRealm_id=?").append(setParameter(parameters, cond.getDynRealm())). - append("))"); - - return query.toString(); - } + Triple checked = check(cond, svs.anyTypeKind); - protected String getQuery( - final ResourceCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { + switch (checked.getRight().getType()) { + case ISNULL: + return new AnySearchNode.Leaf( + svs.field(), + checked.getRight().getSchema() + (not ? " IS NOT NULL" : " IS NULL")); - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE "); + case ISNOTNULL: + return new AnySearchNode.Leaf( + svs.field(), + checked.getRight().getSchema() + (not ? " IS NULL" : " IS NOT NULL")); - if (not) { - query.append("any_id NOT IN ("); - } else { - query.append("any_id IN ("); + default: + return fillAttrQuery( + checked.getRight().getSchema(), + svs.field(), + checked.getMiddle(), + checked.getLeft(), + checked.getRight(), + not, + parameters); } + } - query.append("SELECT DISTINCT any_id FROM "). - append(svs.resource().name). - append(" WHERE resource_id=?"). - append(setParameter(parameters, cond.getResource())); + protected AnySearchNode.Leaf buildAdminRealmsFilter( + final Set realmKeys, + final SearchSupport svs, + final List parameters) { - if (svs.anyTypeKind == AnyTypeKind.USER || svs.anyTypeKind == AnyTypeKind.ANY_OBJECT) { - query.append(" UNION SELECT DISTINCT any_id FROM "). - append(svs.groupResource().name). - append(" WHERE resource_id=?"). - append(setParameter(parameters, cond.getResource())); + if (realmKeys.isEmpty()) { + return new AnySearchNode.Leaf(svs.field(), "any_id IS NOT NULL"); } - query.append(')'); - - return query.toString(); + String realmKeysArg = realmKeys.stream(). + map(realmKey -> "?" + setParameter(parameters, realmKey)). + collect(Collectors.joining(",")); + return new AnySearchNode.Leaf(svs.field(), "realm_id IN (" + realmKeysArg + ")"); } - protected String getQuery( - final MemberCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { + protected Triple, Set> getAdminRealmsFilter( + final Realm base, + final boolean recursive, + final Set adminRealms, + final SearchSupport svs, + final List parameters) { - Set members = check(cond); + Set realmKeys = new HashSet<>(); + Set dynRealmKeys = new HashSet<>(); + Set groupOwners = new HashSet<>(); - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE "); + if (recursive) { + adminRealms.forEach(realmPath -> RealmUtils.parseGroupOwnerRealm(realmPath).ifPresentOrElse( + goRealm -> groupOwners.add(goRealm.getRight()), + () -> { + if (realmPath.startsWith("/")) { + Realm realm = Optional.ofNullable(realmDAO.findByFullPath(realmPath)).orElseThrow(() -> { + SyncopeClientException noRealm = + SyncopeClientException.build(ClientExceptionType.InvalidRealm); + noRealm.getElements().add("Invalid realm specified: " + realmPath); + return noRealm; + }); - if (not) { - query.append("any_id NOT IN ("); + realmKeys.addAll(realmDAO.findDescendants(realm.getFullPath(), base.getFullPath())); + } else { + DynRealm dynRealm = dynRealmDAO.find(realmPath); + if (dynRealm == null) { + LOG.warn("Ignoring invalid dynamic realm {}", realmPath); + } else { + dynRealmKeys.add(dynRealm.getKey()); + } + } + })); + if (!dynRealmKeys.isEmpty()) { + realmKeys.clear(); + } } else { - query.append("any_id IN ("); + if (adminRealms.stream().anyMatch(r -> r.startsWith(base.getFullPath()))) { + realmKeys.add(base.getKey()); + } } - query.append("SELECT DISTINCT group_id AS any_id FROM "). - append(new SearchSupport(AnyTypeKind.USER).membership().name).append(" WHERE "). - append(members.stream(). - map(key -> "any_id=?" + setParameter(parameters, key)). - collect(Collectors.joining(" OR "))). - append(") "); + return Triple.of(buildAdminRealmsFilter(realmKeys, svs, parameters), dynRealmKeys, groupOwners); + } - if (not) { - query.append("AND any_id NOT IN ("); + @Override + protected int doCount( + final Realm base, + final boolean recursive, + final Set adminRealms, + final SearchCond cond, + final AnyTypeKind kind) { + + List parameters = new ArrayList<>(); + + SearchSupport svs = new SearchViewSupport(kind); + + Triple, Set> filter = + getAdminRealmsFilter(base, recursive, adminRealms, svs, parameters); + + // 1. get query string from search condition + Pair> queryInfo = getQuery( + buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind), parameters, svs). + orElseThrow(syncopeClientException("Invalid search condition")); + + // 2. take realms into account + AnySearchNode root; + if (queryInfo.getLeft().getType() == AnySearchNode.Type.AND) { + root = queryInfo.getLeft(); } else { - query.append("OR any_id IN ("); + root = new AnySearchNode(AnySearchNode.Type.AND); + root.add(queryInfo.getLeft()); } + root.add(filter.getLeft()); - query.append("SELECT DISTINCT group_id AS any_id FROM "). - append(new SearchSupport(AnyTypeKind.ANY_OBJECT).membership().name).append(" WHERE "). - append(members.stream(). - map(key -> "any_id=?" + setParameter(parameters, key)). - collect(Collectors.joining(" OR "))). - append(')'); + Set from = new HashSet<>(); + List where = new ArrayList<>(); + + visitNode(root, from, where); + + // 3. generate the query string + String queryString = "SELECT COUNT(DISTINCT sv.any_id)" + + " FROM " + buildFrom(from) + + " WHERE " + buildWhere(where, root); + LOG.debug("Query: {}, parameters: {}", queryString, parameters); - return query.toString(); + // 4. populate the search query with parameter values + Query countQuery = entityManager().createNativeQuery(queryString); + fillWithParameters(countQuery, parameters); + + // 5. execute the query and return the result + return ((Number) countQuery.getSingleResult()).intValue(); } - protected void fillAttrQuery( - final StringBuilder query, - final PlainAttrValue attrValue, + protected void parseOrderByForPlainSchema( + final SearchSupport svs, + final OrderBySupport obs, + final OrderBySupport.Item item, + final OrderByClause clause, final PlainSchema schema, - final AttrCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { + final String fieldName) { + + // keep track of involvement of non-mandatory schemas in the order by clauses + obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition()); + + if (schema.isUniqueConstraint()) { + obs.views.add(svs.asSearchViewSupport().uniqueAttr()); - // This first branch is required for handling with not conditions given on multivalue fields (SYNCOPE-1419) - if (not && schema.isMultivalue() - && !(cond instanceof AnyCond) - && cond.getType() != AttrCond.Type.ISNULL && cond.getType() != AttrCond.Type.ISNOTNULL) { - - query.append("any_id NOT IN (SELECT DISTINCT any_id FROM "). - append((schema.isUniqueConstraint() - ? svs.asSearchViewSupport().uniqueAttr().name - : svs.asSearchViewSupport().attr().name)). - append(" WHERE schema_id='").append(schema.getKey()); - fillAttrQuery(query, attrValue, schema, cond, false, parameters, svs); - query.append(')'); + item.select = new StringBuilder(). + append(svs.asSearchViewSupport().uniqueAttr().alias).append('.'). + append(key(schema.getType())). + append(" AS ").append(fieldName).toString(); + item.where = new StringBuilder(). + append(svs.asSearchViewSupport().uniqueAttr().alias). + append(".schema_id='").append(fieldName).append("'").toString(); + item.orderBy = fieldName + ' ' + clause.getDirection().name(); } else { - // activate ignoreCase only for EQ and LIKE operators - boolean ignoreCase = AttrCond.Type.ILIKE == cond.getType() || AttrCond.Type.IEQ == cond.getType(); + obs.views.add(svs.asSearchViewSupport().attr()); - String column = (cond instanceof AnyCond) ? cond.getSchema() : key(schema.getType()); - if ((schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) && ignoreCase) { - column = "LOWER (" + column + ')'; - } - if (!(cond instanceof AnyCond)) { - column = "' AND " + column; - } + item.select = new StringBuilder(). + append(svs.asSearchViewSupport().attr().alias).append('.').append(key(schema.getType())). + append(" AS ").append(fieldName).toString(); + item.where = new StringBuilder(). + append(svs.asSearchViewSupport().attr().alias). + append(".schema_id='").append(fieldName).append("'").toString(); + item.orderBy = fieldName + ' ' + clause.getDirection().name(); + } + } - switch (cond.getType()) { - - case ISNULL: - query.append(column).append(not - ? " IS NOT NULL" - : " IS NULL"); - break; - - case ISNOTNULL: - query.append(column).append(not - ? " IS NULL" - : " IS NOT NULL"); - break; - - case ILIKE: - case LIKE: - if (schema.getType() == AttrSchemaType.String || schema.getType() == AttrSchemaType.Enum) { - query.append(column); - if (not) { - query.append(" NOT "); - } - query.append(" LIKE "); - if (ignoreCase) { - query.append("LOWER(?").append(setParameter(parameters, cond.getExpression())).append(')'); + protected void parseOrderByForField( + final SearchSupport svs, + final OrderBySupport.Item item, + final String fieldName, + final OrderByClause clause) { + + item.select = svs.field().alias + '.' + fieldName; + item.where = StringUtils.EMPTY; + item.orderBy = svs.field().alias + '.' + fieldName + ' ' + clause.getDirection().name(); + } + + protected void parseOrderByForCustom( + final SearchSupport svs, + final OrderByClause clause, + final OrderBySupport.Item item, + final OrderBySupport obs) { + + // do nothing by default, meant for subclasses + } + + protected OrderBySupport parseOrderBy( + final SearchSupport svs, + final List orderBy) { + + AnyUtils anyUtils = anyUtilsFactory.getInstance(svs.anyTypeKind); + + OrderBySupport obs = new OrderBySupport(); + + Set orderByUniquePlainSchemas = new HashSet<>(); + Set orderByNonUniquePlainSchemas = new HashSet<>(); + orderBy.forEach(clause -> { + OrderBySupport.Item item = new OrderBySupport.Item(); + + parseOrderByForCustom(svs, clause, item, obs); + + if (item.isEmpty()) { + if (anyUtils.getField(clause.getField()) == null) { + PlainSchema schema = plainSchemaDAO.find(clause.getField()); + if (schema != null) { + if (schema.isUniqueConstraint()) { + orderByUniquePlainSchemas.add(schema.getKey()); } else { - query.append('?').append(setParameter(parameters, cond.getExpression())); - } - // workaround for Oracle DB adding explicit escaping string, to search - // for literal _ (underscore) (SYNCOPE-1779) - if (isOracle()) { - query.append(" ESCAPE '\\' "); + orderByNonUniquePlainSchemas.add(schema.getKey()); } - } else { - if (!(cond instanceof AnyCond)) { - query.append("' AND"); + if (orderByUniquePlainSchemas.size() > 1 || orderByNonUniquePlainSchemas.size() > 1) { + throw syncopeClientException("Order by more than one attribute is not allowed; " + + "remove one from " + (orderByUniquePlainSchemas.size() > 1 + ? orderByUniquePlainSchemas : orderByNonUniquePlainSchemas)).get(); } - query.append(" 1=2"); - LOG.error("LIKE is only compatible with string or enum schemas"); - } - break; - - case IEQ: - case EQ: - query.append(column); - if (not) { - query.append("<>"); - } else { - query.append('='); - } - if ((schema.getType() == AttrSchemaType.String - || schema.getType() == AttrSchemaType.Enum) && ignoreCase) { - query.append("LOWER(?").append(setParameter(parameters, attrValue.getValue())).append(')'); - } else { - query.append('?').append(setParameter(parameters, attrValue.getValue())); - } - break; - - case GE: - query.append(column); - if (not) { - query.append('<'); - } else { - query.append(">="); + parseOrderByForPlainSchema(svs, obs, item, clause, schema, clause.getField()); } - query.append('?').append(setParameter(parameters, attrValue.getValue())); - break; + } else { + // Manage difference among external key attribute and internal JPA @Id + String fieldName = "key".equals(clause.getField()) ? "id" : clause.getField(); - case GT: - query.append(column); - if (not) { - query.append("<="); - } else { - query.append('>'); + // Adjust field name to column name + if (ArrayUtils.contains(RELATIONSHIP_FIELDS, fieldName)) { + fieldName += "_id"; } - query.append('?').append(setParameter(parameters, attrValue.getValue())); - break; - case LE: - query.append(column); - if (not) { - query.append('>'); - } else { - query.append("<="); - } - query.append('?').append(setParameter(parameters, attrValue.getValue())); - break; + obs.views.add(svs.field()); - case LT: - query.append(column); - if (not) { - query.append(">="); - } else { - query.append('<'); - } - query.append('?').append(setParameter(parameters, attrValue.getValue())); - break; + parseOrderByForField(svs, item, fieldName, clause); + } + } - default: + if (item.isEmpty()) { + LOG.warn("Cannot build any valid clause from {}", clause); + } else { + obs.items.add(item); } - } + }); + + return obs; } - protected String getQuery( - final AttrCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { + @Override + @SuppressWarnings("unchecked") + protected > List doSearch( + final Realm base, + final boolean recursive, + final Set adminRealms, + final SearchCond cond, + final int page, + final int itemsPerPage, + final List orderBy, + final AnyTypeKind kind) { - Pair checked = check(cond, svs.anyTypeKind); + List parameters = new ArrayList<>(); - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "); - switch (cond.getType()) { - case ISNOTNULL: - query.append(checked.getLeft().isUniqueConstraint() - ? svs.asSearchViewSupport().uniqueAttr().name - : svs.asSearchViewSupport().attr().name). - append(" WHERE schema_id=").append("'").append(checked.getLeft().getKey()).append("'"); - break; + SearchSupport svs = new SearchViewSupport(kind); - case ISNULL: - query.append(svs.field().name). - append(" WHERE any_id NOT IN "). - append('('). - append("SELECT DISTINCT any_id FROM "). - append(checked.getLeft().isUniqueConstraint() - ? svs.asSearchViewSupport().uniqueAttr().name - : svs.asSearchViewSupport().attr().name). - append(" WHERE schema_id=").append("'").append(checked.getLeft().getKey()).append("'"). - append(')'); - break; + Triple, Set> filter = + getAdminRealmsFilter(base, recursive, adminRealms, svs, parameters); - default: - if (not && !(cond instanceof AnyCond) && checked.getLeft().isMultivalue()) { - query.append(svs.field().name).append(" WHERE "); - } else { - query.append((checked.getLeft().isUniqueConstraint() - ? svs.asSearchViewSupport().uniqueAttr().name - : svs.asSearchViewSupport().attr().name)). - append(" WHERE schema_id='").append(checked.getLeft().getKey()); - } - fillAttrQuery(query, checked.getRight(), checked.getLeft(), cond, not, parameters, svs); + // 1. get query string from search condition + Optional>> optionalQueryInfo = getQuery( + buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind), parameters, svs); + if (optionalQueryInfo.isEmpty()) { + LOG.error("Invalid search condition: {}", cond); + return List.of(); } + Pair> queryInfo = optionalQueryInfo.get(); - return query.toString(); - } + // 2. take realms into account + AnySearchNode root; + if (queryInfo.getLeft().getType() == AnySearchNode.Type.AND) { + root = queryInfo.getLeft(); + } else { + root = new AnySearchNode(AnySearchNode.Type.AND); + root.add(queryInfo.getLeft()); + } + root.add(filter.getLeft()); - protected String getQuery( - final AnyCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { + Set from = new HashSet<>(); + List where = new ArrayList<>(); - if (JAXRSService.PARAM_REALM.equals(cond.getSchema()) - && !SyncopeConstants.UUID_PATTERN.matcher(cond.getExpression()).matches()) { + visitNode(root, from, where); - Realm realm = realmDAO.findByFullPath(cond.getExpression()); - if (realm == null) { - throw new IllegalArgumentException("Invalid Realm full path: " + cond.getExpression()); - } - cond.setExpression(realm.getKey()); + // 3. take ordering into account + OrderBySupport obs = parseOrderBy(svs, orderBy); + + // 4. generate the query string + StringBuilder queryString = new StringBuilder("SELECT DISTINCT sv.any_id"); + obs.items.forEach(item -> queryString.append(',').append(item.select)); + + from.addAll(obs.views); + queryString.append(" FROM ").append(buildFrom(from)); + + queryString.append(" WHERE ").append(buildWhere(where, root)); + + if (!obs.items.isEmpty()) { + queryString.append(" ORDER BY "). + append(obs.items.stream().map(item -> item.orderBy).collect(Collectors.joining(","))); } - Triple checked = check(cond, svs.anyTypeKind); + LOG.debug("Query: {}, parameters: {}", queryString, parameters); + + // 5. prepare the search query + Query query = entityManager().createNativeQuery(queryString.toString()); - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE "); + // 6. page starts from 1, while setFirtResult() starts from 0 + query.setFirstResult(itemsPerPage * (page <= 0 ? 0 : page - 1)); + + if (itemsPerPage >= 0) { + query.setMaxResults(itemsPerPage); + } - fillAttrQuery(query, checked.getMiddle(), checked.getLeft(), checked.getRight(), not, parameters, svs); + // 7. populate the search query with parameter values + fillWithParameters(query, parameters); - return query.toString(); + // 8. prepare the result (avoiding duplicates) + return buildResult(query.getResultList(), kind); } } From a8f5e6e5b07dad12f77e40310ac8c9cabf67cc0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Chicchiricc=C3=B2?= Date: Mon, 20 Jan 2025 12:11:19 +0100 Subject: [PATCH 4/9] All but pgjsonb should be working now --- .../jpa/dao/AbstractAnySearchDAO.java | 13 +- .../persistence/jpa/dao/JPAAnySearchDAO.java | 234 ++++++++++-------- .../persistence/jpa/inner/AnySearchTest.java | 55 ++++ 3 files changed, 187 insertions(+), 115 deletions(-) diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractAnySearchDAO.java index 0dac17d18f3..124a4c419de 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractAnySearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractAnySearchDAO.java @@ -199,10 +199,8 @@ protected abstract > List doSearch( protected Pair check(final AttrCond cond, final AnyTypeKind kind) { AnyUtils anyUtils = anyUtilsFactory.getInstance(kind); - PlainSchema schema = plainSchemaDAO.find(cond.getSchema()); - if (schema == null) { - throw new IllegalArgumentException("Invalid schema " + cond.getSchema()); - } + PlainSchema schema = Optional.ofNullable(plainSchemaDAO.find(cond.getSchema())). + orElseThrow(() -> new IllegalArgumentException("Invalid schema " + cond.getSchema())); PlainAttrValue attrValue = schema.isUniqueConstraint() ? anyUtils.newPlainAttrUniqueValue() @@ -229,10 +227,9 @@ protected Triple check(final AnyCond cond, AnyUtils anyUtils = anyUtilsFactory.getInstance(kind); - Field anyField = anyUtils.getField(computed.getSchema()); - if (anyField == null) { - throw new IllegalArgumentException("Invalid schema " + computed.getSchema()); - } + Field anyField = Optional.ofNullable(anyUtils.getField(computed.getSchema())). + orElseThrow(() -> new IllegalArgumentException("Invalid schema " + computed.getSchema())); + // Keeps track of difference between entity's getKey() and JPA @Id fields if ("key".equals(computed.getSchema())) { computed.setSchema("id"); diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnySearchDAO.java index 43ad473bf28..a24bc2c5764 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnySearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnySearchDAO.java @@ -19,8 +19,10 @@ package org.apache.syncope.core.persistence.jpa.dao; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Supplier; @@ -124,25 +126,6 @@ protected static String key(final AttrSchemaType schemaType) { return key; } - protected static void visitNode( - final AnySearchNode node, - final Set from, - final List where) { - - node.asLeaf().ifPresentOrElse( - leaf -> { - from.add(leaf.getFrom()); - where.add(leaf.getClause()); - }, - () -> { - List nodeWhere = new ArrayList<>(); - node.getChildren().forEach(child -> visitNode(child, from, nodeWhere)); - where.add(nodeWhere.stream(). - map(w -> "(" + w + ")"). - collect(Collectors.joining(" " + node.getType().name() + " "))); - }); - } - protected static String buildFrom(final Set from) { String fromString; if (from.size() == 1) { @@ -210,13 +193,12 @@ protected Optional getQueryForCustomConds( return Optional.empty(); } - protected Optional>> getQuery( + protected Optional getQuery( final SearchCond cond, final List parameters, final SearchSupport svs) { boolean not = cond.getType() == SearchCond.Type.NOT_LEAF; Optional node = Optional.empty(); - Set involvedPlainAttrs = new HashSet<>(); switch (cond.getType()) { case LEAF: @@ -279,14 +261,8 @@ protected Optional>> getQuery( map(anyCond -> getQuery(anyCond, not, parameters, svs)). or(() -> cond.getLeaf(AttrCond.class). map(attrCond -> { - try { - Pair checked = check(attrCond, svs.anyTypeKind); - involvedPlainAttrs.add(checked.getLeft().getKey()); - return getQuery(attrCond, not, checked, parameters, svs); - } catch (IllegalArgumentException e) { - // ignore - return null; - } + Pair checked = check(attrCond, svs.anyTypeKind); + return getQuery(attrCond, not, checked, parameters, svs); })); } @@ -299,15 +275,9 @@ protected Optional>> getQuery( case AND: AnySearchNode andNode = new AnySearchNode(AnySearchNode.Type.AND); - getQuery(cond.getLeft(), parameters, svs).ifPresent(left -> { - andNode.add(left.getLeft()); - involvedPlainAttrs.addAll(left.getRight()); - }); + getQuery(cond.getLeft(), parameters, svs).ifPresent(andNode::add); - getQuery(cond.getRight(), parameters, svs).ifPresent(right -> { - andNode.add(right.getLeft()); - involvedPlainAttrs.addAll(right.getRight()); - }); + getQuery(cond.getRight(), parameters, svs).ifPresent(andNode::add); if (!andNode.getChildren().isEmpty()) { node = Optional.of(andNode); @@ -317,15 +287,9 @@ protected Optional>> getQuery( case OR: AnySearchNode orNode = new AnySearchNode(AnySearchNode.Type.OR); - getQuery(cond.getLeft(), parameters, svs).ifPresent(left -> { - orNode.add(left.getLeft()); - involvedPlainAttrs.addAll(left.getRight()); - }); + getQuery(cond.getLeft(), parameters, svs).ifPresent(orNode::add); - getQuery(cond.getRight(), parameters, svs).ifPresent(right -> { - orNode.add(right.getLeft()); - involvedPlainAttrs.addAll(right.getRight()); - }); + getQuery(cond.getRight(), parameters, svs).ifPresent(orNode::add); if (!orNode.getChildren().isEmpty()) { node = Optional.of(orNode); @@ -335,7 +299,7 @@ protected Optional>> getQuery( default: } - return node.map(n -> Pair.of(n, involvedPlainAttrs)); + return node; } protected AnySearchNode getQuery( @@ -888,6 +852,62 @@ protected Triple, Set> getAdminRealmsFil return Triple.of(buildAdminRealmsFilter(realmKeys, svs, parameters), dynRealmKeys, groupOwners); } + protected void visitNode( + final AnySearchNode node, + final Map counters, + final Set from, + final List where) { + + node.asLeaf().ifPresentOrElse( + leaf -> { + from.add(leaf.getFrom()); + + if (counters.computeIfAbsent(leaf.getFrom(), view -> false) && !leaf.getClause().contains(" IN ")) { + where.add("sv.any_id IN (" + + "SELECT any_id FROM " + leaf.getFrom().name + + " WHERE " + leaf.getClause().replace(leaf.getFrom().alias + ".", "") + + ")"); + } else { + counters.put(leaf.getFrom(), true); + where.add(leaf.getClause()); + } + }, + () -> { + List nodeWhere = new ArrayList<>(); + node.getChildren().forEach(child -> visitNode(child, counters, from, nodeWhere)); + where.add(nodeWhere.stream(). + map(w -> "(" + w + ")"). + collect(Collectors.joining(" " + node.getType().name() + " "))); + }); + } + + protected String buildCountQuery( + final AnySearchNode queryInfoNode, + final AnySearchNode.Leaf filterNode, + final List parameters) { + + AnySearchNode root; + if (queryInfoNode.getType() == AnySearchNode.Type.AND) { + root = queryInfoNode; + } else { + root = new AnySearchNode(AnySearchNode.Type.AND); + root.add(queryInfoNode); + } + root.add(filterNode); + + Set from = new HashSet<>(); + List where = new ArrayList<>(); + Map counters = new HashMap<>(); + visitNode(root, counters, from, where); + + String queryString = "SELECT COUNT(DISTINCT sv.any_id)" + + " FROM " + buildFrom(from) + + " WHERE " + buildWhere(where, root); + LOG.debug("Query: {}, parameters: {}", queryString, parameters); + + return queryString; + } + @Override protected int doCount( final Realm base, @@ -900,34 +920,21 @@ protected int doCount( SearchSupport svs = new SearchViewSupport(kind); + // 1. get admin realms filter Triple, Set> filter = getAdminRealmsFilter(base, recursive, adminRealms, svs, parameters); - // 1. get query string from search condition - Pair> queryInfo = getQuery( - buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind), parameters, svs). - orElseThrow(syncopeClientException("Invalid search condition")); - - // 2. take realms into account - AnySearchNode root; - if (queryInfo.getLeft().getType() == AnySearchNode.Type.AND) { - root = queryInfo.getLeft(); - } else { - root = new AnySearchNode(AnySearchNode.Type.AND); - root.add(queryInfo.getLeft()); + // 2. transform search condition + Optional optionalQueryInfoNode = getQuery( + buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind), parameters, svs); + if (optionalQueryInfoNode.isEmpty()) { + LOG.error("Invalid search condition: {}", cond); + return 0; } - root.add(filter.getLeft()); - - Set from = new HashSet<>(); - List where = new ArrayList<>(); - - visitNode(root, from, where); + AnySearchNode queryInfoNode = optionalQueryInfoNode.get(); // 3. generate the query string - String queryString = "SELECT COUNT(DISTINCT sv.any_id)" - + " FROM " + buildFrom(from) - + " WHERE " + buildWhere(where, root); - LOG.debug("Query: {}, parameters: {}", queryString, parameters); + String queryString = buildCountQuery(queryInfoNode, filter.getLeft(), parameters); // 4. populate the search query with parameter values Query countQuery = entityManager().createNativeQuery(queryString); @@ -1048,48 +1055,26 @@ protected OrderBySupport parseOrderBy( return obs; } - @Override - @SuppressWarnings("unchecked") - protected > List doSearch( - final Realm base, - final boolean recursive, - final Set adminRealms, - final SearchCond cond, - final int page, - final int itemsPerPage, - final List orderBy, - final AnyTypeKind kind) { - - List parameters = new ArrayList<>(); - - SearchSupport svs = new SearchViewSupport(kind); - - Triple, Set> filter = - getAdminRealmsFilter(base, recursive, adminRealms, svs, parameters); - - // 1. get query string from search condition - Optional>> optionalQueryInfo = getQuery( - buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind), parameters, svs); - if (optionalQueryInfo.isEmpty()) { - LOG.error("Invalid search condition: {}", cond); - return List.of(); - } - Pair> queryInfo = optionalQueryInfo.get(); + protected String buildSearchQuery( + final AnySearchNode queryInfoNode, + final AnySearchNode.Leaf filterNode, + final List parameters, + final SearchSupport svs, + final List orderBy) { - // 2. take realms into account AnySearchNode root; - if (queryInfo.getLeft().getType() == AnySearchNode.Type.AND) { - root = queryInfo.getLeft(); + if (queryInfoNode.getType() == AnySearchNode.Type.AND) { + root = queryInfoNode; } else { root = new AnySearchNode(AnySearchNode.Type.AND); - root.add(queryInfo.getLeft()); + root.add(queryInfoNode); } - root.add(filter.getLeft()); + root.add(filterNode); Set from = new HashSet<>(); List where = new ArrayList<>(); - - visitNode(root, from, where); + Map counters = new HashMap<>(); + visitNode(root, counters, from, where); // 3. take ordering into account OrderBySupport obs = parseOrderBy(svs, orderBy); @@ -1110,20 +1095,55 @@ protected > List doSearch( LOG.debug("Query: {}, parameters: {}", queryString, parameters); - // 5. prepare the search query - Query query = entityManager().createNativeQuery(queryString.toString()); + return queryString.toString(); + } + + @Override + @SuppressWarnings("unchecked") + protected > List doSearch( + final Realm base, + final boolean recursive, + final Set adminRealms, + final SearchCond cond, + final int page, + final int itemsPerPage, + final List orderBy, + final AnyTypeKind kind) { + + List parameters = new ArrayList<>(); + + SearchSupport svs = new SearchViewSupport(kind); + + // 1. get admin realms filter + Triple, Set> filter = + getAdminRealmsFilter(base, recursive, adminRealms, svs, parameters); + + // 2. transform search condition + Optional optionalQueryInfoNode = getQuery( + buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind), parameters, svs); + if (optionalQueryInfoNode.isEmpty()) { + LOG.error("Invalid search condition: {}", cond); + return List.of(); + } + AnySearchNode queryInfoNode = optionalQueryInfoNode.get(); + + // 3. generate the query string + String queryString = buildSearchQuery(queryInfoNode, filter.getLeft(), parameters, svs, orderBy); + + // 4. prepare the search query + Query query = entityManager().createNativeQuery(queryString); - // 6. page starts from 1, while setFirtResult() starts from 0 + // page starts from 1, while setFirtResult() starts from 0 query.setFirstResult(itemsPerPage * (page <= 0 ? 0 : page - 1)); if (itemsPerPage >= 0) { query.setMaxResults(itemsPerPage); } - // 7. populate the search query with parameter values + // 5. populate the search query with parameter values fillWithParameters(query, parameters); - // 8. prepare the result (avoiding duplicates) + // 6. prepare the result (avoiding duplicates) return buildResult(query.getResultList(), kind); } } diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java index d514181735b..f9fb20d0fec 100644 --- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java +++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java @@ -111,6 +111,61 @@ public void adjustLoginDateForLocalSystem() throws ParseException { userDAO.save(rossini); } + @Test + public void searchTwoPlainSchemas() { + AttrCond firstnameCond = new AttrCond(AttrCond.Type.EQ); + firstnameCond.setSchema("firstname"); + firstnameCond.setExpression("Gioacchino"); + + AttrCond surnameCond = new AttrCond(AttrCond.Type.EQ); + surnameCond.setSchema("surname"); + surnameCond.setExpression("Rossini"); + + SearchCond cond = SearchCond.getAnd(SearchCond.getLeaf(firstnameCond), SearchCond.getLeaf(surnameCond)); + assertTrue(cond.isValid()); + + List users = searchDAO.search(cond, AnyTypeKind.USER); + assertNotNull(users); + assertEquals(1, users.size()); + + surnameCond = new AttrCond(AttrCond.Type.EQ); + surnameCond.setSchema("surname"); + surnameCond.setExpression("Verdi"); + + cond = SearchCond.getAnd(SearchCond.getLeaf(firstnameCond), SearchCond.getNotLeaf(surnameCond)); + assertTrue(cond.isValid()); + + users = searchDAO.search(cond, AnyTypeKind.USER); + assertNotNull(users); + assertEquals(1, users.size()); + + AttrCond fullnameCond = new AttrCond(AttrCond.Type.EQ); + fullnameCond.setSchema("fullname"); + fullnameCond.setExpression("Vincenzo Bellini"); + + AttrCond userIdCond = new AttrCond(AttrCond.Type.EQ); + userIdCond.setSchema("userId"); + userIdCond.setExpression("bellini@apache.org"); + + cond = SearchCond.getAnd(SearchCond.getLeaf(fullnameCond), SearchCond.getLeaf(userIdCond)); + assertTrue(cond.isValid()); + + users = searchDAO.search(cond, AnyTypeKind.USER); + assertNotNull(users); + assertEquals(1, users.size()); + + userIdCond = new AttrCond(AttrCond.Type.EQ); + userIdCond.setSchema("userId"); + userIdCond.setExpression("rossini@apache.org"); + + cond = SearchCond.getAnd(SearchCond.getLeaf(fullnameCond), SearchCond.getNotLeaf(userIdCond)); + assertTrue(cond.isValid()); + + users = searchDAO.search(cond, AnyTypeKind.USER); + assertNotNull(users); + assertEquals(1, users.size()); + } + @Test public void searchWithLikeCondition() { AttrCond fullnameLeafCond = new AttrCond(AttrCond.Type.LIKE); From 4c64180a484363a158ecc64c63e11e3df6ad69fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Chicchiricc=C3=B2?= Date: Mon, 20 Jan 2025 12:18:53 +0100 Subject: [PATCH 5/9] Restoring previous test cases for SYNCOPE-929 --- .../syncope/core/persistence/jpa/inner/AnySearchTest.java | 6 +++--- .../test/java/org/apache/syncope/fit/core/SearchITCase.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java index f9fb20d0fec..5218d666972 100644 --- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java +++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/AnySearchTest.java @@ -832,9 +832,9 @@ public void issueSYNCOPE929() { SearchCond orCond = SearchCond.getOr(SearchCond.getLeaf(rossiniCond), SearchCond.getLeaf(genderCond)); - AnyCond belliniCond = new AnyCond(AttrCond.Type.EQ); - belliniCond.setSchema("username"); - belliniCond.setExpression("bellini"); + AttrCond belliniCond = new AttrCond(AttrCond.Type.EQ); + belliniCond.setSchema("surname"); + belliniCond.setExpression("Bellini"); SearchCond searchCond = SearchCond.getAnd(orCond, SearchCond.getLeaf(belliniCond)); diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java index 0369060318f..93919052c5c 100644 --- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java +++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java @@ -736,7 +736,7 @@ public void issueSYNCOPE768() { public void issueSYNCOPE929() { PagedResult matchingUsers = USER_SERVICE.search( new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM). - fiql("(surname==Rossini,gender==M);username==bellini").build()); + fiql("(surname==Rossini,gender==M);surname==Bellini").build()); assertNotNull(matchingUsers); From 701ab4a5342adf12ebb155705e7298d5ea5747d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Chicchiricc=C3=B2?= Date: Mon, 20 Jan 2025 15:35:37 +0100 Subject: [PATCH 6/9] All working but pgjsonb unit tests slow (?) --- .../jpa/dao/PGJPAJSONAnySearchDAO.java | 57 ++++ .../persistence/jpa/dao/JPAAnySearchDAO.java | 253 ++++++++++-------- 2 files changed, 199 insertions(+), 111 deletions(-) diff --git a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAJSONAnySearchDAO.java b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAJSONAnySearchDAO.java index 47896db5eba..7a5319edf0d 100644 --- a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAJSONAnySearchDAO.java +++ b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAJSONAnySearchDAO.java @@ -18,9 +18,14 @@ */ package org.apache.syncope.core.persistence.jpa.dao; +import static org.apache.syncope.core.persistence.jpa.dao.AbstractDAO.LOG; + import java.time.format.DateTimeFormatter; +import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.syncope.common.lib.types.AttrSchemaType; @@ -79,6 +84,16 @@ public PGJPAJSONAnySearchDAO( validator); } + @Override + protected SearchSupport.SearchView defaultSV(final SearchSupport svs) { + return svs.table(); + } + + @Override + protected String anyId(final SearchSupport svs) { + return defaultSV(svs).alias + ".id"; + } + @Override protected void parseOrderByForPlainSchema( final SearchSupport svs, @@ -265,4 +280,46 @@ protected AnySearchNode getQuery( not); } } + + @Override + protected void visitNode( + final AnySearchNode node, + final Map counters, + final Set from, + final List where, + final SearchSupport svs) { + + counters.clear(); + super.visitNode(node, counters, from, where, svs); + } + + @Override + protected String buildFrom( + final Set from, + final Set plainSchemas, + final OrderBySupport obs) { + + StringBuilder prefix = new StringBuilder(super.buildFrom(from, plainSchemas, obs)); + + Set schemas = new HashSet<>(plainSchemas); + + if (obs != null) { + obs.items.forEach(item -> { + String schema = StringUtils.substringBefore(item.orderBy, ' '); + if (StringUtils.isNotBlank(schema)) { + schemas.add(schema); + } + }); + } + + schemas.forEach(schema -> Optional.ofNullable(plainSchemaDAO.find(schema)).ifPresentOrElse( + pschema -> prefix.append(','). + append("jsonb_path_query_array(plainattrs, '$[*] ? (@.schema==\""). + append(schema).append("\")."). + append("\"").append(pschema.isUniqueConstraint() ? "uniqueValue" : "values").append("\"')"). + append(" AS ").append(schema), + () -> LOG.warn("Ignoring invalid schema '{}'", schema))); + + return prefix.toString(); + } } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnySearchDAO.java index a24bc2c5764..4612016fb99 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnySearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnySearchDAO.java @@ -126,32 +126,6 @@ protected static String key(final AttrSchemaType schemaType) { return key; } - protected static String buildFrom(final Set from) { - String fromString; - if (from.size() == 1) { - SearchSupport.SearchView sv = from.iterator().next(); - fromString = sv.name + " " + sv.alias; - } else { - List joins = new ArrayList<>(from); - StringBuilder join = new StringBuilder(joins.get(0).name + " " + joins.get(0).alias); - for (int i = 1; i < joins.size(); i++) { - SearchSupport.SearchView sv = joins.get(i); - join.append(" LEFT JOIN "). - append(sv.name).append(" ").append(sv.alias). - append(" ON "). - append(joins.get(0).alias).append(".any_id=").append(sv.alias).append(".any_id"); - } - fromString = join.toString(); - } - return fromString; - } - - protected static String buildWhere(final List where, final AnySearchNode root) { - return where.stream(). - map(w -> "(" + w + ")"). - collect(Collectors.joining(" " + root.getType().name() + " ")); - } - protected static Supplier syncopeClientException(final String message) { return () -> { SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidSearchParameters); @@ -183,6 +157,14 @@ public JPAAnySearchDAO( validator); } + protected SearchSupport.SearchView defaultSV(final SearchSupport svs) { + return svs.field(); + } + + protected String anyId(final SearchSupport svs) { + return defaultSV(svs).alias + ".any_id"; + } + protected Optional getQueryForCustomConds( final SearchCond cond, final List parameters, @@ -193,12 +175,13 @@ protected Optional getQueryForCustomConds( return Optional.empty(); } - protected Optional getQuery( + protected Optional>> getQuery( final SearchCond cond, final List parameters, final SearchSupport svs) { boolean not = cond.getType() == SearchCond.Type.NOT_LEAF; Optional node = Optional.empty(); + Set plainSchemas = new HashSet<>(); switch (cond.getType()) { case LEAF: @@ -262,6 +245,7 @@ protected Optional getQuery( or(() -> cond.getLeaf(AttrCond.class). map(attrCond -> { Pair checked = check(attrCond, svs.anyTypeKind); + plainSchemas.add(checked.getLeft().getKey()); return getQuery(attrCond, not, checked, parameters, svs); })); } @@ -275,9 +259,15 @@ protected Optional getQuery( case AND: AnySearchNode andNode = new AnySearchNode(AnySearchNode.Type.AND); - getQuery(cond.getLeft(), parameters, svs).ifPresent(andNode::add); + getQuery(cond.getLeft(), parameters, svs).ifPresent(left -> { + andNode.add(left.getLeft()); + plainSchemas.addAll(left.getRight()); + }); - getQuery(cond.getRight(), parameters, svs).ifPresent(andNode::add); + getQuery(cond.getRight(), parameters, svs).ifPresent(right -> { + andNode.add(right.getLeft()); + plainSchemas.addAll(right.getRight()); + }); if (!andNode.getChildren().isEmpty()) { node = Optional.of(andNode); @@ -287,9 +277,15 @@ protected Optional getQuery( case OR: AnySearchNode orNode = new AnySearchNode(AnySearchNode.Type.OR); - getQuery(cond.getLeft(), parameters, svs).ifPresent(orNode::add); + getQuery(cond.getLeft(), parameters, svs).ifPresent(left -> { + orNode.add(left.getLeft()); + plainSchemas.addAll(left.getRight()); + }); - getQuery(cond.getRight(), parameters, svs).ifPresent(orNode::add); + getQuery(cond.getRight(), parameters, svs).ifPresent(right -> { + orNode.add(right.getLeft()); + plainSchemas.addAll(right.getRight()); + }); if (!orNode.getChildren().isEmpty()) { node = Optional.of(orNode); @@ -299,7 +295,7 @@ protected Optional getQuery( default: } - return node; + return node.map(n -> Pair.of(n, plainSchemas)); } protected AnySearchNode getQuery( @@ -316,7 +312,7 @@ protected AnySearchNode getQuery( } clause.append('?').append(setParameter(parameters, cond.getAnyTypeKey())); - return new AnySearchNode.Leaf(svs.field(), clause.toString()); + return new AnySearchNode.Leaf(defaultSV(svs), clause.toString()); } protected AnySearchNode getQuery( @@ -327,9 +323,9 @@ protected AnySearchNode getQuery( StringBuilder clause = new StringBuilder(); if (not) { - clause.append("sv.any_id NOT IN ("); + clause.append(anyId(svs)).append(" NOT IN ("); } else { - clause.append("sv.any_id IN ("); + clause.append(anyId(svs)).append(" IN ("); } clause.append("SELECT any_id FROM "). append(svs.auxClass().name). @@ -337,7 +333,7 @@ protected AnySearchNode getQuery( append(setParameter(parameters, cond.getAuxClass())). append(')'); - return new AnySearchNode.Leaf(svs.field(), clause.toString()); + return new AnySearchNode.Leaf(defaultSV(svs), clause.toString()); } protected AnySearchNode getQuery( @@ -348,9 +344,9 @@ protected AnySearchNode getQuery( StringBuilder clause = new StringBuilder(); if (not) { - clause.append("sv.any_id NOT IN ("); + clause.append(anyId(svs)).append(" NOT IN ("); } else { - clause.append("sv.any_id IN ("); + clause.append(anyId(svs)).append(" IN ("); } clause.append("SELECT any_id ").append("FROM "). append(svs.relationship().name). @@ -360,7 +356,7 @@ protected AnySearchNode getQuery( append(" WHERE type=?").append(setParameter(parameters, cond.getRelationshipTypeKey())). append(')'); - return new AnySearchNode.Leaf(svs.field(), clause.toString()); + return new AnySearchNode.Leaf(defaultSV(svs), clause.toString()); } protected AnySearchNode getQuery( @@ -373,9 +369,9 @@ protected AnySearchNode getQuery( StringBuilder clause = new StringBuilder(); if (not) { - clause.append("sv.any_id NOT IN ("); + clause.append(anyId(svs)).append(" NOT IN ("); } else { - clause.append("sv.any_id IN ("); + clause.append(anyId(svs)).append(" IN ("); } clause.append("SELECT DISTINCT any_id FROM "). append(svs.relationship().name).append(" WHERE "). @@ -384,7 +380,7 @@ protected AnySearchNode getQuery( collect(Collectors.joining(" OR "))). append(')'); - return new AnySearchNode.Leaf(svs.field(), clause.toString()); + return new AnySearchNode.Leaf(defaultSV(svs), clause.toString()); } protected AnySearchNode getQuery( @@ -402,9 +398,9 @@ protected AnySearchNode getQuery( StringBuilder clause = new StringBuilder("("); if (not) { - clause.append("sv.any_id NOT IN ("); + clause.append(anyId(svs)).append(" NOT IN ("); } else { - clause.append("sv.any_id IN ("); + clause.append(anyId(svs)).append(" IN ("); } clause.append("SELECT DISTINCT any_id FROM "). append(svs.membership().name).append(" WHERE "). @@ -412,9 +408,9 @@ protected AnySearchNode getQuery( append(") "); if (not) { - clause.append("AND sv.any_id NOT IN ("); + clause.append("AND ").append(anyId(svs)).append(" NOT IN ("); } else { - clause.append("OR sv.any_id IN ("); + clause.append("OR ").append(anyId(svs)).append(" IN ("); } clause.append("SELECT DISTINCT any_id FROM "). @@ -422,7 +418,7 @@ protected AnySearchNode getQuery( append(subwhere). append("))"); - return new AnySearchNode.Leaf(svs.field(), clause.toString()); + return new AnySearchNode.Leaf(defaultSV(svs), clause.toString()); } protected AnySearchNode getQuery( @@ -434,9 +430,9 @@ protected AnySearchNode getQuery( StringBuilder clause = new StringBuilder("("); if (not) { - clause.append("sv.any_id NOT IN ("); + clause.append(anyId(svs)).append(" NOT IN ("); } else { - clause.append("sv.any_id IN ("); + clause.append(anyId(svs)).append(" IN ("); } clause.append("SELECT DISTINCT any_id FROM "). @@ -445,9 +441,9 @@ protected AnySearchNode getQuery( append(") "); if (not) { - clause.append("AND sv.any_id NOT IN ("); + clause.append("AND ").append(anyId(svs)).append(" NOT IN ("); } else { - clause.append("OR sv.any_id IN ("); + clause.append("OR ").append(anyId(svs)).append(" IN ("); } clause.append("SELECT DISTINCT any_id FROM "). @@ -455,7 +451,7 @@ protected AnySearchNode getQuery( append("role_id=?").append(setParameter(parameters, cond.getRole())). append("))"); - return new AnySearchNode.Leaf(svs.field(), clause.toString()); + return new AnySearchNode.Leaf(defaultSV(svs), clause.toString()); } protected AnySearchNode getQuery( @@ -467,9 +463,9 @@ protected AnySearchNode getQuery( StringBuilder clause = new StringBuilder("("); if (not) { - clause.append("sv.any_id NOT IN ("); + clause.append(anyId(svs)).append(" NOT IN ("); } else { - clause.append("sv.any_id IN ("); + clause.append(anyId(svs)).append(" IN ("); } clause.append("SELECT DISTINCT any_id FROM "). @@ -478,9 +474,9 @@ protected AnySearchNode getQuery( append(") "); if (not) { - clause.append("AND sv.any_id NOT IN ("); + clause.append("AND ").append(anyId(svs)).append(" NOT IN ("); } else { - clause.append("OR sv.any_id IN ("); + clause.append("OR ").append(anyId(svs)).append(" IN ("); } clause.append("SELECT DISTINCT any_id FROM "). @@ -488,7 +484,7 @@ protected AnySearchNode getQuery( append("privilege_id=?").append(setParameter(parameters, cond.getPrivilege())). append("))"); - return new AnySearchNode.Leaf(svs.field(), clause.toString()); + return new AnySearchNode.Leaf(defaultSV(svs), clause.toString()); } protected AnySearchNode getQuery( @@ -500,9 +496,9 @@ protected AnySearchNode getQuery( StringBuilder clause = new StringBuilder("("); if (not) { - clause.append("sv.any_id NOT IN ("); + clause.append(anyId(svs)).append(" NOT IN ("); } else { - clause.append("sv.any_id IN ("); + clause.append(anyId(svs)).append(" IN ("); } clause.append("SELECT DISTINCT any_id FROM "). @@ -510,7 +506,7 @@ protected AnySearchNode getQuery( append("dynRealm_id=?").append(setParameter(parameters, cond.getDynRealm())). append("))"); - return new AnySearchNode.Leaf(svs.field(), clause.toString()); + return new AnySearchNode.Leaf(defaultSV(svs), clause.toString()); } protected AnySearchNode getQuery( @@ -522,9 +518,9 @@ protected AnySearchNode getQuery( StringBuilder clause = new StringBuilder(); if (not) { - clause.append("sv.any_id NOT IN ("); + clause.append(anyId(svs)).append(" NOT IN ("); } else { - clause.append("sv.any_id IN ("); + clause.append(anyId(svs)).append(" IN ("); } clause.append("SELECT DISTINCT any_id FROM "). @@ -541,7 +537,7 @@ protected AnySearchNode getQuery( clause.append(')'); - return new AnySearchNode.Leaf(svs.field(), clause.toString()); + return new AnySearchNode.Leaf(defaultSV(svs), clause.toString()); } protected AnySearchNode getQuery( @@ -555,9 +551,9 @@ protected AnySearchNode getQuery( StringBuilder clause = new StringBuilder(); if (not) { - clause.append("sv.any_id NOT IN ("); + clause.append(anyId(svs)).append(" NOT IN ("); } else { - clause.append("sv.any_id IN ("); + clause.append(anyId(svs)).append(" IN ("); } clause.append("SELECT DISTINCT group_id AS any_id FROM "). @@ -568,9 +564,9 @@ protected AnySearchNode getQuery( append(") "); if (not) { - clause.append("AND sv.any_id NOT IN ("); + clause.append("AND ").append(anyId(svs)).append(" NOT IN ("); } else { - clause.append("OR sv.any_id IN ("); + clause.append("OR ").append(anyId(svs)).append(" IN ("); } clause.append("SELECT DISTINCT group_id AS any_id FROM "). @@ -580,7 +576,7 @@ protected AnySearchNode getQuery( collect(Collectors.joining(" OR "))). append(')'); - return new AnySearchNode.Leaf(svs.field(), clause.toString()); + return new AnySearchNode.Leaf(defaultSV(svs), clause.toString()); } protected AnySearchNode.Leaf fillAttrQuery( @@ -712,13 +708,13 @@ protected AnySearchNode getQuery( sv.alias + ".schema_id='" + checked.getLeft().getKey() + "'"); case ISNULL: - String clause = new StringBuilder("sv.any_id NOT IN "). + String clause = new StringBuilder(anyId(svs)).append(" NOT IN "). append('('). append("SELECT DISTINCT any_id FROM "). append(sv.name). append(" WHERE schema_id=").append("'").append(checked.getLeft().getKey()).append("'"). append(')').toString(); - return new AnySearchNode.Leaf(svs.field(), clause); + return new AnySearchNode.Leaf(defaultSV(svs), clause); default: AnySearchNode.Leaf node; @@ -733,7 +729,7 @@ protected AnySearchNode getQuery( parameters); node = new AnySearchNode.Leaf( sv, - "sv.any_id NOT IN (" + anyId(svs) + " NOT IN (" + "SELECT any_id FROM " + sv.name + " WHERE " + notNode.getClause().replace(sv.alias + ".", "") + ")"); @@ -772,18 +768,18 @@ protected AnySearchNode getQuery( switch (checked.getRight().getType()) { case ISNULL: return new AnySearchNode.Leaf( - svs.field(), + defaultSV(svs), checked.getRight().getSchema() + (not ? " IS NOT NULL" : " IS NULL")); case ISNOTNULL: return new AnySearchNode.Leaf( - svs.field(), + defaultSV(svs), checked.getRight().getSchema() + (not ? " IS NULL" : " IS NOT NULL")); default: return fillAttrQuery( checked.getRight().getSchema(), - svs.field(), + defaultSV(svs), checked.getMiddle(), checked.getLeft(), checked.getRight(), @@ -798,21 +794,21 @@ protected AnySearchNode.Leaf buildAdminRealmsFilter( final List parameters) { if (realmKeys.isEmpty()) { - return new AnySearchNode.Leaf(svs.field(), "any_id IS NOT NULL"); + return new AnySearchNode.Leaf(defaultSV(svs), StringUtils.substringAfter(anyId(svs), '.') + " IS NOT NULL"); } String realmKeysArg = realmKeys.stream(). map(realmKey -> "?" + setParameter(parameters, realmKey)). collect(Collectors.joining(",")); - return new AnySearchNode.Leaf(svs.field(), "realm_id IN (" + realmKeysArg + ")"); + return new AnySearchNode.Leaf(defaultSV(svs), "realm_id IN (" + realmKeysArg + ")"); } protected Triple, Set> getAdminRealmsFilter( final Realm base, final boolean recursive, final Set adminRealms, - final SearchSupport svs, - final List parameters) { + final List parameters, + final SearchSupport svs) { Set realmKeys = new HashSet<>(); Set dynRealmKeys = new HashSet<>(); @@ -856,14 +852,15 @@ protected void visitNode( final AnySearchNode node, final Map counters, final Set from, - final List where) { + final List where, + final SearchSupport svs) { node.asLeaf().ifPresentOrElse( leaf -> { from.add(leaf.getFrom()); if (counters.computeIfAbsent(leaf.getFrom(), view -> false) && !leaf.getClause().contains(" IN ")) { - where.add("sv.any_id IN (" + where.add(anyId(svs) + " IN (" + "SELECT any_id FROM " + leaf.getFrom().name + " WHERE " + leaf.getClause().replace(leaf.getFrom().alias + ".", "") + ")"); @@ -874,38 +871,72 @@ protected void visitNode( }, () -> { List nodeWhere = new ArrayList<>(); - node.getChildren().forEach(child -> visitNode(child, counters, from, nodeWhere)); + node.getChildren().forEach(child -> visitNode(child, counters, from, nodeWhere, svs)); where.add(nodeWhere.stream(). map(w -> "(" + w + ")"). collect(Collectors.joining(" " + node.getType().name() + " "))); }); } + protected String buildFrom( + final Set from, + final Set plainSchemas, + final OrderBySupport obs) { + + String fromString; + if (from.size() == 1) { + SearchSupport.SearchView sv = from.iterator().next(); + fromString = sv.name + " " + sv.alias; + } else { + List joins = new ArrayList<>(from); + StringBuilder join = new StringBuilder(joins.get(0).name + " " + joins.get(0).alias); + for (int i = 1; i < joins.size(); i++) { + SearchSupport.SearchView sv = joins.get(i); + join.append(" LEFT JOIN "). + append(sv.name).append(" ").append(sv.alias). + append(" ON "). + append(joins.get(0).alias).append(".any_id=").append(sv.alias).append(".any_id"); + } + fromString = join.toString(); + } + return fromString; + } + + protected String buildWhere(final List where, final AnySearchNode root) { + return where.stream(). + map(w -> "(" + w + ")"). + collect(Collectors.joining(" " + root.getType().name() + " ")); + } + protected String buildCountQuery( - final AnySearchNode queryInfoNode, + final Pair> queryInfo, final AnySearchNode.Leaf filterNode, - final List parameters) { + final List parameters, + final SearchSupport svs) { AnySearchNode root; - if (queryInfoNode.getType() == AnySearchNode.Type.AND) { - root = queryInfoNode; + if (queryInfo.getLeft().getType() == AnySearchNode.Type.AND) { + root = queryInfo.getLeft(); } else { root = new AnySearchNode(AnySearchNode.Type.AND); - root.add(queryInfoNode); + root.add(queryInfo.getLeft()); } root.add(filterNode); Set from = new HashSet<>(); List where = new ArrayList<>(); Map counters = new HashMap<>(); - visitNode(root, counters, from, where); + visitNode(root, counters, from, where, svs); + + StringBuilder queryString = new StringBuilder("SELECT COUNT(DISTINCT ").append(anyId(svs)).append(") "); + + queryString.append("FROM ").append(buildFrom(from, queryInfo.getRight(), null)); + + queryString.append(" WHERE ").append(buildWhere(where, root)); - String queryString = "SELECT COUNT(DISTINCT sv.any_id)" - + " FROM " + buildFrom(from) - + " WHERE " + buildWhere(where, root); LOG.debug("Query: {}, parameters: {}", queryString, parameters); - return queryString; + return queryString.toString(); } @Override @@ -922,19 +953,19 @@ protected int doCount( // 1. get admin realms filter Triple, Set> filter = - getAdminRealmsFilter(base, recursive, adminRealms, svs, parameters); + getAdminRealmsFilter(base, recursive, adminRealms, parameters, svs); // 2. transform search condition - Optional optionalQueryInfoNode = getQuery( + Optional>> optionalQueryInfo = getQuery( buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind), parameters, svs); - if (optionalQueryInfoNode.isEmpty()) { + if (optionalQueryInfo.isEmpty()) { LOG.error("Invalid search condition: {}", cond); return 0; } - AnySearchNode queryInfoNode = optionalQueryInfoNode.get(); + Pair> queryInfo = optionalQueryInfo.get(); // 3. generate the query string - String queryString = buildCountQuery(queryInfoNode, filter.getLeft(), parameters); + String queryString = buildCountQuery(queryInfo, filter.getLeft(), parameters, svs); // 4. populate the search query with parameter values Query countQuery = entityManager().createNativeQuery(queryString); @@ -985,9 +1016,9 @@ protected void parseOrderByForField( final String fieldName, final OrderByClause clause) { - item.select = svs.field().alias + '.' + fieldName; + item.select = defaultSV(svs).alias + '.' + fieldName; item.where = StringUtils.EMPTY; - item.orderBy = svs.field().alias + '.' + fieldName + ' ' + clause.getDirection().name(); + item.orderBy = defaultSV(svs).alias + '.' + fieldName + ' ' + clause.getDirection().name(); } protected void parseOrderByForCustom( @@ -1039,7 +1070,7 @@ protected OrderBySupport parseOrderBy( fieldName += "_id"; } - obs.views.add(svs.field()); + obs.views.add(defaultSV(svs)); parseOrderByForField(svs, item, fieldName, clause); } @@ -1056,35 +1087,35 @@ protected OrderBySupport parseOrderBy( } protected String buildSearchQuery( - final AnySearchNode queryInfoNode, + final Pair> queryInfo, final AnySearchNode.Leaf filterNode, final List parameters, final SearchSupport svs, final List orderBy) { AnySearchNode root; - if (queryInfoNode.getType() == AnySearchNode.Type.AND) { - root = queryInfoNode; + if (queryInfo.getLeft().getType() == AnySearchNode.Type.AND) { + root = queryInfo.getLeft(); } else { root = new AnySearchNode(AnySearchNode.Type.AND); - root.add(queryInfoNode); + root.add(queryInfo.getLeft()); } root.add(filterNode); Set from = new HashSet<>(); List where = new ArrayList<>(); Map counters = new HashMap<>(); - visitNode(root, counters, from, where); + visitNode(root, counters, from, where, svs); // 3. take ordering into account OrderBySupport obs = parseOrderBy(svs, orderBy); // 4. generate the query string - StringBuilder queryString = new StringBuilder("SELECT DISTINCT sv.any_id"); + StringBuilder queryString = new StringBuilder("SELECT DISTINCT ").append(anyId(svs)); obs.items.forEach(item -> queryString.append(',').append(item.select)); from.addAll(obs.views); - queryString.append(" FROM ").append(buildFrom(from)); + queryString.append(" FROM ").append(buildFrom(from, queryInfo.getRight(), obs)); queryString.append(" WHERE ").append(buildWhere(where, root)); @@ -1116,19 +1147,19 @@ protected > List doSearch( // 1. get admin realms filter Triple, Set> filter = - getAdminRealmsFilter(base, recursive, adminRealms, svs, parameters); + getAdminRealmsFilter(base, recursive, adminRealms, parameters, svs); // 2. transform search condition - Optional optionalQueryInfoNode = getQuery( + Optional>> optionalQueryInfo = getQuery( buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind), parameters, svs); - if (optionalQueryInfoNode.isEmpty()) { + if (optionalQueryInfo.isEmpty()) { LOG.error("Invalid search condition: {}", cond); return List.of(); } - AnySearchNode queryInfoNode = optionalQueryInfoNode.get(); + Pair> queryInfo = optionalQueryInfo.get(); // 3. generate the query string - String queryString = buildSearchQuery(queryInfoNode, filter.getLeft(), parameters, svs, orderBy); + String queryString = buildSearchQuery(queryInfo, filter.getLeft(), parameters, svs, orderBy); // 4. prepare the search query Query query = entityManager().createNativeQuery(queryString); From a4e463ca93797733f6ee68325a85e3d1e33d9130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Chicchiricc=C3=B2?= Date: Tue, 21 Jan 2025 12:23:19 +0100 Subject: [PATCH 7/9] Small changes --- .../jpa/dao/PGJPAJSONAnySearchDAO.java | 8 +-- .../test/resources/simplelogger.properties | 2 + .../test/resources/simplelogger.properties | 3 +- .../test/resources/simplelogger.properties | 3 +- .../java/data/GroupDataBinderImpl.java | 9 +-- .../test/resources/simplelogger.properties | 3 +- .../syncope/fit/core/UserIssuesITCase.java | 55 ++++++++++--------- 7 files changed, 45 insertions(+), 38 deletions(-) diff --git a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAJSONAnySearchDAO.java b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAJSONAnySearchDAO.java index 7a5319edf0d..e9974776a2d 100644 --- a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAJSONAnySearchDAO.java +++ b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAJSONAnySearchDAO.java @@ -18,8 +18,6 @@ */ package org.apache.syncope.core.persistence.jpa.dao; -import static org.apache.syncope.core.persistence.jpa.dao.AbstractDAO.LOG; - import java.time.format.DateTimeFormatter; import java.util.HashSet; import java.util.List; @@ -299,7 +297,7 @@ protected String buildFrom( final Set plainSchemas, final OrderBySupport obs) { - StringBuilder prefix = new StringBuilder(super.buildFrom(from, plainSchemas, obs)); + StringBuilder clause = new StringBuilder(super.buildFrom(from, plainSchemas, obs)); Set schemas = new HashSet<>(plainSchemas); @@ -313,13 +311,13 @@ protected String buildFrom( } schemas.forEach(schema -> Optional.ofNullable(plainSchemaDAO.find(schema)).ifPresentOrElse( - pschema -> prefix.append(','). + pschema -> clause.append(','). append("jsonb_path_query_array(plainattrs, '$[*] ? (@.schema==\""). append(schema).append("\")."). append("\"").append(pschema.isUniqueConstraint() ? "uniqueValue" : "values").append("\"')"). append(" AS ").append(schema), () -> LOG.warn("Ignoring invalid schema '{}'", schema))); - return prefix.toString(); + return clause.toString(); } } diff --git a/core/persistence-jpa-json/src/test/resources/simplelogger.properties b/core/persistence-jpa-json/src/test/resources/simplelogger.properties index 4f528c65c98..df82bd0d023 100644 --- a/core/persistence-jpa-json/src/test/resources/simplelogger.properties +++ b/core/persistence-jpa-json/src/test/resources/simplelogger.properties @@ -17,5 +17,7 @@ # See http://www.slf4j.org/api/org/slf4j/impl/SimpleLogger.html # Possible values: "trace", "debug", "info", "warn", or "error" +org.slf4j.simpleLogger.showDateTime=true +org.slf4j.simpleLogger.dateTimeFormat=YYYY-mm-dd HH:mm:ss.SSS org.slf4j.simpleLogger.defaultLogLevel=debug org.slf4j.simpleLogger.log.org.springframework.jdbc.core.JdbcTemplate=error diff --git a/core/persistence-jpa/src/test/resources/simplelogger.properties b/core/persistence-jpa/src/test/resources/simplelogger.properties index 929ded23352..df82bd0d023 100644 --- a/core/persistence-jpa/src/test/resources/simplelogger.properties +++ b/core/persistence-jpa/src/test/resources/simplelogger.properties @@ -17,6 +17,7 @@ # See http://www.slf4j.org/api/org/slf4j/impl/SimpleLogger.html # Possible values: "trace", "debug", "info", "warn", or "error" +org.slf4j.simpleLogger.showDateTime=true +org.slf4j.simpleLogger.dateTimeFormat=YYYY-mm-dd HH:mm:ss.SSS org.slf4j.simpleLogger.defaultLogLevel=debug org.slf4j.simpleLogger.log.org.springframework.jdbc.core.JdbcTemplate=error - diff --git a/core/provisioning-api/src/test/resources/simplelogger.properties b/core/provisioning-api/src/test/resources/simplelogger.properties index 973e0096ff1..030d4d091c1 100644 --- a/core/provisioning-api/src/test/resources/simplelogger.properties +++ b/core/provisioning-api/src/test/resources/simplelogger.properties @@ -17,5 +17,6 @@ # See http://www.slf4j.org/api/org/slf4j/impl/SimpleLogger.html # Possible values: "trace", "debug", "info", "warn", or "error" +org.slf4j.simpleLogger.showDateTime=true +org.slf4j.simpleLogger.dateTimeFormat=YYYY-mm-dd HH:mm:ss.SSS org.slf4j.simpleLogger.defaultLogLevel=debug - diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java index 51c59822112..7429c05b381 100644 --- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java +++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java @@ -468,10 +468,11 @@ protected static void populateTransitiveResources( PropagationByResource propByRes = new PropagationByResource<>(); group.getResources().forEach(resource -> { // exclude from propagation those objects that have that resource assigned by some other membership(s) - if (!any.getResources().contains(resource) && any.getMemberships().stream() - .filter(otherGrpMemb -> !otherGrpMemb.getRightEnd().equals(group)) - .noneMatch(otherGrpMemb -> otherGrpMemb.getRightEnd().getResources().stream() - .anyMatch(r -> resource.getKey().equals(r.getKey())))) { + if (!any.getResources().contains(resource) + && any.getMemberships().stream(). + filter(m -> !m.getRightEnd().equals(group)). + noneMatch(m -> m.getRightEnd().getResources().contains(resource))) { + propByRes.add(ResourceOperation.DELETE, resource.getKey()); } diff --git a/ext/scimv2/logic/src/test/resources/simplelogger.properties b/ext/scimv2/logic/src/test/resources/simplelogger.properties index 973e0096ff1..030d4d091c1 100644 --- a/ext/scimv2/logic/src/test/resources/simplelogger.properties +++ b/ext/scimv2/logic/src/test/resources/simplelogger.properties @@ -17,5 +17,6 @@ # See http://www.slf4j.org/api/org/slf4j/impl/SimpleLogger.html # Possible values: "trace", "debug", "info", "warn", or "error" +org.slf4j.simpleLogger.showDateTime=true +org.slf4j.simpleLogger.dateTimeFormat=YYYY-mm-dd HH:mm:ss.SSS org.slf4j.simpleLogger.defaultLogLevel=debug - diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java index 5b4b367f6cf..e01ed30adfa 100644 --- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java +++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java @@ -1831,12 +1831,11 @@ void issueSYNCOPE1818() { void issueSYNCOPE1853() { GroupTO cGroupForPropagation = createGroup( new GroupCR.Builder(SyncopeConstants.ROOT_REALM, "cGroupForPropagation") - .resource(RESOURCE_NAME_LDAP) - .build()).getEntity(); + .resource(RESOURCE_NAME_LDAP).build()).getEntity(); GroupTO dGroupForPropagation = createGroup( new GroupCR.Builder(SyncopeConstants.ROOT_REALM, "dGroupForPropagation") - .resource(RESOURCE_NAME_LDAP) - .build()).getEntity(); + .resource(RESOURCE_NAME_LDAP).build()).getEntity(); + // 1. assign both groups cGroupForPropagation and dGroupForPropagation with resource-csv to bellini updateUser(new UserUR.Builder("c9b2dec2-00a7-4855-97c0-d854842b4b24").memberships( new MembershipUR.Builder(cGroupForPropagation.getKey()).build(), @@ -1844,38 +1843,42 @@ void issueSYNCOPE1853() { // 2. assign cGroupForPropagation also to vivaldi updateUser(new UserUR.Builder("b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee").membership( new MembershipUR.Builder(dGroupForPropagation.getKey()).build()).build()); + // 3. propagation tasks cleanup TASK_SERVICE.search( - new TaskQuery.Builder(TaskType.PROPAGATION) - .anyTypeKind(AnyTypeKind.USER) - .resource(RESOURCE_NAME_LDAP) - .entityKey("c9b2dec2-00a7-4855-97c0-d854842b4b24") - .build()).getResult() + new TaskQuery.Builder(TaskType.PROPAGATION) + .anyTypeKind(AnyTypeKind.USER) + .resource(RESOURCE_NAME_LDAP) + .entityKey("c9b2dec2-00a7-4855-97c0-d854842b4b24") + .build()).getResult() .forEach(pt -> TASK_SERVICE.delete(TaskType.PROPAGATION, pt.getKey())); TASK_SERVICE.search( - new TaskQuery.Builder(TaskType.PROPAGATION) - .anyTypeKind(AnyTypeKind.USER) - .resource(RESOURCE_NAME_LDAP) - .entityKey("b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee") - .build()).getResult() + new TaskQuery.Builder(TaskType.PROPAGATION) + .anyTypeKind(AnyTypeKind.USER) + .resource(RESOURCE_NAME_LDAP) + .entityKey("b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee") + .build()).getResult() .forEach(pt -> TASK_SERVICE.delete(TaskType.PROPAGATION, pt.getKey())); + // 4. delete group cGroupForPropagation: no deprovision should be fired on bellini, since there is already // bGroupForPropagation, deprovision instead must be fired for vivaldi GROUP_SERVICE.delete(cGroupForPropagation.getKey()); - await().during(5, TimeUnit.SECONDS).atMost(10, TimeUnit.SECONDS).until(() -> TASK_SERVICE.search( - new TaskQuery.Builder(TaskType.PROPAGATION) - .anyTypeKind(AnyTypeKind.USER) - .resource(RESOURCE_NAME_LDAP) - .entityKey("c9b2dec2-00a7-4855-97c0-d854842b4b24").build()) + await().during(MAX_WAIT_SECONDS, TimeUnit.SECONDS).atMost(MAX_WAIT_SECONDS, TimeUnit.SECONDS) + .until(() -> TASK_SERVICE.search(new TaskQuery.Builder(TaskType.PROPAGATION) + .anyTypeKind(AnyTypeKind.USER) + .resource(RESOURCE_NAME_LDAP) + .entityKey("c9b2dec2-00a7-4855-97c0-d854842b4b24").build()) .getResult().stream().map(PropagationTaskTO.class::cast) - .collect(Collectors.toList()).stream().noneMatch(pt -> ResourceOperation.DELETE == pt.getOperation())); + .collect(Collectors.toList()).stream() + .noneMatch(pt -> ResourceOperation.DELETE == pt.getOperation())); GROUP_SERVICE.delete(dGroupForPropagation.getKey()); - await().atMost(10, TimeUnit.SECONDS).until(() -> TASK_SERVICE.search( - new TaskQuery.Builder(TaskType.PROPAGATION) - .anyTypeKind(AnyTypeKind.USER) - .resource(RESOURCE_NAME_LDAP) - .entityKey("b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee").build()) + await().atMost(MAX_WAIT_SECONDS, TimeUnit.SECONDS) + .until(() -> TASK_SERVICE.search(new TaskQuery.Builder(TaskType.PROPAGATION) + .anyTypeKind(AnyTypeKind.USER) + .resource(RESOURCE_NAME_LDAP) + .entityKey("b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee").build()) .getResult().stream().map(PropagationTaskTO.class::cast) - .collect(Collectors.toList()).stream().anyMatch(pt -> ResourceOperation.DELETE == pt.getOperation())); + .collect(Collectors.toList()).stream() + .anyMatch(pt -> ResourceOperation.DELETE == pt.getOperation())); } } From 48c8ad9f2a45b9747ebf6011fce3f1c20b05fc67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Chicchiricc=C3=B2?= Date: Tue, 21 Jan 2025 12:42:34 +0100 Subject: [PATCH 8/9] Revert --- .../core/provisioning/java/data/GroupDataBinderImpl.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java index 7429c05b381..522c123bb42 100644 --- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java +++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java @@ -471,7 +471,8 @@ protected static void populateTransitiveResources( if (!any.getResources().contains(resource) && any.getMemberships().stream(). filter(m -> !m.getRightEnd().equals(group)). - noneMatch(m -> m.getRightEnd().getResources().contains(resource))) { + noneMatch(m -> m.getRightEnd().getResources().stream(). + anyMatch(r -> resource.getKey().equals(r.getKey())))) { propByRes.add(ResourceOperation.DELETE, resource.getKey()); } From 4b8da2734c3241b2b4aaf7e7b83629e554fcd1b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Chicchiricc=C3=B2?= Date: Tue, 21 Jan 2025 13:03:50 +0100 Subject: [PATCH 9/9] fixes --- .../java/data/GroupDataBinderImpl.java | 3 +- .../syncope/fit/core/UserIssuesITCase.java | 54 +++++++++---------- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java index 522c123bb42..7429c05b381 100644 --- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java +++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java @@ -471,8 +471,7 @@ protected static void populateTransitiveResources( if (!any.getResources().contains(resource) && any.getMemberships().stream(). filter(m -> !m.getRightEnd().equals(group)). - noneMatch(m -> m.getRightEnd().getResources().stream(). - anyMatch(r -> resource.getKey().equals(r.getKey())))) { + noneMatch(m -> m.getRightEnd().getResources().contains(resource))) { propByRes.add(ResourceOperation.DELETE, resource.getKey()); } diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java index e01ed30adfa..003b2d8426f 100644 --- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java +++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserIssuesITCase.java @@ -1845,40 +1845,38 @@ void issueSYNCOPE1853() { new MembershipUR.Builder(dGroupForPropagation.getKey()).build()).build()); // 3. propagation tasks cleanup - TASK_SERVICE.search( - new TaskQuery.Builder(TaskType.PROPAGATION) - .anyTypeKind(AnyTypeKind.USER) - .resource(RESOURCE_NAME_LDAP) - .entityKey("c9b2dec2-00a7-4855-97c0-d854842b4b24") - .build()).getResult() + TASK_SERVICE.search(new TaskQuery.Builder(TaskType.PROPAGATION) + .anyTypeKind(AnyTypeKind.USER) + .resource(RESOURCE_NAME_LDAP) + .entityKey("c9b2dec2-00a7-4855-97c0-d854842b4b24") + .build()).getResult() .forEach(pt -> TASK_SERVICE.delete(TaskType.PROPAGATION, pt.getKey())); - TASK_SERVICE.search( - new TaskQuery.Builder(TaskType.PROPAGATION) - .anyTypeKind(AnyTypeKind.USER) - .resource(RESOURCE_NAME_LDAP) - .entityKey("b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee") - .build()).getResult() + TASK_SERVICE.search(new TaskQuery.Builder(TaskType.PROPAGATION) + .anyTypeKind(AnyTypeKind.USER) + .resource(RESOURCE_NAME_LDAP) + .entityKey("b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee") + .build()).getResult() .forEach(pt -> TASK_SERVICE.delete(TaskType.PROPAGATION, pt.getKey())); // 4. delete group cGroupForPropagation: no deprovision should be fired on bellini, since there is already // bGroupForPropagation, deprovision instead must be fired for vivaldi GROUP_SERVICE.delete(cGroupForPropagation.getKey()); - await().during(MAX_WAIT_SECONDS, TimeUnit.SECONDS).atMost(MAX_WAIT_SECONDS, TimeUnit.SECONDS) - .until(() -> TASK_SERVICE.search(new TaskQuery.Builder(TaskType.PROPAGATION) - .anyTypeKind(AnyTypeKind.USER) - .resource(RESOURCE_NAME_LDAP) - .entityKey("c9b2dec2-00a7-4855-97c0-d854842b4b24").build()) - .getResult().stream().map(PropagationTaskTO.class::cast) - .collect(Collectors.toList()).stream() - .noneMatch(pt -> ResourceOperation.DELETE == pt.getOperation())); + await().during(5, TimeUnit.SECONDS).atMost(MAX_WAIT_SECONDS, TimeUnit.SECONDS).until( + () -> TASK_SERVICE.search(new TaskQuery.Builder(TaskType.PROPAGATION) + .anyTypeKind(AnyTypeKind.USER) + .resource(RESOURCE_NAME_LDAP) + .entityKey("c9b2dec2-00a7-4855-97c0-d854842b4b24").build()) + .getResult().stream().map(PropagationTaskTO.class::cast) + .collect(Collectors.toList()).stream().noneMatch(pt -> ResourceOperation.DELETE == pt. + getOperation())); GROUP_SERVICE.delete(dGroupForPropagation.getKey()); - await().atMost(MAX_WAIT_SECONDS, TimeUnit.SECONDS) - .until(() -> TASK_SERVICE.search(new TaskQuery.Builder(TaskType.PROPAGATION) - .anyTypeKind(AnyTypeKind.USER) - .resource(RESOURCE_NAME_LDAP) - .entityKey("b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee").build()) - .getResult().stream().map(PropagationTaskTO.class::cast) - .collect(Collectors.toList()).stream() - .anyMatch(pt -> ResourceOperation.DELETE == pt.getOperation())); + await().atMost(MAX_WAIT_SECONDS, TimeUnit.SECONDS).until( + () -> TASK_SERVICE.search(new TaskQuery.Builder(TaskType.PROPAGATION) + .anyTypeKind(AnyTypeKind.USER) + .resource(RESOURCE_NAME_LDAP) + .entityKey("b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee").build()) + .getResult().stream().map(PropagationTaskTO.class::cast) + .collect(Collectors.toList()).stream().anyMatch(pt -> ResourceOperation.DELETE == pt. + getOperation())); } }