From db24b633e9cb33d4fb2b1c82497942de3e1314a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Chicchiricc=C3=B2?= Date: Tue, 21 Jan 2025 13:49:13 +0100 Subject: [PATCH] [SYNCOPE-1855] Rewriting JPAAnySearchDAO to reduce subqueries (#957) --- .../jpa/dao/MaJPAJSONAnySearchDAO.java | 48 +- .../jpa/dao/MyJPAJSONAnySearchDAO.java | 285 ++- .../jpa/dao/OJPAJSONAnySearchDAO.java | 272 ++- .../jpa/dao/PGJPAJSONAnySearchDAO.java | 906 ++------- .../test/resources/simplelogger.properties | 2 + core/persistence-jpa/pom.xml | 16 +- .../jpa/dao/AbstractAnySearchDAO.java | 13 +- .../persistence/jpa/dao/AnySearchNode.java | 151 ++ .../persistence/jpa/dao/JPAAnySearchDAO.java | 1669 +++++++++-------- .../persistence/jpa/dao/SearchSupport.java | 5 + .../persistence/jpa/inner/AnySearchTest.java | 58 +- .../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 | 61 +- 16 files changed, 1505 insertions(+), 1999 deletions(-) create mode 100644 core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnySearchNode.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 8ec7e90070..00fcbdce4d 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 40f4c1cf91..ec86a34bdd 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 67216ee358..d08a4acb42 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 2a829bc461..e9974776a2 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,14 @@ package org.apache.syncope.core.persistence.jpa.dao; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Map; 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 +34,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) { @@ -105,6 +82,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, @@ -136,757 +123,185 @@ 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 ("); - } + clause.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? "). + append("(@.").append(key); - 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(')'); + if (StringUtils.containsAny(value, POSTGRESQL_REGEX_CHARS) || lower) { + clause.append(" like_regex \"^"). + append(escapeForLikeRegex(value).replace("'", "''")). + append("$\""); + } else { + clause.append(" == ").append(escapeIfString(value, isStr)); + } - query.append(')'); + clause.append(lower ? " flag \"i\"" : "").append(")')"); + break; - return query.toString(); - } + case GE: + clause.append("jsonb_path_exists(").append(schema.getKey()).append(", '$[*] ? "). + append("(@.").append(key).append(" >= "). + append(escapeIfString(value, isStr)).append(")')"); + break; - @Override - protected String getQuery( - final RelationshipTypeCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { + 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 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 ("); + clause.insert(0, "NOT "); } - 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(); + return new AnySearchNode.Leaf(from, clause.toString()); } @Override - protected String getQuery( - final MembershipCond cond, + protected AnySearchNode getQuery( + final AttrCond cond, final boolean not, + final Pair checked, 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 ")); - - 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.membership().name).append(" WHERE "). - append('(').append(where).append(')'). - append(") "); - + // normalize NULL / NOT NULL checks 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()); + if (cond.getType() == AttrCond.Type.ISNULL) { + cond.setType(AttrCond.Type.ISNOTNULL); + } else if (cond.getType() == AttrCond.Type.ISNOTNULL) { + cond.setType(AttrCond.Type.ISNULL); } - 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"; } - String realmKeysArg = realmKeys.stream(). - map(realmKey -> "?" + setParameter(parameters, realmKey)). - collect(Collectors.joining(",")); - return "realm_id IN (" + realmKeysArg + ')'; - } - - @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); - } + switch (cond.getType()) { + case ISNOTNULL: + return new AnySearchNode.Leaf( + svs.table(), + "jsonb_path_exists(" + checked.getLeft().getKey() + ",'$[*]')"); - // 5. populate the search query with parameter values - fillWithParameters(query, parameters); + case ISNULL: + return new AnySearchNode.Leaf( + svs.table(), + "NOT jsonb_path_exists(" + checked.getLeft().getKey() + ",'$[*]')"); - // 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); + default: + return filJSONAttrQuery( + svs.table(), + checked.getRight(), + checked.getLeft(), + cond, + not); } - - 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, - final AttrCond cond, - final boolean not, - final List parameters, + protected void visitNode( + final AnySearchNode node, + final Map counters, + final Set from, + final List where, 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: - } - } + counters.clear(); + super.visitNode(node, counters, from, where, svs); } - protected void buildFromAndWhere( - final StringBuilder queryString, - final Pair> queryInfo, - final String realmsFilter, - final SearchSupport svs, + @Override + protected String buildFrom( + final Set from, + final Set plainSchemas, final OrderBySupport obs) { - queryString.append(" FROM ").append(svs.table().name).append(' ').append(svs.table().alias); + StringBuilder clause = new StringBuilder(super.buildFrom(from, plainSchemas, obs)); - Set schemas = queryInfo.getRight(); + Set schemas = new HashSet<>(plainSchemas); 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)) { @@ -895,49 +310,14 @@ protected void buildFromAndWhere( }); } - 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(','). + schemas.forEach(schema -> Optional.ofNullable(plainSchemaDAO.find(schema)).ifPresentOrElse( + pschema -> clause.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(); - - if (queryInfo.getLeft().length() > 0) { - where.append(" WHERE ").append(queryInfo.getLeft()); - } - - 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); - } - } + append(" AS ").append(schema), + () -> LOG.warn("Ignoring invalid schema '{}'", schema))); - queryString.append(where); + 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 4f528c65c9..df82bd0d02 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/pom.xml b/core/persistence-jpa/pom.xml index 3f18cfc0f5..ba23eb7215 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/dao/AbstractAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractAnySearchDAO.java index 0dac17d18f..124a4c419d 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/AnySearchNode.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnySearchNode.java new file mode 100644 index 0000000000..404593b7ea --- /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/JPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAnySearchDAO.java index 55f9c2ef50..4612016fb9 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,10 +19,13 @@ 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; import java.util.stream.Collectors; import javax.persistence.Query; import org.apache.commons.lang3.ArrayUtils; @@ -76,6 +79,61 @@ 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 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 +157,1024 @@ public JPAAnySearchDAO( validator); } - protected String buildAdminRealmsFilter( - final Set realmKeys, - final SearchSupport svs, - final List parameters) { - - if (realmKeys.isEmpty()) { - return "u.any_id IS NOT NULL"; - } + protected SearchSupport.SearchView defaultSV(final SearchSupport svs) { + return svs.field(); + } - 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 + "))"; + protected String anyId(final SearchSupport svs) { + return defaultSV(svs).alias + ".any_id"; } - protected Triple, Set> getAdminRealmsFilter( - final Realm base, - final boolean recursive, - final Set adminRealms, + protected Optional getQueryForCustomConds( + final SearchCond cond, + final List parameters, final SearchSupport svs, - final List parameters) { - - Set realmKeys = new HashSet<>(); - Set dynRealmKeys = new HashSet<>(); - Set groupOwners = new HashSet<>(); + final boolean not) { - 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; - }); + // do nothing by default, leave it open for subclasses + return Optional.empty(); + } - 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 plainSchemas = 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 -> { + Pair checked = check(attrCond, svs.anyTypeKind); + plainSchemas.add(checked.getLeft().getKey()); + return getQuery(attrCond, not, checked, parameters, svs); + })); + } - 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()); + plainSchemas.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()); + plainSchemas.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()); + plainSchemas.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()); + plainSchemas.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, plainSchemas)); + } - // 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(defaultSV(svs), 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(anyId(svs)).append(" NOT IN ("); + } else { + clause.append(anyId(svs)).append(" 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(defaultSV(svs), 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(anyId(svs)).append(" NOT IN ("); + } else { + clause.append(anyId(svs)).append(" 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(defaultSV(svs), 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(anyId(svs)).append(" NOT IN ("); + } else { + clause.append(anyId(svs)).append(" 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(defaultSV(svs), 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(anyId(svs)).append(" NOT IN ("); + } else { + clause.append(anyId(svs)).append(" 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 ").append(anyId(svs)).append(" NOT IN ("); + } else { + clause.append("OR ").append(anyId(svs)).append(" 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(defaultSV(svs), 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(anyId(svs)).append(" NOT IN ("); + } else { + clause.append(anyId(svs)).append(" IN ("); } - return orderBy; - } - - protected String key(final AttrSchemaType schemaType) { - String key; - switch (schemaType) { - case Boolean: - key = "booleanValue"; - break; + clause.append("SELECT DISTINCT any_id FROM "). + append(svs.role().name).append(" WHERE "). + append("role_id=?").append(setParameter(parameters, cond.getRole())). + append(") "); - case Date: - key = "dateValue"; - break; + if (not) { + clause.append("AND ").append(anyId(svs)).append(" NOT IN ("); + } else { + clause.append("OR ").append(anyId(svs)).append(" IN ("); + } - case Double: - key = "doubleValue"; - break; + clause.append("SELECT DISTINCT any_id FROM "). + append(SearchSupport.dynrolemembership().name).append(" WHERE "). + append("role_id=?").append(setParameter(parameters, cond.getRole())). + append("))"); - case Long: - key = "longValue"; - break; + return new AnySearchNode.Leaf(defaultSV(svs), clause.toString()); + } - case Binary: - key = "binaryValue"; - break; + protected AnySearchNode getQuery( + final PrivilegeCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { - default: - key = "stringValue"; + StringBuilder clause = new StringBuilder("("); + + if (not) { + clause.append(anyId(svs)).append(" NOT IN ("); + } else { + clause.append(anyId(svs)).append(" IN ("); } - return key; - } + clause.append("SELECT DISTINCT any_id FROM "). + append(svs.priv().name).append(" WHERE "). + append("privilege_id=?").append(setParameter(parameters, cond.getPrivilege())). + append(") "); - protected void parseOrderByForPlainSchema( - final SearchSupport svs, - final OrderBySupport obs, - final OrderBySupport.Item item, - final OrderByClause clause, - final PlainSchema schema, - final String fieldName) { + if (not) { + clause.append("AND ").append(anyId(svs)).append(" NOT IN ("); + } else { + clause.append("OR ").append(anyId(svs)).append(" IN ("); + } - // keep track of involvement of non-mandatory schemas in the order by clauses - obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition()); + clause.append("SELECT DISTINCT any_id FROM "). + append(svs.dynpriv().name).append(" WHERE "). + append("privilege_id=?").append(setParameter(parameters, cond.getPrivilege())). + append("))"); - if (schema.isUniqueConstraint()) { - obs.views.add(svs.asSearchViewSupport().uniqueAttr()); + return new AnySearchNode.Leaf(defaultSV(svs), clause.toString()); + } - 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()); + protected AnySearchNode getQuery( + final DynRealmCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { - 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(); + StringBuilder clause = new StringBuilder("("); + + if (not) { + clause.append(anyId(svs)).append(" NOT IN ("); + } else { + clause.append(anyId(svs)).append(" IN ("); } - } - protected void parseOrderByForField( - final SearchSupport svs, - final OrderBySupport.Item item, - final String fieldName, - final OrderByClause clause) { + clause.append("SELECT DISTINCT any_id FROM "). + append(SearchSupport.dynrealmmembership().name).append(" WHERE "). + append("dynRealm_id=?").append(setParameter(parameters, cond.getDynRealm())). + append("))"); - item.select = svs.field().alias + '.' + fieldName; - item.where = StringUtils.EMPTY; - item.orderBy = svs.field().alias + '.' + fieldName + ' ' + clause.getDirection().name(); + return new AnySearchNode.Leaf(defaultSV(svs), clause.toString()); } - protected void parseOrderByForCustom( - final SearchSupport svs, - final OrderByClause clause, - final OrderBySupport.Item item, - final OrderBySupport obs) { - - // do nothing by default, meant for subclasses - } + protected AnySearchNode getQuery( + final ResourceCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { - protected OrderBySupport parseOrderBy( - final SearchSupport svs, - final List orderBy) { + StringBuilder clause = new StringBuilder(); - AnyUtils anyUtils = anyUtilsFactory.getInstance(svs.anyTypeKind); + if (not) { + clause.append(anyId(svs)).append(" NOT IN ("); + } else { + clause.append(anyId(svs)).append(" IN ("); + } - OrderBySupport obs = new OrderBySupport(); + clause.append("SELECT DISTINCT any_id FROM "). + append(svs.resource().name). + append(" WHERE resource_id=?"). + append(setParameter(parameters, cond.getResource())); - Set orderByUniquePlainSchemas = new HashSet<>(); - Set orderByNonUniquePlainSchemas = new HashSet<>(); - orderBy.forEach(clause -> { - OrderBySupport.Item item = new OrderBySupport.Item(); + 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())); + } - parseOrderByForCustom(svs, clause, item, obs); + clause.append(')'); - 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(); + return new AnySearchNode.Leaf(defaultSV(svs), clause.toString()); + } - // Adjust field name to column name - if (ArrayUtils.contains(RELATIONSHIP_FIELDS, fieldName)) { - fieldName += "_id"; - } + protected AnySearchNode getQuery( + final MemberCond cond, + final boolean not, + final List parameters, + final SearchSupport svs) { - obs.views.add(svs.field()); + Set members = check(cond); - parseOrderByForField(svs, item, fieldName, clause); - } - } + StringBuilder clause = new StringBuilder(); - if (item.isEmpty()) { - LOG.warn("Cannot build any valid clause from {}", clause); - } else { - obs.items.add(item); - } - }); + if (not) { + clause.append(anyId(svs)).append(" NOT IN ("); + } else { + clause.append(anyId(svs)).append(" IN ("); + } - return obs; - } + 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(") "); - protected void getQueryForCustomConds( - final SearchCond cond, - final List parameters, - final SearchSupport svs, - final boolean not, - final StringBuilder query) { + if (not) { + clause.append("AND ").append(anyId(svs)).append(" NOT IN ("); + } else { + clause.append("OR ").append(anyId(svs)).append(" IN ("); + } - // do nothing by default, leave it open for subclasses - } + 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(')'); - 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("))"); + return new AnySearchNode.Leaf(defaultSV(svs), clause.toString()); } - protected Pair> getQuery( - final SearchCond cond, final List parameters, final SearchSupport svs) { + 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) { - boolean not = cond.getType() == SearchCond.Type.NOT_LEAF; + // activate ignoreCase only for EQ and LIKE operators + boolean ignoreCase = AttrCond.Type.ILIKE == cond.getType() || AttrCond.Type.IEQ == cond.getType(); - StringBuilder query = new StringBuilder(); - Set involvedPlainAttrs = new HashSet<>(); + 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 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); + 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; - 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); + 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 OR: - Pair> leftOrInfo = getQuery(cond.getLeft(), parameters, svs); - involvedPlainAttrs.addAll(leftOrInfo.getRight()); + case GE: + if (not) { + clause.append('<'); + } else { + clause.append(">="); + } + clause.append('?').append(setParameter(parameters, attrValue.getValue())); + break; - Pair> rigthOrInfo = getQuery(cond.getRight(), parameters, svs); - involvedPlainAttrs.addAll(rigthOrInfo.getRight()); + case GT: + if (not) { + clause.append("<="); + } else { + clause.append('>'); + } + clause.append('?').append(setParameter(parameters, attrValue.getValue())); + break; - queryOp(query, "OR", leftOrInfo, rigthOrInfo); + case LE: + if (not) { + clause.append('>'); + } else { + clause.append("<="); + } + clause.append('?').append(setParameter(parameters, attrValue.getValue())); break; - default: + case LT: + if (not) { + clause.append(">="); + } else { + clause.append('<'); + } + clause.append('?').append(setParameter(parameters, attrValue.getValue())); + break; } - return Pair.of(query, involvedPlainAttrs); + return new AnySearchNode.Leaf( + from, + cond instanceof AnyCond + ? clause.toString() + : from.alias + ".schema_id='" + schema.getKey() + "' AND " + clause); } - protected String getQuery( - final AnyTypeCond 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 type_id"); - + // normalize NULL / NOT NULL checks if (not) { - query.append("<>"); - } else { - query.append('='); + 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('?').append(setParameter(parameters, cond.getAnyTypeKey())); - - return query.toString(); - } + SearchSupport.SearchView sv = checked.getLeft().isUniqueConstraint() + ? svs.asSearchViewSupport().uniqueAttr() + : svs.asSearchViewSupport().attr(); - protected String getQuery( - final AuxClassCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { + switch (cond.getType()) { + case ISNOTNULL: + return new AnySearchNode.Leaf( + sv, + sv.alias + ".schema_id='" + checked.getLeft().getKey() + "'"); - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE "); + case ISNULL: + 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(defaultSV(svs), clause); - if (not) { - query.append("any_id NOT IN ("); - } else { - query.append("any_id IN ("); + 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, + anyId(svs) + " 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; } - - query.append("SELECT DISTINCT any_id FROM "). - append(svs.auxClass().name). - append(" WHERE anyTypeClass_id=?"). - append(setParameter(parameters, cond.getAuxClass())). - append(')'); - - return query.toString(); } - protected String getQuery( - final RelationshipTypeCond 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 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(')'); + Triple checked = check(cond, svs.anyTypeKind); - return query.toString(); - } + switch (checked.getRight().getType()) { + case ISNULL: + return new AnySearchNode.Leaf( + defaultSV(svs), + checked.getRight().getSchema() + (not ? " IS NOT NULL" : " IS NULL")); - protected String getQuery( - final RelationshipCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { + case ISNOTNULL: + return new AnySearchNode.Leaf( + defaultSV(svs), + checked.getRight().getSchema() + (not ? " IS NULL" : " IS NOT NULL")); - Set rightAnyObjects = check(cond); + default: + return fillAttrQuery( + checked.getRight().getSchema(), + defaultSV(svs), + checked.getMiddle(), + checked.getLeft(), + checked.getRight(), + not, + parameters); + } + } - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE "); + protected AnySearchNode.Leaf buildAdminRealmsFilter( + final Set realmKeys, + final SearchSupport svs, + final List parameters) { - if (not) { - query.append("any_id NOT IN ("); - } else { - query.append("any_id IN ("); + if (realmKeys.isEmpty()) { + return new AnySearchNode.Leaf(defaultSV(svs), StringUtils.substringAfter(anyId(svs), '.') + " IS NOT NULL"); } - 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(')'); - - return query.toString(); + String realmKeysArg = realmKeys.stream(). + map(realmKey -> "?" + setParameter(parameters, realmKey)). + collect(Collectors.joining(",")); + return new AnySearchNode.Leaf(defaultSV(svs), "realm_id IN (" + realmKeysArg + ")"); } - protected String getQuery( - final MembershipCond cond, - final boolean not, + protected Triple, Set> getAdminRealmsFilter( + final Realm base, + final boolean recursive, + final Set adminRealms, 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 ")); - - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE ("); - - if (not) { - query.append("any_id NOT IN ("); - } else { - query.append("any_id IN ("); - } + Set realmKeys = new HashSet<>(); + Set dynRealmKeys = new HashSet<>(); + Set groupOwners = new HashSet<>(); - query.append("SELECT DISTINCT any_id FROM "). - append(svs.membership().name).append(" WHERE "). - append(where). - append(") "); + 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("AND 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("OR any_id IN ("); - } - - query.append("SELECT DISTINCT any_id FROM "). - append(svs.dyngroupmembership().name).append(" WHERE "). - append(where). - append("))"); + if (adminRealms.stream().anyMatch(r -> r.startsWith(base.getFullPath()))) { + realmKeys.add(base.getKey()); + } + } - return query.toString(); + return Triple.of(buildAdminRealmsFilter(realmKeys, svs, parameters), dynRealmKeys, groupOwners); } - protected String getQuery( - final RoleCond cond, - final boolean not, - final List parameters, + protected void visitNode( + final AnySearchNode node, + final Map counters, + final Set from, + final List where, final SearchSupport svs) { - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE ("); + node.asLeaf().ifPresentOrElse( + leaf -> { + from.add(leaf.getFrom()); - if (not) { - query.append("any_id NOT IN ("); - } else { - query.append("any_id IN ("); - } + if (counters.computeIfAbsent(leaf.getFrom(), view -> false) && !leaf.getClause().contains(" IN ")) { + where.add(anyId(svs) + " 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, svs)); + where.add(nodeWhere.stream(). + map(w -> "(" + w + ")"). + collect(Collectors.joining(" " + node.getType().name() + " "))); + }); + } - query.append("SELECT DISTINCT any_id FROM "). - append(svs.role().name).append(" WHERE "). - append("role_id=?").append(setParameter(parameters, cond.getRole())). - append(") "); + protected String buildFrom( + final Set from, + final Set plainSchemas, + final OrderBySupport obs) { - if (not) { - query.append("AND any_id NOT IN ("); + String fromString; + if (from.size() == 1) { + SearchSupport.SearchView sv = from.iterator().next(); + fromString = sv.name + " " + sv.alias; } else { - query.append("OR any_id IN ("); + 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; + } - query.append("SELECT DISTINCT any_id FROM "). - append(SearchSupport.dynrolemembership().name).append(" WHERE "). - append("role_id=?").append(setParameter(parameters, cond.getRole())). - append("))"); - - return query.toString(); + protected String buildWhere(final List where, final AnySearchNode root) { + return where.stream(). + map(w -> "(" + w + ")"). + collect(Collectors.joining(" " + root.getType().name() + " ")); } - protected String getQuery( - final PrivilegeCond cond, - final boolean not, + protected String buildCountQuery( + final Pair> queryInfo, + final AnySearchNode.Leaf filterNode, final List parameters, final SearchSupport svs) { - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE ("); - - if (not) { - query.append("any_id NOT IN ("); - } else { - query.append("any_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 any_id NOT IN ("); + 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(filterNode); - query.append("SELECT DISTINCT any_id FROM "). - append(svs.dynpriv().name).append(" WHERE "). - append("privilege_id=?").append(setParameter(parameters, cond.getPrivilege())). - append("))"); - - return query.toString(); - } + Set from = new HashSet<>(); + List where = new ArrayList<>(); + Map counters = new HashMap<>(); + visitNode(root, counters, from, where, svs); - protected String getQuery( - final DynRealmCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { + StringBuilder queryString = new StringBuilder("SELECT COUNT(DISTINCT ").append(anyId(svs)).append(") "); - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE ("); + queryString.append("FROM ").append(buildFrom(from, queryInfo.getRight(), null)); - if (not) { - query.append("any_id NOT IN ("); - } else { - query.append("any_id IN ("); - } + queryString.append(" WHERE ").append(buildWhere(where, root)); - query.append("SELECT DISTINCT any_id FROM "). - append(SearchSupport.dynrealmmembership().name).append(" WHERE "). - append("dynRealm_id=?").append(setParameter(parameters, cond.getDynRealm())). - append("))"); + LOG.debug("Query: {}, parameters: {}", queryString, parameters); - return query.toString(); + return queryString.toString(); } - protected String getQuery( - final ResourceCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { + @Override + protected int doCount( + final Realm base, + final boolean recursive, + final Set adminRealms, + final SearchCond cond, + final AnyTypeKind kind) { - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE "); + List parameters = new ArrayList<>(); - if (not) { - query.append("any_id NOT IN ("); - } else { - query.append("any_id IN ("); - } + SearchSupport svs = new SearchViewSupport(kind); - query.append("SELECT DISTINCT any_id FROM "). - append(svs.resource().name). - append(" WHERE resource_id=?"). - append(setParameter(parameters, cond.getResource())); + // 1. get admin realms filter + Triple, Set> filter = + getAdminRealmsFilter(base, recursive, adminRealms, parameters, svs); - 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())); + // 2. transform search condition + Optional>> optionalQueryInfo = getQuery( + buildEffectiveCond(cond, filter.getMiddle(), filter.getRight(), kind), parameters, svs); + if (optionalQueryInfo.isEmpty()) { + LOG.error("Invalid search condition: {}", cond); + return 0; } + Pair> queryInfo = optionalQueryInfo.get(); + + // 3. generate the query string + String queryString = buildCountQuery(queryInfo, filter.getLeft(), parameters, svs); - query.append(')'); + // 4. populate the search query with parameter values + Query countQuery = entityManager().createNativeQuery(queryString); + fillWithParameters(countQuery, parameters); - return query.toString(); + // 5. execute the query and return the result + return ((Number) countQuery.getSingleResult()).intValue(); } - protected String getQuery( - final MemberCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { + protected void parseOrderByForPlainSchema( + final SearchSupport svs, + final OrderBySupport obs, + final OrderBySupport.Item item, + final OrderByClause clause, + final PlainSchema schema, + final String fieldName) { - Set members = check(cond); + // keep track of involvement of non-mandatory schemas in the order by clauses + obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition()); - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE "); + if (schema.isUniqueConstraint()) { + obs.views.add(svs.asSearchViewSupport().uniqueAttr()); - if (not) { - query.append("any_id NOT IN ("); + 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 { - query.append("any_id IN ("); + 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(); } + } - 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(") "); + protected void parseOrderByForField( + final SearchSupport svs, + final OrderBySupport.Item item, + final String fieldName, + final OrderByClause clause) { - if (not) { - query.append("AND any_id NOT IN ("); - } else { - query.append("OR any_id IN ("); - } + item.select = defaultSV(svs).alias + '.' + fieldName; + item.where = StringUtils.EMPTY; + item.orderBy = defaultSV(svs).alias + '.' + fieldName + ' ' + clause.getDirection().name(); + } - 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(')'); + protected void parseOrderByForCustom( + final SearchSupport svs, + final OrderByClause clause, + final OrderBySupport.Item item, + final OrderBySupport obs) { - return query.toString(); + // do nothing by default, meant for subclasses } - protected void fillAttrQuery( - final StringBuilder query, - final PlainAttrValue attrValue, - final PlainSchema schema, - final AttrCond cond, - final boolean not, - final List parameters, - final SearchSupport svs) { + protected OrderBySupport parseOrderBy( + final SearchSupport svs, + final List orderBy) { - // 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(')'); - } else { - // activate ignoreCase only for EQ and LIKE operators - boolean ignoreCase = AttrCond.Type.ILIKE == cond.getType() || AttrCond.Type.IEQ == cond.getType(); + AnyUtils anyUtils = anyUtilsFactory.getInstance(svs.anyTypeKind); - 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; - } + OrderBySupport obs = new OrderBySupport(); - 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(')'); + 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(defaultSV(svs)); - 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, + protected String buildSearchQuery( + final Pair> queryInfo, + final AnySearchNode.Leaf filterNode, final List parameters, - final SearchSupport svs) { + final SearchSupport svs, + final List orderBy) { - Pair checked = check(cond, svs.anyTypeKind); + 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(filterNode); - 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; + Set from = new HashSet<>(); + List where = new ArrayList<>(); + Map counters = new HashMap<>(); + visitNode(root, counters, from, where, svs); - 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; + // 3. take ordering into account + OrderBySupport obs = parseOrderBy(svs, orderBy); - 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); + // 4. generate the query string + 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, queryInfo.getRight(), obs)); + + 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(","))); } - return query.toString(); + LOG.debug("Query: {}, parameters: {}", queryString, parameters); + + return queryString.toString(); } - protected String getQuery( - final AnyCond 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) { - if (JAXRSService.PARAM_REALM.equals(cond.getSchema()) - && !SyncopeConstants.UUID_PATTERN.matcher(cond.getExpression()).matches()) { + List parameters = new ArrayList<>(); - Realm realm = realmDAO.findByFullPath(cond.getExpression()); - if (realm == null) { - throw new IllegalArgumentException("Invalid Realm full path: " + cond.getExpression()); - } - cond.setExpression(realm.getKey()); + SearchSupport svs = new SearchViewSupport(kind); + + // 1. get admin realms filter + Triple, Set> filter = + getAdminRealmsFilter(base, recursive, adminRealms, parameters, svs); + + // 2. transform 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(); - Triple checked = check(cond, svs.anyTypeKind); + // 3. generate the query string + String queryString = buildSearchQuery(queryInfo, filter.getLeft(), parameters, svs, orderBy); + + // 4. prepare the search query + Query query = entityManager().createNativeQuery(queryString); - StringBuilder query = new StringBuilder("SELECT DISTINCT any_id FROM "). - append(svs.field().name).append(" WHERE "); + // 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); + // 5. populate the search query with parameter values + fillWithParameters(query, parameters); - return query.toString(); + // 6. 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/SearchSupport.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/SearchSupport.java index 936315574d..7235f6a81f 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/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 0ace0bd550..5218d66697 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); @@ -758,8 +813,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); diff --git a/core/persistence-jpa/src/test/resources/simplelogger.properties b/core/persistence-jpa/src/test/resources/simplelogger.properties index 929ded2335..df82bd0d02 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 973e0096ff..030d4d091c 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 51c5982211..7429c05b38 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 973e0096ff..030d4d091c 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 5b4b367f6c..003b2d8426 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,40 @@ 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() + 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(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()) - .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(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()) - .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())); } }