diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnyDAO.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnyDAO.java index abf6ff4392..cb46e5d156 100644 --- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnyDAO.java +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/AnyDAO.java @@ -28,9 +28,6 @@ import org.apache.syncope.core.persistence.api.entity.Any; import org.apache.syncope.core.persistence.api.entity.DerSchema; import org.apache.syncope.core.persistence.api.entity.ExternalResource; -import org.apache.syncope.core.persistence.api.entity.PlainAttrUniqueValue; -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.Schema; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -45,11 +42,6 @@ public interface AnyDAO> extends DAO { A authFind(String key); - List findByPlainAttrValue(PlainSchema schema, PlainAttrValue attrValue, boolean ignoreCaseMatch); - - Optional findByPlainAttrUniqueValue( - PlainSchema schema, PlainAttrUniqueValue attrUniqueValue, boolean ignoreCaseMatch); - /** * Find any objects by derived attribute value. This method could fail if one or more string literals contained * into the derived attribute value provided derive from identifier (schema key) replacement. When you are going to diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/search/SearchCond.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/search/SearchCond.java index ec2daaee47..942d01d974 100644 --- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/search/SearchCond.java +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/search/SearchCond.java @@ -60,7 +60,7 @@ public static SearchCond of(final AbstractSearchCond leaf) { return cond; } - public static SearchCond getNotLeaf(final AbstractSearchCond leaf) { + public static SearchCond negate(final AbstractSearchCond leaf) { SearchCond cond = of(leaf); cond.type = Type.NOT_LEAF; @@ -68,7 +68,7 @@ public static SearchCond getNotLeaf(final AbstractSearchCond leaf) { return cond; } - public static SearchCond and(final SearchCond left, final SearchCond right) { + private static SearchCond and(final SearchCond left, final SearchCond right) { SearchCond cond = new SearchCond(); cond.type = Type.AND; @@ -89,10 +89,10 @@ public static SearchCond and(final List conditions) { } public static SearchCond and(final SearchCond... conditions) { - return or(Arrays.asList(conditions)); + return and(Arrays.asList(conditions)); } - public static SearchCond or(final SearchCond left, final SearchCond right) { + private static SearchCond or(final SearchCond left, final SearchCond right) { SearchCond cond = new SearchCond(); cond.type = Type.OR; @@ -135,11 +135,12 @@ public String hasAnyTypeCond() { switch (type) { case LEAF: case NOT_LEAF: - if (leaf instanceof AnyTypeCond) { - anyTypeName = ((AnyTypeCond) leaf).getAnyTypeKey(); + if (leaf instanceof AnyTypeCond anyTypeCond) { + anyTypeName = anyTypeCond.getAnyTypeKey(); } break; + case AND: case OR: if (left != null) { diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondVisitor.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondVisitor.java index 59514b2db8..32644ca883 100644 --- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondVisitor.java +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SearchCondVisitor.java @@ -207,7 +207,7 @@ protected SearchCond visitPrimitive(final SearchCondition sc) { if (notEquals.isPresent() && notEquals.get().getType() == AttrCond.Type.ISNULL) { notEquals.get().setType(AttrCond.Type.ISNOTNULL); } else { - leaf = SearchCond.getNotLeaf(leaf); + leaf = SearchCond.negate(leaf); } } break; diff --git a/core/persistence-api/src/test/java/org/apache/syncope/core/persistence/api/search/SearchCondConverterTest.java b/core/persistence-api/src/test/java/org/apache/syncope/core/persistence/api/search/SearchCondConverterTest.java index eccd4a1abc..4e767d4620 100644 --- a/core/persistence-api/src/test/java/org/apache/syncope/core/persistence/api/search/SearchCondConverterTest.java +++ b/core/persistence-api/src/test/java/org/apache/syncope/core/persistence/api/search/SearchCondConverterTest.java @@ -78,7 +78,7 @@ public void nieq() { AnyCond anyCond = new AnyCond(AttrCond.Type.IEQ); anyCond.setSchema("username"); anyCond.setExpression("rossini"); - SearchCond leaf = SearchCond.getNotLeaf(anyCond); + SearchCond leaf = SearchCond.negate(anyCond); assertEquals(leaf, SearchCondConverter.convert(VISITOR, fiql)); } @@ -117,7 +117,7 @@ public void nilike() { AttrCond attrCond = new AnyCond(AttrCond.Type.ILIKE); attrCond.setSchema("username"); attrCond.setExpression("ros%"); - SearchCond leaf = SearchCond.getNotLeaf(attrCond); + SearchCond leaf = SearchCond.negate(attrCond); assertEquals(leaf, SearchCondConverter.convert(VISITOR, fiql)); } diff --git a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AnyFinder.java b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AnyFinder.java new file mode 100644 index 0000000000..f9dbb602ef --- /dev/null +++ b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AnyFinder.java @@ -0,0 +1,184 @@ +/* + * 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.common.dao; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; +import org.apache.commons.jexl3.parser.Parser; +import org.apache.commons.jexl3.parser.ParserConstants; +import org.apache.commons.jexl3.parser.Token; +import org.apache.commons.lang3.StringUtils; +import org.apache.syncope.common.lib.types.AnyTypeKind; +import org.apache.syncope.core.persistence.api.dao.AnySearchDAO; +import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; +import org.apache.syncope.core.persistence.api.dao.search.AttrCond; +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.DerSchema; +import org.apache.syncope.core.persistence.api.entity.PlainAttrUniqueValue; +import org.apache.syncope.core.persistence.api.entity.PlainSchema; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.transaction.annotation.Transactional; + +public class AnyFinder { + + protected static final Logger LOG = LoggerFactory.getLogger(AnyFinder.class); + + protected static final Comparator LITERAL_COMPARATOR = (l1, l2) -> { + if (l1 == null && l2 == null) { + return 0; + } else if (l1 != null && l2 == null) { + return -1; + } else if (l1 == null) { + return 1; + } else if (l1.length() == l2.length()) { + return 0; + } else if (l1.length() > l2.length()) { + return -1; + } else { + return 1; + } + }; + + /** + * Split an attribute value recurring on provided literals/tokens. + * + * @param attrValue value to be split + * @param literals literals/tokens + * @return split value + */ + protected static List split(final String attrValue, final List literals) { + final List attrValues = new ArrayList<>(); + + if (literals.isEmpty()) { + attrValues.add(attrValue); + } else { + for (String token : attrValue.split(Pattern.quote(literals.get(0)))) { + if (!token.isEmpty()) { + attrValues.addAll(split(token, literals.subList(1, literals.size()))); + } + } + } + + return attrValues; + } + + protected final PlainSchemaDAO plainSchemaDAO; + + protected final AnySearchDAO anySearchDAO; + + public AnyFinder(final PlainSchemaDAO plainSchemaDAO, final AnySearchDAO anySearchDAO) { + this.plainSchemaDAO = plainSchemaDAO; + this.anySearchDAO = anySearchDAO; + } + + @Transactional(readOnly = true) + public > Optional findByPlainAttrUniqueValue( + final AnyTypeKind anyTypeKind, + final PlainSchema schema, + final PlainAttrUniqueValue attrUniqueValue) { + + AttrCond cond = new AttrCond(AttrCond.Type.EQ); + cond.setSchema(schema.getKey()); + cond.setExpression(attrUniqueValue.getStringValue()); + + List result = anySearchDAO.search(SearchCond.of(cond), anyTypeKind); + if (result.isEmpty()) { + return Optional.empty(); + } + return Optional.of(result.get(0)); + } + + @Transactional(readOnly = true) + public > List findByDerAttrValue( + final AnyTypeKind anyTypeKind, + final DerSchema derSchema, + final String value, + final boolean ignoreCaseMatch) { + + if (derSchema == null) { + LOG.error("No DerSchema"); + return List.of(); + } + + Parser parser = new Parser(derSchema.getExpression()); + + // Schema keys + List identifiers = new ArrayList<>(); + + // Literals + List literals = new ArrayList<>(); + + // Get schema keys and literals + for (Token token = parser.getNextToken(); token != null && StringUtils.isNotBlank(token.toString()); + token = parser.getNextToken()) { + + if (token.kind == ParserConstants.STRING_LITERAL) { + literals.add(token.toString().substring(1, token.toString().length() - 1)); + } + + if (token.kind == ParserConstants.IDENTIFIER) { + identifiers.add(token.toString()); + } + } + + // Sort literals in order to process later literals included into others + literals.sort(LITERAL_COMPARATOR); + + // Split value on provided literals + List attrValues = split(value, literals); + + if (attrValues.size() != identifiers.size()) { + LOG.error("Ambiguous JEXL expression resolution: literals and values have different size"); + return List.of(); + } + + List andConditions = new ArrayList<>(); + + // Contains used identifiers in order to avoid replications + Set used = new HashSet<>(); + + // Create several clauses: one for eanch identifiers + for (int i = 0; i < identifiers.size() && !used.contains(identifiers.get(i)); i++) { + // verify schema existence and get schema type + PlainSchema schema = plainSchemaDAO.findById(identifiers.get(i)).orElse(null); + + if (schema == null) { + LOG.error("Invalid schema '{}', ignoring", identifiers.get(i)); + } else { + used.add(identifiers.get(i)); + + AttrCond cond = new AttrCond(ignoreCaseMatch ? AttrCond.Type.IEQ : AttrCond.Type.EQ); + cond.setSchema(schema.getKey()); + cond.setExpression(attrValues.get(i)); + andConditions.add(SearchCond.of(cond)); + } + } + + LOG.debug("Generated search {} conditions: {}", anyTypeKind, andConditions); + + return anySearchDAO.search(SearchCond.and(andConditions), anyTypeKind); + } +} diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MariaDBPersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MariaDBPersistenceContext.java index 337d5700fd..e8c8b39086 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MariaDBPersistenceContext.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MariaDBPersistenceContext.java @@ -31,8 +31,6 @@ import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; import org.apache.syncope.core.persistence.api.entity.EntityFactory; -import org.apache.syncope.core.persistence.jpa.dao.AnyFinder; -import org.apache.syncope.core.persistence.jpa.dao.MariaDBAnyFinder; import org.apache.syncope.core.persistence.jpa.dao.MariaDBJPAAnySearchDAO; import org.apache.syncope.core.persistence.jpa.dao.repo.MariaDBPlainSchemaRepoExtImpl; import org.apache.syncope.core.persistence.jpa.dao.repo.PlainSchemaRepoExt; @@ -53,12 +51,6 @@ public EntityFactory entityFactory() { return new MariaDBEntityFactory(); } - @ConditionalOnMissingBean - @Bean - public AnyFinder anyFinder(final @Lazy PlainSchemaDAO plainSchemaDAO, final EntityManager entityManager) { - return new MariaDBAnyFinder(plainSchemaDAO, entityManager); - } - @ConditionalOnMissingBean @Bean public AnySearchDAO anySearchDAO( diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MySQLPersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MySQLPersistenceContext.java index 44b35dbcce..2bde40db09 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MySQLPersistenceContext.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/MySQLPersistenceContext.java @@ -31,8 +31,6 @@ import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; import org.apache.syncope.core.persistence.api.entity.EntityFactory; -import org.apache.syncope.core.persistence.jpa.dao.AnyFinder; -import org.apache.syncope.core.persistence.jpa.dao.MySQLAnyFinder; import org.apache.syncope.core.persistence.jpa.dao.MySQLJPAAnySearchDAO; import org.apache.syncope.core.persistence.jpa.dao.repo.MySQLPlainSchemaRepoExtImpl; import org.apache.syncope.core.persistence.jpa.dao.repo.PlainSchemaRepoExt; @@ -53,12 +51,6 @@ public EntityFactory entityFactory() { return new MySQLEntityFactory(); } - @ConditionalOnMissingBean - @Bean - public AnyFinder anyFinder(final @Lazy PlainSchemaDAO plainSchemaDAO, final EntityManager entityManager) { - return new MySQLAnyFinder(plainSchemaDAO, entityManager); - } - @ConditionalOnMissingBean @Bean public AnySearchDAO anySearchDAO( diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OraclePersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OraclePersistenceContext.java index 2d4a7fa11d..ac25ca2f7d 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OraclePersistenceContext.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/OraclePersistenceContext.java @@ -31,8 +31,6 @@ import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; import org.apache.syncope.core.persistence.api.entity.EntityFactory; -import org.apache.syncope.core.persistence.jpa.dao.AnyFinder; -import org.apache.syncope.core.persistence.jpa.dao.OracleAnyFinder; import org.apache.syncope.core.persistence.jpa.dao.OracleJPAAnySearchDAO; import org.apache.syncope.core.persistence.jpa.dao.repo.OraclePlainSchemaRepoExtImpl; import org.apache.syncope.core.persistence.jpa.dao.repo.PlainSchemaRepoExt; @@ -53,12 +51,6 @@ public EntityFactory entityFactory() { return new OracleEntityFactory(); } - @ConditionalOnMissingBean - @Bean - public AnyFinder anyFinder(final @Lazy PlainSchemaDAO plainSchemaDAO, final EntityManager entityManager) { - return new OracleAnyFinder(plainSchemaDAO, entityManager); - } - @ConditionalOnMissingBean @Bean public AnySearchDAO anySearchDAO( diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PGPersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PGPersistenceContext.java index 8a4f2dfe23..00af19795c 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PGPersistenceContext.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PGPersistenceContext.java @@ -31,8 +31,6 @@ import org.apache.syncope.core.persistence.api.dao.UserDAO; import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; import org.apache.syncope.core.persistence.api.entity.EntityFactory; -import org.apache.syncope.core.persistence.jpa.dao.AnyFinder; -import org.apache.syncope.core.persistence.jpa.dao.PGAnyFinder; import org.apache.syncope.core.persistence.jpa.dao.PGJPAAnySearchDAO; import org.apache.syncope.core.persistence.jpa.dao.repo.PGPlainSchemaRepoExtImpl; import org.apache.syncope.core.persistence.jpa.dao.repo.PlainSchemaRepoExt; @@ -53,12 +51,6 @@ public EntityFactory entityFactory() { return new PGEntityFactory(); } - @ConditionalOnMissingBean - @Bean - public AnyFinder anyFinder(final @Lazy PlainSchemaDAO plainSchemaDAO, final EntityManager entityManager) { - return new PGAnyFinder(plainSchemaDAO, entityManager); - } - @ConditionalOnMissingBean @Bean public AnySearchDAO anySearchDAO( diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java index 82abb71e9f..cfa3ce359e 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/PersistenceContext.java @@ -88,9 +88,9 @@ import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; import org.apache.syncope.core.persistence.common.CommonPersistenceContext; import org.apache.syncope.core.persistence.common.RuntimeDomainLoader; +import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.jpa.content.XMLContentExporter; import org.apache.syncope.core.persistence.jpa.content.XMLContentLoader; -import org.apache.syncope.core.persistence.jpa.dao.AnyFinder; import org.apache.syncope.core.persistence.jpa.dao.JPAAnyMatchDAO; import org.apache.syncope.core.persistence.jpa.dao.JPAAuditEventDAO; import org.apache.syncope.core.persistence.jpa.dao.JPABatchDAO; @@ -371,6 +371,12 @@ protected Class getRepositoryBaseClass(final RepositoryMetadata metadata) { }; } + @ConditionalOnMissingBean + @Bean + public AnyFinder anyFinder(final @Lazy PlainSchemaDAO plainSchemaDAO, final @Lazy AnySearchDAO anySearchDAO) { + return new AnyFinder(plainSchemaDAO, anySearchDAO); + } + @ConditionalOnMissingBean @Bean public AccessTokenDAO accessTokenDAO(final JpaRepositoryFactory jpaRepositoryFactory) { diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPAAnySearchDAO.java index 7574ecdf7e..2e045b69ee 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPAAnySearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPAAnySearchDAO.java @@ -242,8 +242,11 @@ protected Optional>> getQuery( or(() -> cond.of(AttrCond.class). map(attrCond -> { Pair checked = check(attrCond, svs.anyTypeKind); - plainSchemas.add(checked.getLeft().getKey()); - return getQuery(attrCond, not, checked, parameters, svs); + Pair query = getQuery(attrCond, not, checked, parameters, svs); + if (query.getLeft()) { + plainSchemas.add(checked.getLeft().getKey()); + } + return query.getRight(); })); } @@ -608,9 +611,8 @@ protected AnySearchNode.Leaf fillAttrQuery( } 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 '\\' "); + clause.append(" ESCAPE '\\'"); } } else { LOG.error("LIKE is only compatible with string or enum schemas"); @@ -679,7 +681,7 @@ protected AnySearchNode.Leaf fillAttrQuery( : from.alias() + ".schema_id='" + schema.getKey() + "' AND " + clause); } - protected AnySearchNode getQuery( + protected Pair getQuery( final AttrCond cond, final boolean not, final Pair checked, @@ -701,9 +703,9 @@ protected AnySearchNode getQuery( switch (cond.getType()) { case ISNOTNULL -> { - return new AnySearchNode.Leaf( + return Pair.of(true, new AnySearchNode.Leaf( sv, - sv.alias() + ".schema_id='" + checked.getLeft().getKey() + "'"); + sv.alias() + ".schema_id='" + checked.getLeft().getKey() + "'")); } case ISNULL -> { @@ -713,7 +715,7 @@ protected AnySearchNode getQuery( append(sv.name()). append(" WHERE schema_id=").append("'").append(checked.getLeft().getKey()).append("'"). append(')').toString(); - return new AnySearchNode.Leaf(defaultSV(svs), clause); + return Pair.of(true, new AnySearchNode.Leaf(defaultSV(svs), clause)); } default -> { @@ -743,7 +745,7 @@ protected AnySearchNode getQuery( not, parameters); } - return node; + return Pair.of(true, node); } } } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnyFinder.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnyFinder.java deleted file mode 100644 index 8b962e652b..0000000000 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AnyFinder.java +++ /dev/null @@ -1,334 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.syncope.core.persistence.jpa.dao; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.Query; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.StringJoiner; -import java.util.regex.Pattern; -import org.apache.commons.jexl3.parser.Parser; -import org.apache.commons.jexl3.parser.ParserConstants; -import org.apache.commons.jexl3.parser.Token; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.syncope.common.lib.types.AnyTypeKind; -import org.apache.syncope.common.lib.types.AttrSchemaType; -import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; -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.DerSchema; -import org.apache.syncope.core.persistence.api.entity.PlainAttrUniqueValue; -import org.apache.syncope.core.persistence.api.entity.PlainAttrValue; -import org.apache.syncope.core.persistence.api.entity.PlainSchema; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.transaction.annotation.Transactional; - -public abstract class AnyFinder { - - protected static final Logger LOG = LoggerFactory.getLogger(AnyFinder.class); - - /** - * Split an attribute value recurring on provided literals/tokens. - * - * @param attrValue value to be split - * @param literals literals/tokens - * @return split value - */ - protected static List split(final String attrValue, final List literals) { - final List attrValues = new ArrayList<>(); - - if (literals.isEmpty()) { - attrValues.add(attrValue); - } else { - for (String token : attrValue.split(Pattern.quote(literals.get(0)))) { - if (!token.isEmpty()) { - attrValues.addAll(split(token, literals.subList(1, literals.size()))); - } - } - } - - return attrValues; - } - - protected final PlainSchemaDAO plainSchemaDAO; - - protected final EntityManager entityManager; - - protected AnyFinder(final PlainSchemaDAO plainSchemaDAO, final EntityManager entityManager) { - this.plainSchemaDAO = plainSchemaDAO; - this.entityManager = entityManager; - } - - protected String view(final String table) { - return StringUtils.containsIgnoreCase(table, AnyTypeKind.USER.name()) - ? "user_search" - : StringUtils.containsIgnoreCase(table, AnyTypeKind.GROUP.name()) - ? "group_search" - : "anyObject_search"; - } - - protected abstract String queryBegin(String table); - - protected Pair schemaInfo(final AttrSchemaType schemaType, final boolean ignoreCaseMatch) { - String key; - boolean lower = false; - - 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: - lower = ignoreCaseMatch; - key = "stringValue"; - } - - return Pair.of(key, lower); - } - - protected abstract String attrValueMatch( - AnyUtils anyUtils, - PlainSchema schema, - PlainAttrValue attrValue, - boolean ignoreCaseMatch); - - protected Object getAttrValue( - final PlainSchema schema, - final PlainAttrValue attrValue, - final boolean ignoreCaseMatch) { - - return attrValue.getValue(); - } - - protected > List buildResult(final AnyUtils anyUtils, final List queryResult) { - List result = new ArrayList<>(); - queryResult.forEach(anyKey -> anyUtils.dao().findById(anyKey.toString()).ifPresentOrElse( - result::add, - () -> LOG.error("Could not find any for key {}", anyKey))); - return result; - } - - protected String plainAttrQuery( - final String table, - final AnyUtils anyUtils, - final PlainSchema schema, - final PlainAttrValue attrValue, - final boolean ignoreCaseMatch, - final List queryParams) { - - queryParams.add(schema.getKey()); - queryParams.add(getAttrValue(schema, attrValue, ignoreCaseMatch)); - - return queryBegin(table) + "WHERE " + attrValueMatch(anyUtils, schema, attrValue, ignoreCaseMatch); - } - - @SuppressWarnings("unchecked") - @Transactional(readOnly = true) - public > List findByPlainAttrValue( - final String table, - final AnyUtils anyUtils, - final PlainSchema schema, - final PlainAttrValue attrValue, - final boolean ignoreCaseMatch) { - - if (schema == null) { - LOG.error("No PlainSchema"); - return List.of(); - } - - List queryParams = new ArrayList<>(); - Query query = entityManager.createNativeQuery( - plainAttrQuery(table, anyUtils, schema, attrValue, ignoreCaseMatch, queryParams)); - for (int i = 0; i < queryParams.size(); i++) { - query.setParameter(i + 1, queryParams.get(i)); - } - - return buildResult(anyUtils, query.getResultList()); - } - - @Transactional(readOnly = true) - public > Optional findByPlainAttrUniqueValue( - final String table, - final AnyUtils anyUtils, - final PlainSchema schema, - final PlainAttrUniqueValue attrUniqueValue, - final boolean ignoreCaseMatch) { - - if (schema == null) { - LOG.error("No PlainSchema"); - return Optional.empty(); - } - if (!schema.isUniqueConstraint()) { - LOG.error("This schema has not unique constraint: '{}'", schema.getKey()); - return Optional.empty(); - } - - List result = findByPlainAttrValue(table, anyUtils, schema, attrUniqueValue, ignoreCaseMatch); - return result.isEmpty() - ? Optional.empty() - : Optional.of(result.get(0)); - } - - @SuppressWarnings("unchecked") - private List findByDerAttrValue( - final String table, - final Map> clauses) { - - StringJoiner actualClauses = new StringJoiner(" AND id IN "); - List queryParams = new ArrayList<>(); - - clauses.forEach((clause, parameters) -> { - actualClauses.add(clause); - queryParams.addAll(parameters); - }); - - Query query = entityManager.createNativeQuery( - "SELECT DISTINCT id FROM " + table + " u WHERE id IN " + actualClauses.toString()); - for (int i = 0; i < queryParams.size(); i++) { - query.setParameter(i + 1, queryParams.get(i)); - } - - return query.getResultList(); - } - - @Transactional(readOnly = true) - public > List findByDerAttrValue( - final String table, - final AnyUtils anyUtils, - final DerSchema derSchema, - final String value, - final boolean ignoreCaseMatch) { - - if (derSchema == null) { - LOG.error("No DerSchema"); - return List.of(); - } - - Parser parser = new Parser(derSchema.getExpression()); - - // Schema keys - List identifiers = new ArrayList<>(); - - // Literals - List literals = new ArrayList<>(); - - // Get schema keys and literals - for (Token token = parser.getNextToken(); token != null && StringUtils.isNotBlank(token.toString()); - token = parser.getNextToken()) { - - if (token.kind == ParserConstants.STRING_LITERAL) { - literals.add(token.toString().substring(1, token.toString().length() - 1)); - } - - if (token.kind == ParserConstants.IDENTIFIER) { - identifiers.add(token.toString()); - } - } - - // Sort literals in order to process later literals included into others - literals.sort((l1, l2) -> { - if (l1 == null && l2 == null) { - return 0; - } else if (l1 != null && l2 == null) { - return -1; - } else if (l1 == null) { - return 1; - } else if (l1.length() == l2.length()) { - return 0; - } else if (l1.length() > l2.length()) { - return -1; - } else { - return 1; - } - }); - - // Split value on provided literals - List attrValues = split(value, literals); - - if (attrValues.size() != identifiers.size()) { - LOG.error("Ambiguous JEXL expression resolution: literals and values have different size"); - return List.of(); - } - - Map> clauses = new LinkedHashMap<>(); - - // builder to build the clauses - StringBuilder bld = new StringBuilder(); - - // Contains used identifiers in order to avoid replications - Set used = new HashSet<>(); - - // Create several clauses: one for eanch identifiers - for (int i = 0; i < identifiers.size(); i++) { - if (!used.contains(identifiers.get(i))) { - // verify schema existence and get schema type - PlainSchema schema = plainSchemaDAO.findById(identifiers.get(i)).orElse(null); - - if (schema == null) { - LOG.error("Invalid schema '{}', ignoring", identifiers.get(i)); - } else { - // clear builder - bld.delete(0, bld.length()); - - PlainAttrValue attrValue = schema.isUniqueConstraint() - ? anyUtils.newPlainAttrUniqueValue() - : anyUtils.newPlainAttrValue(); - attrValue.setStringValue(attrValues.get(i)); - - List queryParams = new ArrayList<>(); - bld.append('('). - append(plainAttrQuery(table, anyUtils, schema, attrValue, ignoreCaseMatch, queryParams)). - append(')'); - - used.add(identifiers.get(i)); - - clauses.put(bld.toString(), queryParams); - } - } - } - - LOG.debug("Generated where clauses {}", clauses); - - return buildResult(anyUtils, findByDerAttrValue(table, clauses)); - } -} diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBAnyFinder.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBAnyFinder.java deleted file mode 100644 index 5fd194b942..0000000000 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBAnyFinder.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.syncope.core.persistence.jpa.dao; - -import jakarta.persistence.EntityManager; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.Optional; -import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; -import org.apache.syncope.core.persistence.api.entity.AnyUtils; -import org.apache.syncope.core.persistence.api.entity.PlainAttrValue; -import org.apache.syncope.core.persistence.api.entity.PlainSchema; - -public class MariaDBAnyFinder extends AnyFinder { - - public MariaDBAnyFinder(final PlainSchemaDAO plainSchemaDAO, final EntityManager entityManager) { - super(plainSchemaDAO, entityManager); - } - - @Override - protected String queryBegin(final String table) { - throw new UnsupportedOperationException("This method shall never be called"); - } - - @Override - protected String attrValueMatch( - final AnyUtils anyUtils, - final PlainSchema schema, - final PlainAttrValue attrValue, - final boolean ignoreCaseMatch) { - - throw new UnsupportedOperationException("This method shall never be called"); - } - - @Override - protected String plainAttrQuery( - final String table, - final AnyUtils anyUtils, - final PlainSchema schema, - final PlainAttrValue attrValue, - final boolean ignoreCaseMatch, - final List queryParams) { - - queryParams.add(schema.getKey()); - queryParams.add(attrValue.getStringValue()); - queryParams.add(attrValue.getBooleanValue()); - queryParams.add(Optional.ofNullable(attrValue.getDateValue()). - map(DateTimeFormatter.ISO_OFFSET_DATE_TIME::format).orElse(null)); - queryParams.add(attrValue.getLongValue()); - queryParams.add(attrValue.getDoubleValue()); - - SearchViewSupport svs = new SearchViewSupport(anyUtils.anyTypeKind()); - return "SELECT DISTINCT any_id FROM " - + (schema.isUniqueConstraint() ? svs.uniqueAttr().name() : svs.attr().name()) - + " WHERE schema_id = ? AND ((stringValue IS NOT NULL" - + " AND " - + (ignoreCaseMatch ? "LOWER(" : "") + "stringValue" + (ignoreCaseMatch ? ")" : "") - + " = " - + (ignoreCaseMatch ? "LOWER(" : "BINARY ") + "?" + (ignoreCaseMatch ? ")" : "") + ')' - + " OR (booleanValue IS NOT NULL AND booleanValue = ?)" - + " OR (dateValue IS NOT NULL AND dateValue = ?)" - + " OR (longValue IS NOT NULL AND longValue = ?)" - + " OR (doubleValue IS NOT NULL AND doubleValue = ?))"; - } -} diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java index 3d0e8af191..2d1ac6d301 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MariaDBJPAAnySearchDAO.java @@ -70,7 +70,7 @@ public MariaDBJPAAnySearchDAO( } @Override - protected AnySearchNode getQuery( + protected Pair getQuery( final AttrCond cond, final boolean not, final Pair checked, @@ -88,19 +88,19 @@ protected AnySearchNode getQuery( switch (cond.getType()) { case ISNOTNULL -> { - return new AnySearchNode.Leaf( + return Pair.of(true, new AnySearchNode.Leaf( svs.field(), "JSON_SEARCH(" + "plainAttrs, 'one', '" + checked.getLeft().getKey() + "', NULL, '$[*].schema'" - + ") IS NOT NULL"); + + ") IS NOT NULL")); } case ISNULL -> { - return new AnySearchNode.Leaf( + return Pair.of(true, new AnySearchNode.Leaf( svs.field(), "JSON_SEARCH(" + "plainAttrs, 'one', '" + checked.getLeft().getKey() + "', NULL, '$[*].schema'" - + ") IS NULL"); + + ") IS NULL")); } default -> { @@ -113,11 +113,11 @@ protected AnySearchNode getQuery( container.add(checked.getRight()); } - return new AnySearchNode.Leaf( + return Pair.of(true, new AnySearchNode.Leaf( svs.field(), "JSON_CONTAINS(" + "plainAttrs, '" + POJOHelper.serialize(List.of(container)).replace("'", "''") - + "')"); + + "')")); } else { Optional.ofNullable(checked.getRight().getDateValue()). map(DateTimeFormatter.ISO_OFFSET_DATE_TIME::format). diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLAnyFinder.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLAnyFinder.java deleted file mode 100644 index 27b8498175..0000000000 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLAnyFinder.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.syncope.core.persistence.jpa.dao; - -import jakarta.persistence.EntityManager; -import java.util.List; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; -import org.apache.syncope.core.persistence.api.entity.AnyUtils; -import org.apache.syncope.core.persistence.api.entity.PlainAttr; -import org.apache.syncope.core.persistence.api.entity.PlainAttrUniqueValue; -import org.apache.syncope.core.persistence.api.entity.PlainAttrValue; -import org.apache.syncope.core.persistence.api.entity.PlainSchema; -import org.apache.syncope.core.provisioning.api.serialization.POJOHelper; - -public class MySQLAnyFinder extends AnyFinder { - - public MySQLAnyFinder(final PlainSchemaDAO plainSchemaDAO, final EntityManager entityManager) { - super(plainSchemaDAO, entityManager); - } - - @Override - protected String queryBegin(final String table) { - return "SELECT DISTINCT id FROM " + view(table) + ' '; - } - - @Override - protected String attrValueMatch( - final AnyUtils anyUtils, - final PlainSchema schema, - final PlainAttrValue attrValue, - final boolean ignoreCaseMatch) { - - Pair schemaInfo = schemaInfo(schema.getType(), ignoreCaseMatch); - if (schemaInfo.getRight()) { - return "plainSchema = ? " - + "AND LOWER(" - + (schema.isUniqueConstraint() - ? "attrUniqueValue ->> '$." + schemaInfo.getLeft() + '\'' - : schemaInfo.getLeft()) - + ") = LOWER(?)"; - } else { - PlainAttr container = anyUtils.newPlainAttr(); - container.setSchema(schema); - if (attrValue instanceof PlainAttrUniqueValue plainAttrUniqueValue) { - container.setUniqueValue(plainAttrUniqueValue); - } else { - container.add(attrValue); - } - return "JSON_CONTAINS(plainAttrs, '" + POJOHelper.serialize(List.of(container)).replace("'", "''") + "')"; - } - } -} diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java index 1586bde69c..9a75375fe1 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPAAnySearchDAO.java @@ -175,7 +175,7 @@ protected AnySearchNode.Leaf filJSONAttrQuery( } @Override - protected AnySearchNode getQuery( + protected Pair getQuery( final AttrCond cond, final boolean not, final Pair checked, @@ -193,19 +193,19 @@ protected AnySearchNode getQuery( switch (cond.getType()) { case ISNOTNULL -> { - return new AnySearchNode.Leaf( + return Pair.of(true, new AnySearchNode.Leaf( svs.field(), "JSON_SEARCH(" + "plainAttrs, 'one', '" + checked.getLeft().getKey() + "', NULL, '$[*].schema'" - + ") IS NOT NULL"); + + ") IS NOT NULL")); } case ISNULL -> { - return new AnySearchNode.Leaf( + return Pair.of(true, new AnySearchNode.Leaf( svs.field(), "JSON_SEARCH(" + "plainAttrs, 'one', '" + checked.getLeft().getKey() + "', NULL, '$[*].schema'" - + ") IS NULL"); + + ") IS NULL")); } default -> { @@ -218,11 +218,11 @@ protected AnySearchNode getQuery( container.add(checked.getRight()); } - return new AnySearchNode.Leaf( + return Pair.of(true, new AnySearchNode.Leaf( svs.field(), "JSON_CONTAINS(" + "plainAttrs, '" + POJOHelper.serialize(List.of(container)).replace("'", "''") - + "')"); + + "')")); } else { AnySearchNode.Leaf node; if (not && checked.getLeft().isMultivalue()) { @@ -248,7 +248,7 @@ protected AnySearchNode getQuery( not, parameters); } - return node; + return Pair.of(true, node); } } } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleAnyFinder.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleAnyFinder.java deleted file mode 100644 index 31c12ed185..0000000000 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleAnyFinder.java +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.syncope.core.persistence.jpa.dao; - -import jakarta.persistence.EntityManager; -import org.apache.commons.lang3.BooleanUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.syncope.common.lib.types.AttrSchemaType; -import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; -import org.apache.syncope.core.persistence.api.entity.AnyUtils; -import org.apache.syncope.core.persistence.api.entity.PlainAttrValue; -import org.apache.syncope.core.persistence.api.entity.PlainSchema; - -public class OracleAnyFinder extends AnyFinder { - - public OracleAnyFinder(final PlainSchemaDAO plainSchemaDAO, final EntityManager entityManager) { - super(plainSchemaDAO, entityManager); - } - - @Override - protected String queryBegin(final String table) { - return "SELECT DISTINCT id FROM " + view(table) + ' '; - } - - @Override - protected Object getAttrValue( - final PlainSchema schema, - final PlainAttrValue attrValue, - final boolean ignoreCaseMatch) { - - return schema.getType() == AttrSchemaType.Boolean - ? BooleanUtils.toStringTrueFalse(attrValue.getBooleanValue()) - : schema.getType() == AttrSchemaType.String && ignoreCaseMatch - ? StringUtils.lowerCase(attrValue.getStringValue()) - : attrValue.getValue(); - } - - @Override - protected String attrValueMatch( - final AnyUtils anyUtils, - final PlainSchema schema, - final PlainAttrValue attrValue, - final boolean ignoreCaseMatch) { - - StringBuilder query = new StringBuilder("plainSchema = ? AND "); - - Pair schemaInfo = schemaInfo(schema.getType(), ignoreCaseMatch); - query.append(schemaInfo.getRight() ? "LOWER(" : ""); - - if (schema.isUniqueConstraint()) { - query.append("u").append(schemaInfo.getLeft()); - } else { - query.append("JSON_VALUE(").append(schemaInfo.getLeft()).append(", '$[*]')"); - } - - query.append(schemaInfo.getRight() ? ")" : ""). - append(" = "). - append(schemaInfo.getRight() ? "LOWER(" : ""). - append('?').append(schemaInfo.getRight() ? ")" : ""); - - return query.toString(); - } -} diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPAAnySearchDAO.java index 2e0cb9ec5b..6d1e309ecb 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPAAnySearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPAAnySearchDAO.java @@ -22,8 +22,10 @@ import jakarta.persistence.EntityManagerFactory; import java.time.format.DateTimeFormatter; import java.util.List; +import java.util.Map; import java.util.Optional; -import org.apache.commons.lang3.BooleanUtils; +import java.util.Set; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.syncope.common.lib.types.AttrSchemaType; import org.apache.syncope.core.persistence.api.attrvalue.PlainAttrValidationManager; @@ -42,6 +44,22 @@ public class OracleJPAAnySearchDAO extends AbstractJPAAnySearchDAO { + /** + * + * @param schema + * @return JSON_TABLE(plainAttrs, '$[*]?(@.schema == "fullname").uniqueValue' \ + * COLUMNS uniqueValue PATH '$.stringValue') AS fullname + * or JSON_TABLE(plainAttrs, '$[*]?(@.schema == "loginDate").values[*]' \ + * COLUMNS valuez PATH '$.dateValue') AS loginDate + */ + public static String from(final PlainSchema schema) { + return new StringBuilder("JSON_TABLE(plainAttrs, '$[*]?(@.schema == \"").append(schema.getKey()).append("\")."). + append(schema.isUniqueConstraint() ? "uniqueValue" : "values[*]"). + append("' COLUMNS ").append(schema.isUniqueConstraint() ? "uniqueValue" : "valuez"). + append(" PATH '$.").append(key(schema.getType())).append("') AS ").append(schema.getKey()). + toString(); + } + public OracleJPAAnySearchDAO( final RealmSearchDAO realmSearchDAO, final DynRealmDAO dynRealmDAO, @@ -69,6 +87,16 @@ public OracleJPAAnySearchDAO( entityManager); } + @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, @@ -81,15 +109,27 @@ protected void parseOrderByForPlainSchema( // keep track of involvement of non-mandatory schemas in the order by clauses obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition()); - obs.views.add(svs.field()); + obs.views.add(svs.table()); - item.select = svs.field().alias() + '.' - + (schema.isUniqueConstraint() ? "u" : "") + key(schema.getType()) - + " AS " + fieldName; - item.where = "plainSchema = '" + fieldName + '\''; + item.select = schema.getKey() + "." + + (schema.isUniqueConstraint() ? "uniqueValue" : "valuez") + + " AS " + schema.getKey(); + item.where = StringUtils.EMPTY; item.orderBy = fieldName + ' ' + clause.getDirection().name(); } + @Override + protected void parseOrderByForField( + final SearchSupport svs, + final OrderBySupport.Item item, + final String fieldName, + final Sort.Order clause) { + + item.select = svs.table().alias() + '.' + fieldName; + item.where = StringUtils.EMPTY; + item.orderBy = svs.table().alias() + '.' + fieldName + ' ' + clause.getDirection().name(); + } + protected AnySearchNode.Leaf filJSONAttrQuery( final SearchSupport.SearchView from, final PlainAttrValue attrValue, @@ -98,26 +138,16 @@ protected AnySearchNode.Leaf filJSONAttrQuery( final boolean not, final List parameters) { - 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()); + orElse(cond.getExpression()); 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 ? ')' : ""); + StringBuilder clause = new StringBuilder(lower ? "LOWER(" : ""). + append(schema.getKey()).append('.').append(schema.isUniqueConstraint() ? "uniqueValue" : "valuez"). + append(lower ? ')' : ""); switch (cond.getType()) { case LIKE: @@ -173,17 +203,15 @@ protected AnySearchNode.Leaf filJSONAttrQuery( append('?').append(setParameter(parameters, value)). append(lower ? ")" : ""); - // workaround for Oracle DB adding explicit escaping string, to search - // for literal _ (underscore) (SYNCOPE-1779) + // workaround for Oracle DB adding explicit escaping string, to search for literal _ (underscore) if (cond.getType() == AttrCond.Type.ILIKE || cond.getType() == AttrCond.Type.LIKE) { - clause.append(" ESCAPE '\\' "); + clause.append(" ESCAPE '\\'"); } - return new AnySearchNode.Leaf(from, clause.toString()); } @Override - protected AnySearchNode getQuery( + protected Pair getQuery( final AttrCond cond, final boolean not, final Pair checked, @@ -201,22 +229,22 @@ protected AnySearchNode getQuery( switch (cond.getType()) { case ISNOTNULL -> { - return new AnySearchNode.Leaf( - svs.field(), - "JSON_EXISTS(plainAttrs, '$[*]?(@.schema == \"" + checked.getLeft().getKey() + "\")')"); + return Pair.of(false, new AnySearchNode.Leaf( + svs.table(), + "JSON_EXISTS(plainAttrs, '$[*]?(@.schema == \"" + checked.getLeft().getKey() + "\")')")); } case ISNULL -> { - return new AnySearchNode.Leaf( - svs.field(), - "NOT JSON_EXISTS(plainAttrs, '$[*]?(@.schema == \"" + checked.getLeft().getKey() + "\")')"); + return Pair.of(false, new AnySearchNode.Leaf( + svs.table(), + "NOT JSON_EXISTS(plainAttrs, '$[*]?(@.schema == \"" + checked.getLeft().getKey() + "\")')")); } default -> { AnySearchNode.Leaf node; if (not && checked.getLeft().isMultivalue()) { AnySearchNode.Leaf notNode = filJSONAttrQuery( - svs.field(), + svs.table(), checked.getRight(), checked.getLeft(), cond, @@ -224,21 +252,58 @@ protected AnySearchNode getQuery( parameters); node = new AnySearchNode.Leaf( notNode.getFrom(), - "sv.any_id NOT IN (" - + "SELECT any_id FROM " + notNode.getFrom().name() + "id NOT IN (" + + "SELECT id FROM " + notNode.getFrom().name() + "," + from(checked.getLeft()) + " WHERE " + notNode.getClause().replace(notNode.getFrom().alias() + ".", "") + ")"); + return Pair.of(false, node); } else { node = filJSONAttrQuery( - svs.field(), + svs.table(), checked.getRight(), checked.getLeft(), cond, not, parameters); } - return node; + return Pair.of(true, node); } } } + + @Override + protected void visitNode( + final AnySearchNode node, + final Map counters, + final Set from, + final List where, + final SearchSupport svs) { + + counters.clear(); + super.visitNode(node, counters, from, where, svs); + } + + @Override + protected String buildFrom( + final Set from, + final Set plainSchemas, + final OrderBySupport obs) { + + StringBuilder clause = new StringBuilder(super.buildFrom(from, plainSchemas, obs)); + + plainSchemas.forEach(schema -> plainSchemaDAO.findById(schema). + ifPresent(pschema -> clause.append(",").append(from(pschema)))); + + if (obs != null) { + obs.items.forEach(item -> { + String schema = StringUtils.substringBefore(item.orderBy, ' '); + if (StringUtils.isNotBlank(schema) && !plainSchemas.contains(schema)) { + plainSchemaDAO.findById(schema).ifPresent( + pschema -> clause.append(" LEFT OUTER JOIN ").append(from(pschema)).append(" ON 1=1")); + } + }); + } + + return clause.toString(); + } } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGAnyFinder.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGAnyFinder.java deleted file mode 100644 index afe257a819..0000000000 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGAnyFinder.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.syncope.core.persistence.jpa.dao; - -import jakarta.persistence.EntityManager; -import java.util.List; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO; -import org.apache.syncope.core.persistence.api.entity.AnyUtils; -import org.apache.syncope.core.persistence.api.entity.PlainAttr; -import org.apache.syncope.core.persistence.api.entity.PlainAttrUniqueValue; -import org.apache.syncope.core.persistence.api.entity.PlainAttrValue; -import org.apache.syncope.core.persistence.api.entity.PlainSchema; -import org.apache.syncope.core.provisioning.api.serialization.POJOHelper; - -public class PGAnyFinder extends AnyFinder { - - public PGAnyFinder(final PlainSchemaDAO plainSchemaDAO, final EntityManager entityManager) { - super(plainSchemaDAO, entityManager); - } - - @Override - protected String queryBegin(final String table) { - return "SELECT DISTINCT id FROM " + table + " u," - + "jsonb_array_elements(u.plainAttrs) attrs," - + "jsonb_array_elements(COALESCE(attrs -> 'values', '[{}]'::jsonb)) attrValues "; - } - - @Override - protected String attrValueMatch( - final AnyUtils anyUtils, - final PlainSchema schema, - final PlainAttrValue attrValue, - final boolean ignoreCaseMatch) { - - Pair schemaInfo = schemaInfo(schema.getType(), ignoreCaseMatch); - if (schemaInfo.getRight()) { - return "attrs ->> 'schema' = ? " - + "AND LOWER(" - + (schema.isUniqueConstraint() ? "attrs -> 'uniqueValue'" : "attrValues") - + " ->> '" + schemaInfo.getLeft() - + "') = LOWER(?)"; - } - - PlainAttr container = anyUtils.newPlainAttr(); - container.setSchema(schema); - if (attrValue instanceof PlainAttrUniqueValue plainAttrUniqueValue) { - container.setUniqueValue(plainAttrUniqueValue); - } else { - container.add(attrValue); - } - return "plainAttrs::jsonb @> '" + POJOHelper.serialize(List.of(container)).replace("'", "''") + "'::jsonb"; - } -} diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAAnySearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAAnySearchDAO.java index c06ce2aa8e..e9562cabd5 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAAnySearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAAnySearchDAO.java @@ -239,7 +239,7 @@ protected AnySearchNode.Leaf filJSONAttrQuery( } @Override - protected AnySearchNode getQuery( + protected Pair getQuery( final AttrCond cond, final boolean not, final Pair checked, @@ -257,22 +257,22 @@ protected AnySearchNode getQuery( return switch (cond.getType()) { case ISNOTNULL -> - new AnySearchNode.Leaf( + Pair.of(true, new AnySearchNode.Leaf( svs.table(), - "jsonb_path_exists(" + checked.getLeft().getKey() + ",'$[*]')"); + "jsonb_path_exists(" + checked.getLeft().getKey() + ",'$[*]')")); case ISNULL -> - new AnySearchNode.Leaf( + Pair.of(true, new AnySearchNode.Leaf( svs.table(), - "NOT jsonb_path_exists(" + checked.getLeft().getKey() + ",'$[*]')"); + "NOT jsonb_path_exists(" + checked.getLeft().getKey() + ",'$[*]')")); default -> - filJSONAttrQuery( + Pair.of(true, filJSONAttrQuery( svs.table(), checked.getRight(), checked.getLeft(), cond, - not); + not)); }; } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AbstractAnyRepoExt.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AbstractAnyRepoExt.java index 2786163b67..3add4130f8 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AbstractAnyRepoExt.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AbstractAnyRepoExt.java @@ -40,15 +40,13 @@ import org.apache.syncope.core.persistence.api.entity.AnyUtils; import org.apache.syncope.core.persistence.api.entity.DerSchema; import org.apache.syncope.core.persistence.api.entity.DynRealm; -import org.apache.syncope.core.persistence.api.entity.PlainAttrUniqueValue; -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.Schema; import org.apache.syncope.core.persistence.api.entity.VirSchema; import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject; import org.apache.syncope.core.persistence.api.entity.group.Group; import org.apache.syncope.core.persistence.api.entity.user.User; -import org.apache.syncope.core.persistence.jpa.dao.AnyFinder; +import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.jpa.entity.AbstractAttributable; import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAAnyObject; import org.apache.syncope.core.persistence.jpa.entity.group.JPAGroup; @@ -139,28 +137,9 @@ public A authFind(final String key) { return any; } - @Override - @SuppressWarnings("unchecked") - public List findByPlainAttrValue( - final PlainSchema schema, - final PlainAttrValue attrValue, - final boolean ignoreCaseMatch) { - - return anyFinder.findByPlainAttrValue(table, anyUtils, schema, attrValue, ignoreCaseMatch); - } - - @Override - public Optional findByPlainAttrUniqueValue( - final PlainSchema schema, - final PlainAttrUniqueValue attrUniqueValue, - final boolean ignoreCaseMatch) { - - return anyFinder.findByPlainAttrUniqueValue(table, anyUtils, schema, attrUniqueValue, ignoreCaseMatch); - } - @Override public List findByDerAttrValue(final DerSchema derSchema, final String value, final boolean ignoreCaseMatch) { - return anyFinder.findByDerAttrValue(table, anyUtils, derSchema, value, ignoreCaseMatch); + return anyFinder.findByDerAttrValue(anyUtils.anyTypeKind(), derSchema, value, ignoreCaseMatch); } @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true) @@ -239,7 +218,8 @@ protected void checkBeforeSave(final A any) { new ArrayList<>(((AbstractAttributable) any).getPlainAttrsList()).stream(). filter(attr -> attr.getUniqueValue() != null). forEach(attr -> { - Optional other = findByPlainAttrUniqueValue(attr.getSchema(), attr.getUniqueValue(), false); + Optional other = anyFinder.findByPlainAttrUniqueValue( + anyUtils.anyTypeKind(), attr.getSchema(), attr.getUniqueValue()); if (other.isEmpty() || other.get().getKey().equals(any.getKey())) { LOG.debug("No duplicate value found for {}={}", attr.getSchema().getKey(), attr.getUniqueValue().getValueAsString()); diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyObjectRepoExtImpl.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyObjectRepoExtImpl.java index 6bff4c4e72..abe9be08d6 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyObjectRepoExtImpl.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyObjectRepoExtImpl.java @@ -49,7 +49,7 @@ import org.apache.syncope.core.persistence.api.entity.group.Group; import org.apache.syncope.core.persistence.api.entity.user.URelationship; import org.apache.syncope.core.persistence.api.utils.RealmUtils; -import org.apache.syncope.core.persistence.jpa.dao.AnyFinder; +import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAAMembership; import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAARelationship; import org.apache.syncope.core.persistence.jpa.entity.user.JPAURelationship; diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyRepoExt.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyRepoExt.java index 93bdaec31e..a04e3bf278 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyRepoExt.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/AnyRepoExt.java @@ -25,9 +25,6 @@ import org.apache.syncope.core.persistence.api.dao.AllowedSchemas; import org.apache.syncope.core.persistence.api.entity.Any; import org.apache.syncope.core.persistence.api.entity.DerSchema; -import org.apache.syncope.core.persistence.api.entity.PlainAttrUniqueValue; -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.Schema; public interface AnyRepoExt> { @@ -36,11 +33,6 @@ public interface AnyRepoExt> { A authFind(String key); - List findByPlainAttrValue(PlainSchema schema, PlainAttrValue attrValue, boolean ignoreCaseMatch); - - Optional findByPlainAttrUniqueValue( - PlainSchema schema, PlainAttrUniqueValue attrUniqueValue, boolean ignoreCaseMatch); - List findByDerAttrValue(DerSchema schema, String value, boolean ignoreCaseMatch); AllowedSchemas findAllowedSchemas(A any, Class reference); diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/GroupRepoExtImpl.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/GroupRepoExtImpl.java index 88b1879cd9..7f3508ecc0 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/GroupRepoExtImpl.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/GroupRepoExtImpl.java @@ -56,7 +56,7 @@ import org.apache.syncope.core.persistence.api.search.SearchCondConverter; import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; import org.apache.syncope.core.persistence.api.utils.RealmUtils; -import org.apache.syncope.core.persistence.jpa.dao.AnyFinder; +import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAADynGroupMembership; import org.apache.syncope.core.persistence.jpa.entity.anyobject.JPAAMembership; import org.apache.syncope.core.persistence.jpa.entity.group.JPATypeExtension; diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/OraclePlainSchemaRepoExtImpl.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/OraclePlainSchemaRepoExtImpl.java index 729336b27c..561704e53d 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/OraclePlainSchemaRepoExtImpl.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/OraclePlainSchemaRepoExtImpl.java @@ -24,6 +24,7 @@ import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory; import org.apache.syncope.core.persistence.api.entity.PlainAttr; import org.apache.syncope.core.persistence.api.entity.PlainSchema; +import org.apache.syncope.core.persistence.jpa.dao.OracleJPAAnySearchDAO; import org.apache.syncope.core.persistence.jpa.dao.SearchSupport; public class OraclePlainSchemaRepoExtImpl extends AbstractPlainSchemaRepoExt { @@ -40,9 +41,8 @@ public OraclePlainSchemaRepoExtImpl( public > boolean hasAttrs(final PlainSchema schema, final Class reference) { Query query = entityManager.createNativeQuery( "SELECT COUNT(id) FROM " - + new SearchSupport(getAnyTypeKind(reference)).field().name() - + " WHERE plainSchema = ?"); - query.setParameter(1, schema.getKey()); + + new SearchSupport(getAnyTypeKind(reference)).table().name() + "," + + OracleJPAAnySearchDAO.from(schema)); return ((Number) query.getSingleResult()).intValue() > 0; } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/UserRepoExtImpl.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/UserRepoExtImpl.java index 20d04067b3..8a1d108e3e 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/UserRepoExtImpl.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/repo/UserRepoExtImpl.java @@ -45,7 +45,7 @@ import org.apache.syncope.core.persistence.api.entity.user.UMembership; import org.apache.syncope.core.persistence.api.entity.user.User; import org.apache.syncope.core.persistence.api.utils.RealmUtils; -import org.apache.syncope.core.persistence.jpa.dao.AnyFinder; +import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.jpa.entity.user.JPALinkedAccount; import org.apache.syncope.core.persistence.jpa.entity.user.JPAUMembership; import org.apache.syncope.core.spring.security.AuthContextUtils; diff --git a/core/persistence-jpa/src/main/resources/META-INF/oracle/views.xml b/core/persistence-jpa/src/main/resources/META-INF/oracle/views.xml index 0bc2c43286..c813b1cfa1 100644 --- a/core/persistence-jpa/src/main/resources/META-INF/oracle/views.xml +++ b/core/persistence-jpa/src/main/resources/META-INF/oracle/views.xml @@ -47,28 +47,6 @@ under the License. - - CREATE VIEW user_search AS - - SELECT u.id as any_id, u.*, attrs.* - FROM SyncopeUser u LEFT OUTER JOIN JSON_TABLE(u.plainAttrs, '$[*]' COLUMNS ( - plainSchema PATH '$.schema', - NESTED PATH '$.uniqueValue' COLUMNS( - ubinaryValue PATH '$.binaryValue', - ubooleanValue PATH '$.booleanValue', - udateValue PATH '$.dateValue', - ulongValue PATH '$.longValue', - udoubleValue PATH '$.doubleValue', - ustringValue PATH '$.stringValue'), - NESTED PATH '$.values[*]' COLUMNS( - binaryValue FORMAT JSON WITH WRAPPER PATH '$.binaryValue', - booleanValue FORMAT JSON WITH WRAPPER PATH '$.booleanValue', - dateValue FORMAT JSON WITH WRAPPER PATH '$.dateValue', - longValue FORMAT JSON WITH WRAPPER PATH '$.longValue', - doubleValue FORMAT JSON WITH WRAPPER PATH '$.doubleValue', - stringValue FORMAT JSON WITH WRAPPER PATH '$.stringValue') - )) AS attrs ON 1=1 - CREATE VIEW user_search_urelationship AS 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 87c024f41b..efb8d50b26 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 @@ -137,7 +137,7 @@ public void searchTwoPlainSchemas() { surnameCond.setSchema("surname"); surnameCond.setExpression("Verdi"); - cond = SearchCond.and(SearchCond.of(firstnameCond), SearchCond.getNotLeaf(surnameCond)); + cond = SearchCond.and(SearchCond.of(firstnameCond), SearchCond.negate(surnameCond)); assertTrue(cond.isValid()); users = searchDAO.search(cond, AnyTypeKind.USER); @@ -163,7 +163,7 @@ public void searchTwoPlainSchemas() { userIdCond.setSchema("userId"); userIdCond.setExpression("rossini@apache.org"); - cond = SearchCond.and(SearchCond.of(fullnameCond), SearchCond.getNotLeaf(userIdCond)); + cond = SearchCond.and(SearchCond.of(fullnameCond), SearchCond.negate(userIdCond)); assertTrue(cond.isValid()); users = searchDAO.search(cond, AnyTypeKind.USER); @@ -231,7 +231,7 @@ public void searchWithNotAttrCond() { fullnameLeafCond.setSchema("fullname"); fullnameLeafCond.setExpression("Giuseppe Verdi"); - SearchCond cond = SearchCond.getNotLeaf(fullnameLeafCond); + SearchCond cond = SearchCond.negate(fullnameLeafCond); assertTrue(cond.isValid()); List users = searchDAO.search(cond, AnyTypeKind.USER); @@ -249,7 +249,7 @@ public void searchWithNotAnyCond() { usernameLeafCond.setSchema("username"); usernameLeafCond.setExpression("verdi"); - SearchCond cond = SearchCond.getNotLeaf(usernameLeafCond); + SearchCond cond = SearchCond.negate(usernameLeafCond); assertTrue(cond.isValid()); List users = searchDAO.search(cond, AnyTypeKind.USER); @@ -265,7 +265,7 @@ public void searchCaseInsensitiveWithNotCondition() { fullnameLeafCond.setSchema("fullname"); fullnameLeafCond.setExpression("giuseppe verdi"); - SearchCond cond = SearchCond.getNotLeaf(fullnameLeafCond); + SearchCond cond = SearchCond.negate(fullnameLeafCond); assertTrue(cond.isValid()); List users = searchDAO.search(cond, AnyTypeKind.USER); @@ -440,7 +440,7 @@ public void searchByResource() { ResourceCond ws1 = new ResourceCond(); ws1.setResource("ws-target-resource-list-mappings-2"); - SearchCond searchCondition = SearchCond.and(SearchCond.getNotLeaf(ws2), SearchCond.of(ws1)); + SearchCond searchCondition = SearchCond.and(SearchCond.negate(ws2), SearchCond.of(ws1)); assertTrue(searchCondition.isValid()); List users = searchDAO.search(searchCondition, AnyTypeKind.USER); @@ -748,7 +748,7 @@ public void changePwdDate() { AnyCond changePwdDateCond = new AnyCond(AttrCond.Type.ISNULL); changePwdDateCond.setSchema("changePwdDate"); - SearchCond cond = SearchCond.and(SearchCond.getNotLeaf(statusCond), SearchCond.of(changePwdDateCond)); + SearchCond cond = SearchCond.and(SearchCond.negate(statusCond), SearchCond.of(changePwdDateCond)); assertTrue(cond.isValid()); List users = searchDAO.search(cond, AnyTypeKind.USER); @@ -765,7 +765,7 @@ public void issue202() { ws1.setResource("ws-target-resource-list-mappings-1"); SearchCond searchCondition = - SearchCond.and(SearchCond.getNotLeaf(ws2), SearchCond.getNotLeaf(ws1)); + SearchCond.and(SearchCond.negate(ws2), SearchCond.negate(ws1)); assertTrue(searchCondition.isValid()); List users = searchDAO.search(searchCondition, AnyTypeKind.USER); @@ -952,7 +952,7 @@ public void issueSYNCOPE1419() { loginDateCond.setSchema("loginDate"); loginDateCond.setExpression(LOGIN_DATE_VALUE); - SearchCond cond = SearchCond.getNotLeaf(loginDateCond); + SearchCond cond = SearchCond.negate(loginDateCond); assertTrue(cond.isValid()); List users = searchDAO.search(cond, AnyTypeKind.USER); diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/UserTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/UserTest.java index 6105bcf5a2..179ea8e749 100644 --- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/UserTest.java +++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/UserTest.java @@ -27,7 +27,6 @@ import java.time.OffsetDateTime; import java.util.List; -import java.util.Optional; import org.apache.syncope.common.lib.types.CipherAlgorithm; import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO; import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO; @@ -36,10 +35,7 @@ import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO; import org.apache.syncope.core.persistence.api.dao.SecurityQuestionDAO; import org.apache.syncope.core.persistence.api.dao.UserDAO; -import org.apache.syncope.core.persistence.api.entity.PlainSchema; import org.apache.syncope.core.persistence.api.entity.user.UMembership; -import org.apache.syncope.core.persistence.api.entity.user.UPlainAttrUniqueValue; -import org.apache.syncope.core.persistence.api.entity.user.UPlainAttrValue; import org.apache.syncope.core.persistence.api.entity.user.User; import org.apache.syncope.core.persistence.jpa.AbstractTest; import org.apache.syncope.core.spring.security.Encryptor; @@ -141,35 +137,6 @@ public void findByInvalidDerAttrExpression() { derSchemaDAO.findById("noschema").orElseThrow(), "Antonio, Maria", false).isEmpty()); } - @Test - public void findByPlainAttrUniqueValue() { - UPlainAttrUniqueValue fullnameValue = entityFactory.newEntity(UPlainAttrUniqueValue.class); - fullnameValue.setStringValue("Gioacchino Rossini"); - - PlainSchema fullname = plainSchemaDAO.findById("fullname").orElseThrow(); - - Optional found = userDAO.findByPlainAttrUniqueValue(fullname, fullnameValue, false); - assertTrue(found.isPresent()); - - fullnameValue.setStringValue("Gioacchino ROSSINI"); - - found = userDAO.findByPlainAttrUniqueValue(fullname, fullnameValue, false); - assertFalse(found.isPresent()); - - found = userDAO.findByPlainAttrUniqueValue(fullname, fullnameValue, true); - assertTrue(found.isPresent()); - } - - @Test - public void findByPlainAttrBooleanValue() { - UPlainAttrValue coolValue = entityFactory.newEntity(UPlainAttrValue.class); - coolValue.setBooleanValue(true); - - List list = userDAO.findByPlainAttrValue( - plainSchemaDAO.findById("cool").orElseThrow(), coolValue, false); - assertEquals(1, list.size()); - } - @Test public void findByKey() { assertTrue(userDAO.findById("1417acbe-cbf6-4277-9372-e75e04f97000").isPresent()); diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/PersistenceContext.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/PersistenceContext.java index f5a716fa90..e46cb9ebbf 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/PersistenceContext.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/PersistenceContext.java @@ -90,6 +90,7 @@ import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; import org.apache.syncope.core.persistence.common.CommonPersistenceContext; import org.apache.syncope.core.persistence.common.RuntimeDomainLoader; +import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.neo4j.content.XMLContentExporter; import org.apache.syncope.core.persistence.neo4j.content.XMLContentLoader; import org.apache.syncope.core.persistence.neo4j.dao.Neo4jAnyMatchDAO; @@ -467,6 +468,12 @@ public SyncopeNeo4jRepositoryFactory neo4jRepositoryFactory( return new SyncopeNeo4jRepositoryFactory(neo4jOperations, mappingContext); } + @ConditionalOnMissingBean + @Bean + public AnyFinder anyFinder(final @Lazy PlainSchemaDAO plainSchemaDAO, final @Lazy AnySearchDAO anySearchDAO) { + return new AnyFinder(plainSchemaDAO, anySearchDAO); + } + @ConditionalOnMissingBean @Bean public AccessTokenDAO accessTokenDAO(final SyncopeNeo4jRepositoryFactory neo4jRepositoryFactory) { @@ -519,6 +526,7 @@ public AnyObjectRepoExt anyObjectRepoExt( final @Lazy DynRealmDAO dynRealmDAO, final @Lazy UserDAO userDAO, final @Lazy GroupDAO groupDAO, + final @Lazy AnyFinder anyFinder, final Neo4jTemplate neo4jTemplate, final Neo4jClient neo4jClient, final NodeValidator nodeValidator, @@ -534,6 +542,7 @@ public AnyObjectRepoExt anyObjectRepoExt( dynRealmDAO, userDAO, groupDAO, + anyFinder, neo4jTemplate, neo4jClient, nodeValidator, @@ -955,6 +964,7 @@ public GroupRepoExt groupRepoExt( final @Lazy UserDAO userDAO, final @Lazy AnyObjectDAO anyObjectDAO, final AnySearchDAO anySearchDAO, + final @Lazy AnyFinder anyFinder, final SearchCondVisitor searchCondVisitor, final Neo4jTemplate neo4jTemplate, final Neo4jClient neo4jClient, @@ -974,6 +984,7 @@ public GroupRepoExt groupRepoExt( userDAO, anyObjectDAO, anySearchDAO, + anyFinder, searchCondVisitor, neo4jTemplate, neo4jClient, @@ -1476,6 +1487,7 @@ public UserRepoExt userRepoExt( final @Lazy GroupDAO groupDAO, final DelegationDAO delegationDAO, final FIQLQueryDAO fiqlQueryDAO, + final @Lazy AnyFinder anyFinder, final Neo4jTemplate neo4jTemplate, final Neo4jClient neo4jClient, final NodeValidator nodeValidator, @@ -1494,6 +1506,7 @@ public UserRepoExt userRepoExt( groupDAO, delegationDAO, fiqlQueryDAO, + anyFinder, securityProperties, neo4jTemplate, neo4jClient, diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AbstractAnyRepoExt.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AbstractAnyRepoExt.java index 8857f06f90..490be35fb0 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AbstractAnyRepoExt.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AbstractAnyRepoExt.java @@ -28,12 +28,7 @@ import java.util.Optional; import java.util.Set; import java.util.regex.Pattern; -import java.util.stream.Collectors; import javax.cache.Cache; -import org.apache.commons.jexl3.parser.Parser; -import org.apache.commons.jexl3.parser.ParserConstants; -import org.apache.commons.jexl3.parser.Token; -import org.apache.commons.lang3.StringUtils; import org.apache.syncope.core.persistence.api.dao.AllowedSchemas; import org.apache.syncope.core.persistence.api.dao.AnyTypeClassDAO; import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO; @@ -49,21 +44,18 @@ import org.apache.syncope.core.persistence.api.entity.AnyUtils; import org.apache.syncope.core.persistence.api.entity.DerSchema; import org.apache.syncope.core.persistence.api.entity.ExternalResource; -import org.apache.syncope.core.persistence.api.entity.PlainAttr; -import org.apache.syncope.core.persistence.api.entity.PlainAttrUniqueValue; -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.Schema; import org.apache.syncope.core.persistence.api.entity.VirSchema; import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject; import org.apache.syncope.core.persistence.api.entity.group.Group; import org.apache.syncope.core.persistence.api.entity.user.User; +import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.neo4j.dao.AbstractDAO; import org.apache.syncope.core.persistence.neo4j.entity.AbstractAny; import org.apache.syncope.core.persistence.neo4j.entity.EntityCacheKey; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jDynRealm; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jExternalResource; -import org.apache.syncope.core.provisioning.api.serialization.POJOHelper; import org.apache.syncope.core.spring.security.AuthContextUtils; import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.Neo4jTemplate; @@ -108,6 +100,8 @@ protected static List split(final String attrValue, final List l protected final DynRealmDAO dynRealmDAO; + protected final AnyFinder anyFinder; + protected final AnyUtils anyUtils; protected AbstractAnyRepoExt( @@ -117,6 +111,7 @@ protected AbstractAnyRepoExt( final DerSchemaDAO derSchemaDAO, final VirSchemaDAO virSchemaDAO, final DynRealmDAO dynRealmDAO, + final AnyFinder anyFinder, final AnyUtils anyUtils, final Neo4jTemplate neo4jTemplate, final Neo4jClient neo4jClient) { @@ -128,6 +123,7 @@ protected AbstractAnyRepoExt( this.derSchemaDAO = derSchemaDAO; this.virSchemaDAO = virSchemaDAO; this.dynRealmDAO = dynRealmDAO; + this.anyFinder = anyFinder; this.anyUtils = anyUtils; } @@ -174,173 +170,9 @@ public A authFind(final String key) { return any; } - @Override - @SuppressWarnings("unchecked") - public List findByPlainAttrValue( - final PlainSchema schema, - final PlainAttrValue attrValue, - final boolean ignoreCaseMatch) { - - if (schema == null) { - LOG.error("No PlainSchema"); - return List.of(); - } - - PlainAttr attr = anyUtils.newPlainAttr(); - attr.setSchema(schema); - if (attrValue instanceof PlainAttrUniqueValue plainAttrUniqueValue) { - attr.setUniqueValue(plainAttrUniqueValue); - } else { - attr.add(attrValue); - } - - String op; - Map parameters; - if (ignoreCaseMatch) { - op = "=~"; - parameters = Map.of("value", "(?i)" + AnyRepoExt.escapeForLikeRegex(POJOHelper.serialize(attr))); - } else { - op = "="; - parameters = Map.of("value", POJOHelper.serialize(attr)); - } - return toList( - neo4jClient.query( - "MATCH (n:" + AnyRepoExt.node(anyUtils.anyTypeKind()) + ") " - + "WHERE n.`plainAttrs." + schema.getKey() + "` " + op + " $value RETURN n.id"). - bindAll(parameters).fetch().all(), - "n.id", - anyUtils.anyClass(), - cache()); - } - - @Override - public Optional findByPlainAttrUniqueValue( - final PlainSchema schema, - final PlainAttrUniqueValue attrUniqueValue, - final boolean ignoreCaseMatch) { - - if (schema == null) { - LOG.error("No PlainSchema"); - return Optional.empty(); - } - if (!schema.isUniqueConstraint()) { - LOG.error("This schema has not unique constraint: '{}'", schema.getKey()); - return Optional.empty(); - } - - List result = findByPlainAttrValue(schema, attrUniqueValue, ignoreCaseMatch); - return result.isEmpty() - ? Optional.empty() - : Optional.of(result.get(0)); - } - @Override public List findByDerAttrValue(final DerSchema derSchema, final String value, final boolean ignoreCaseMatch) { - if (derSchema == null) { - LOG.error("No DerSchema"); - return List.of(); - } - - Parser parser = new Parser(derSchema.getExpression()); - - // Schema keys - List identifiers = new ArrayList<>(); - - // Literals - List literals = new ArrayList<>(); - - // Get schema keys and literals - for (Token token = parser.getNextToken(); token != null && StringUtils.isNotBlank(token.toString()); - token = parser.getNextToken()) { - - if (token.kind == ParserConstants.STRING_LITERAL) { - literals.add(token.toString().substring(1, token.toString().length() - 1)); - } - - if (token.kind == ParserConstants.IDENTIFIER) { - identifiers.add(token.toString()); - } - } - - // Sort literals in order to process later literals included into others - literals.sort((l1, l2) -> { - if (l1 == null && l2 == null) { - return 0; - } else if (l1 != null && l2 == null) { - return -1; - } else if (l1 == null) { - return 1; - } else if (l1.length() == l2.length()) { - return 0; - } else if (l1.length() > l2.length()) { - return -1; - } else { - return 1; - } - }); - - // Split value on provided literals - List attrValues = split(value, literals); - - if (attrValues.size() != identifiers.size()) { - LOG.error("Ambiguous JEXL expression resolution: literals and values have different size"); - return List.of(); - } - - // Contains used identifiers in order to avoid replications - Set used = new HashSet<>(); - - // Create several clauses: one for eanch identifiers - List clauses = new ArrayList<>(); - Map parameters = new HashMap<>(); - for (int i = 0; i < identifiers.size(); i++) { - if (!used.contains(identifiers.get(i))) { - // verify schema existence and get schema type - PlainSchema schema = plainSchemaDAO.findById(identifiers.get(i)).orElse(null); - - if (schema == null) { - LOG.error("Invalid schema '{}', ignoring", identifiers.get(i)); - } else { - PlainAttr attr = anyUtils.newPlainAttr(); - attr.setSchema(schema); - if (schema.isUniqueConstraint()) { - PlainAttrUniqueValue attrValue = anyUtils.newPlainAttrUniqueValue(); - attrValue.setStringValue(attrValues.get(i)); - attr.setUniqueValue(attrValue); - } else { - PlainAttrValue attrValue = anyUtils.newPlainAttrValue(); - attrValue.setStringValue(attrValues.get(i)); - attr.add(attrValue); - } - - String op; - if (ignoreCaseMatch) { - op = "=~"; - parameters.put( - identifiers.get(i), - "(?i)" + AnyRepoExt.escapeForLikeRegex(POJOHelper.serialize(attr))); - } else { - op = "="; - parameters.put(identifiers.get(i), POJOHelper.serialize(attr)); - } - clauses.add("n.`plainAttrs." + schema.getKey() + "` " + op + " $" + identifiers.get(i)); - - used.add(identifiers.get(i)); - } - } - } - - LOG.debug("Generated where clauses {}", clauses); - - return toList( - neo4jClient.query( - "MATCH (n:" + AnyRepoExt.node(anyUtils.anyTypeKind()) + ") " - + "WHERE " + clauses.stream().collect(Collectors.joining(" AND ")) - + " RETURN n.id"). - bindAll(parameters).fetch().all(), - "n.id", - anyUtils.anyClass(), - cache()); + return anyFinder.findByDerAttrValue(anyUtils.anyTypeKind(), derSchema, value, ignoreCaseMatch); } @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true) @@ -462,7 +294,8 @@ public List findByResourcesContaining(final ExternalResource resource) { protected void checkBeforeSave(final A any) { // check UNIQUE constraints any.getPlainAttrs().stream().filter(attr -> attr.getUniqueValue() != null).forEach(attr -> { - Optional other = findByPlainAttrUniqueValue(attr.getSchema(), attr.getUniqueValue(), false); + Optional other = anyFinder.findByPlainAttrUniqueValue( + anyUtils.anyTypeKind(), attr.getSchema(), attr.getUniqueValue()); if (other.isEmpty() || other.get().getKey().equals(any.getKey())) { LOG.debug("No duplicate value found for {}={}", attr.getSchema().getKey(), attr.getUniqueValue().getValueAsString()); diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyObjectRepoExtImpl.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyObjectRepoExtImpl.java index 55060cf293..b1bcfc7aa3 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyObjectRepoExtImpl.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyObjectRepoExtImpl.java @@ -51,6 +51,7 @@ import org.apache.syncope.core.persistence.api.entity.group.Group; import org.apache.syncope.core.persistence.api.entity.user.URelationship; import org.apache.syncope.core.persistence.api.utils.RealmUtils; +import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.neo4j.entity.EntityCacheKey; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jAnyType; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jAnyTypeClass; @@ -89,6 +90,7 @@ public AnyObjectRepoExtImpl( final DynRealmDAO dynRealmDAO, final UserDAO userDAO, final GroupDAO groupDAO, + final AnyFinder anyFinder, final Neo4jTemplate neo4jTemplate, final Neo4jClient neo4jClient, final NodeValidator nodeValidator, @@ -101,6 +103,7 @@ public AnyObjectRepoExtImpl( derSchemaDAO, virSchemaDAO, dynRealmDAO, + anyFinder, anyUtilsFactory.getInstance(AnyTypeKind.ANY_OBJECT), neo4jTemplate, neo4jClient); diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyRepoExt.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyRepoExt.java index 8aa602c186..b8a0dfcffc 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyRepoExt.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/AnyRepoExt.java @@ -27,9 +27,6 @@ import org.apache.syncope.core.persistence.api.entity.Any; import org.apache.syncope.core.persistence.api.entity.DerSchema; import org.apache.syncope.core.persistence.api.entity.ExternalResource; -import org.apache.syncope.core.persistence.api.entity.PlainAttrUniqueValue; -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.Schema; import org.apache.syncope.core.persistence.neo4j.entity.anyobject.Neo4jAMembership; import org.apache.syncope.core.persistence.neo4j.entity.anyobject.Neo4jAnyObject; @@ -79,11 +76,6 @@ static String membNode(final AnyTypeKind anyTypeKind) { A authFind(String key); - List findByPlainAttrValue(PlainSchema schema, PlainAttrValue attrValue, boolean ignoreCaseMatch); - - Optional findByPlainAttrUniqueValue( - PlainSchema schema, PlainAttrUniqueValue attrUniqueValue, boolean ignoreCaseMatch); - List findByDerAttrValue(DerSchema schema, String value, boolean ignoreCaseMatch); AllowedSchemas findAllowedSchemas(A any, Class reference); diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/GroupRepoExtImpl.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/GroupRepoExtImpl.java index 2c1a4789a1..8a748a00d3 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/GroupRepoExtImpl.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/GroupRepoExtImpl.java @@ -60,6 +60,7 @@ import org.apache.syncope.core.persistence.api.search.SearchCondConverter; import org.apache.syncope.core.persistence.api.search.SearchCondVisitor; import org.apache.syncope.core.persistence.api.utils.RealmUtils; +import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.neo4j.entity.EntityCacheKey; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jAnyType; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jAnyTypeClass; @@ -114,7 +115,8 @@ public GroupRepoExtImpl( final AnyMatchDAO anyMatchDAO, final UserDAO userDAO, final AnyObjectDAO anyObjectDAO, - final AnySearchDAO searchDAO, + final AnySearchDAO anySearchDAO, + final AnyFinder anyFinder, final SearchCondVisitor searchCondVisitor, final Neo4jTemplate neo4jTemplate, final Neo4jClient neo4jClient, @@ -128,6 +130,7 @@ public GroupRepoExtImpl( derSchemaDAO, virSchemaDAO, dynRealmDAO, + anyFinder, anyUtilsFactory.getInstance(AnyTypeKind.GROUP), neo4jTemplate, neo4jClient); @@ -135,7 +138,7 @@ public GroupRepoExtImpl( this.anyMatchDAO = anyMatchDAO; this.userDAO = userDAO; this.anyObjectDAO = anyObjectDAO; - this.anySearchDAO = searchDAO; + this.anySearchDAO = anySearchDAO; this.searchCondVisitor = searchCondVisitor; this.nodeValidator = nodeValidator; this.groupCache = groupCache; diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/UserRepoExtImpl.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/UserRepoExtImpl.java index 38366fa2f8..96379b9c8e 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/UserRepoExtImpl.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/UserRepoExtImpl.java @@ -53,6 +53,7 @@ import org.apache.syncope.core.persistence.api.entity.user.URelationship; import org.apache.syncope.core.persistence.api.entity.user.User; import org.apache.syncope.core.persistence.api.utils.RealmUtils; +import org.apache.syncope.core.persistence.common.dao.AnyFinder; import org.apache.syncope.core.persistence.neo4j.entity.EntityCacheKey; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jAnyTypeClass; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jExternalResource; @@ -105,6 +106,7 @@ public UserRepoExtImpl( final GroupDAO groupDAO, final DelegationDAO delegationDAO, final FIQLQueryDAO fiqlQueryDAO, + final AnyFinder anyFinder, final SecurityProperties securityProperties, final Neo4jTemplate neo4jTemplate, final Neo4jClient neo4jClient, @@ -118,6 +120,7 @@ public UserRepoExtImpl( derSchemaDAO, virSchemaDAO, dynRealmDAO, + anyFinder, anyUtilsFactory.getInstance(AnyTypeKind.USER), neo4jTemplate, neo4jClient); diff --git a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/AnySearchTest.java b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/AnySearchTest.java index d3db4ed3ce..4e79c75c32 100644 --- a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/AnySearchTest.java +++ b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/AnySearchTest.java @@ -141,6 +141,61 @@ public void orOfThree() { assertTrue(users.stream().anyMatch(u -> "puccini".equals(u.getUsername()))); } + @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.and(SearchCond.of(firstnameCond), SearchCond.of(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.and(SearchCond.of(firstnameCond), SearchCond.negate(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.and(SearchCond.of(fullnameCond), SearchCond.of(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.and(SearchCond.of(fullnameCond), SearchCond.negate(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); @@ -201,7 +256,7 @@ public void searchWithNotAttrCond() { fullnameLeafCond.setSchema("fullname"); fullnameLeafCond.setExpression("Giuseppe Verdi"); - SearchCond cond = SearchCond.getNotLeaf(fullnameLeafCond); + SearchCond cond = SearchCond.negate(fullnameLeafCond); assertTrue(cond.isValid()); List users = searchDAO.search(cond, AnyTypeKind.USER); @@ -219,7 +274,7 @@ public void searchWithNotAnyCond() { usernameLeafCond.setSchema("username"); usernameLeafCond.setExpression("verdi"); - SearchCond cond = SearchCond.getNotLeaf(usernameLeafCond); + SearchCond cond = SearchCond.negate(usernameLeafCond); assertTrue(cond.isValid()); List users = searchDAO.search(cond, AnyTypeKind.USER); @@ -235,7 +290,7 @@ public void searchCaseInsensitiveWithNotCondition() { fullnameLeafCond.setSchema("fullname"); fullnameLeafCond.setExpression("giuseppe verdi"); - SearchCond cond = SearchCond.getNotLeaf(fullnameLeafCond); + SearchCond cond = SearchCond.negate(fullnameLeafCond); assertTrue(cond.isValid()); List users = searchDAO.search(cond, AnyTypeKind.USER); @@ -410,7 +465,7 @@ public void searchByResource() { ResourceCond ws1 = new ResourceCond(); ws1.setResource("ws-target-resource-list-mappings-2"); - SearchCond searchCondition = SearchCond.and(SearchCond.getNotLeaf(ws2), SearchCond.of(ws1)); + SearchCond searchCondition = SearchCond.and(SearchCond.negate(ws2), SearchCond.of(ws1)); assertTrue(searchCondition.isValid()); List users = searchDAO.search(searchCondition, AnyTypeKind.USER); @@ -720,7 +775,7 @@ public void changePwdDate() { AnyCond changePwdDateCond = new AnyCond(AttrCond.Type.ISNULL); changePwdDateCond.setSchema("changePwdDate"); - SearchCond cond = SearchCond.and(SearchCond.getNotLeaf(statusCond), SearchCond.of(changePwdDateCond)); + SearchCond cond = SearchCond.and(SearchCond.negate(statusCond), SearchCond.of(changePwdDateCond)); assertTrue(cond.isValid()); List users = searchDAO.search(cond, AnyTypeKind.USER); @@ -737,7 +792,7 @@ public void issue202() { ws1.setResource("ws-target-resource-list-mappings-1"); SearchCond searchCondition = - SearchCond.and(SearchCond.getNotLeaf(ws2), SearchCond.getNotLeaf(ws1)); + SearchCond.and(SearchCond.negate(ws2), SearchCond.negate(ws1)); assertTrue(searchCondition.isValid()); List users = searchDAO.search(searchCondition, AnyTypeKind.USER); @@ -923,7 +978,7 @@ public void issueSYNCOPE1419() { loginDateCond.setSchema("loginDate"); loginDateCond.setExpression(LOGIN_DATE_VALUE); - SearchCond cond = SearchCond.getNotLeaf(loginDateCond); + SearchCond cond = SearchCond.negate(loginDateCond); assertTrue(cond.isValid()); List users = searchDAO.search(cond, AnyTypeKind.USER); diff --git a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/UserTest.java b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/UserTest.java index 6653097058..4ffd32334d 100644 --- a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/UserTest.java +++ b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/UserTest.java @@ -27,7 +27,6 @@ import java.time.OffsetDateTime; import java.util.List; -import java.util.Optional; import org.apache.syncope.common.lib.types.CipherAlgorithm; import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO; import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO; @@ -36,10 +35,7 @@ import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO; import org.apache.syncope.core.persistence.api.dao.SecurityQuestionDAO; import org.apache.syncope.core.persistence.api.dao.UserDAO; -import org.apache.syncope.core.persistence.api.entity.PlainSchema; import org.apache.syncope.core.persistence.api.entity.user.UMembership; -import org.apache.syncope.core.persistence.api.entity.user.UPlainAttrUniqueValue; -import org.apache.syncope.core.persistence.api.entity.user.UPlainAttrValue; import org.apache.syncope.core.persistence.api.entity.user.User; import org.apache.syncope.core.persistence.neo4j.AbstractTest; import org.apache.syncope.core.spring.security.Encryptor; @@ -141,35 +137,6 @@ public void findByInvalidDerAttrExpression() { derSchemaDAO.findById("noschema").orElseThrow(), "Antonio, Maria", false).isEmpty()); } - @Test - public void findByPlainAttrUniqueValue() { - UPlainAttrUniqueValue fullnameValue = entityFactory.newEntity(UPlainAttrUniqueValue.class); - fullnameValue.setStringValue("Gioacchino Rossini"); - - PlainSchema fullname = plainSchemaDAO.findById("fullname").orElseThrow(); - - Optional found = userDAO.findByPlainAttrUniqueValue(fullname, fullnameValue, false); - assertTrue(found.isPresent()); - - fullnameValue.setStringValue("Gioacchino ROSSINI"); - - found = userDAO.findByPlainAttrUniqueValue(fullname, fullnameValue, false); - assertFalse(found.isPresent()); - - found = userDAO.findByPlainAttrUniqueValue(fullname, fullnameValue, true); - assertTrue(found.isPresent()); - } - - @Test - public void findByPlainAttrBooleanValue() { - UPlainAttrValue coolValue = entityFactory.newEntity(UPlainAttrValue.class); - coolValue.setBooleanValue(true); - - List list = userDAO.findByPlainAttrValue( - plainSchemaDAO.findById("cool").orElseThrow(), coolValue, false); - assertEquals(1, list.size()); - } - @Test public void findByKey() { assertTrue(userDAO.findById("1417acbe-cbf6-4277-9372-e75e04f97000").isPresent()); diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/InboundMatcher.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/InboundMatcher.java index 78cdb46bd7..faf4ce71f3 100644 --- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/InboundMatcher.java +++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/InboundMatcher.java @@ -32,7 +32,6 @@ import org.apache.syncope.common.lib.to.Provision; import org.apache.syncope.common.lib.types.AnyTypeKind; import org.apache.syncope.common.lib.types.MatchType; -import org.apache.syncope.core.persistence.api.attrvalue.ParsingValidationException; import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO; import org.apache.syncope.core.persistence.api.dao.AnySearchDAO; import org.apache.syncope.core.persistence.api.dao.GroupDAO; @@ -51,9 +50,6 @@ import org.apache.syncope.core.persistence.api.entity.DerSchema; import org.apache.syncope.core.persistence.api.entity.ExternalResource; import org.apache.syncope.core.persistence.api.entity.Implementation; -import org.apache.syncope.core.persistence.api.entity.PlainAttrUniqueValue; -import org.apache.syncope.core.persistence.api.entity.PlainAttrValue; -import org.apache.syncope.core.persistence.api.entity.PlainSchema; import org.apache.syncope.core.persistence.api.entity.Realm; import org.apache.syncope.core.persistence.api.entity.VirSchema; import org.apache.syncope.core.persistence.api.entity.policy.InboundCorrelationRuleEntity; @@ -77,7 +73,6 @@ import org.identityconnectors.framework.common.objects.SyncDeltaBuilder; import org.identityconnectors.framework.common.objects.SyncDeltaType; import org.identityconnectors.framework.common.objects.SyncToken; -import org.identityconnectors.framework.common.objects.Uid; import org.identityconnectors.framework.common.objects.filter.FilterBuilder; import org.identityconnectors.framework.spi.SearchResultsHandler; import org.slf4j.Logger; @@ -304,29 +299,15 @@ public List matchByConnObjectKeyValue( } else if (intAttrName.getSchemaType() != null) { switch (intAttrName.getSchemaType()) { case PLAIN -> { - PlainAttrValue value = intAttrName.getSchema().isUniqueConstraint() - ? anyUtils.newPlainAttrUniqueValue() - : anyUtils.newPlainAttrValue(); - try { - value.parseValue((PlainSchema) intAttrName.getSchema(), finalConnObjectKeyValue); - } catch (ParsingValidationException e) { - LOG.error("While parsing provided {} {}", Uid.NAME, value, e); - value.setStringValue(finalConnObjectKeyValue); - } - - if (intAttrName.getSchema().isUniqueConstraint()) { - anyUtils.dao().findByPlainAttrUniqueValue((PlainSchema) intAttrName.getSchema(), - (PlainAttrUniqueValue) value, ignoreCaseMatch). - ifPresent(anys::add); - } else { - anys.addAll(anyUtils.dao().findByPlainAttrValue((PlainSchema) intAttrName.getSchema(), - value, ignoreCaseMatch)); - } + AttrCond attrCond = new AttrCond(ignoreCaseMatch ? AttrCond.Type.IEQ : AttrCond.Type.EQ); + attrCond.setSchema(intAttrName.getSchema().getKey()); + attrCond.setExpression(finalConnObjectKeyValue); + anys.addAll(anySearchDAO.search(SearchCond.of(attrCond), anyTypeKind)); } case DERIVED -> - anys.addAll(anyUtils.dao().findByDerAttrValue((DerSchema) intAttrName.getSchema(), - finalConnObjectKeyValue, ignoreCaseMatch)); + anys.addAll(anyUtils.dao().findByDerAttrValue( + (DerSchema) intAttrName.getSchema(), finalConnObjectKeyValue, ignoreCaseMatch)); default -> { } diff --git a/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/scim/SearchCondVisitor.java b/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/scim/SearchCondVisitor.java index 9aef821297..fdf6b8f48a 100644 --- a/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/scim/SearchCondVisitor.java +++ b/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/scim/SearchCondVisitor.java @@ -92,7 +92,7 @@ private static SearchCond setOperator(final AttrCond attrCond, final String oper } return "ne".equals(operator) - ? SearchCond.getNotLeaf(attrCond) + ? SearchCond.negate(attrCond) : SearchCond.of(attrCond); } @@ -353,7 +353,7 @@ public SearchCond visitNOT_EXPR(final SCIMFilterParser.NOT_EXPRContext ctx) { attrCond.get().setType(AnyCond.Type.ISNULL); } } else { - cond = SearchCond.getNotLeaf(cond); + cond = SearchCond.negate(cond); } } diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java index ec9247e9d6..b91250c28a 100644 --- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java +++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/SearchITCase.java @@ -984,46 +984,14 @@ public void issueSYNCOPE1727() { @Test public void issueSYNCOPE1779() { // 1. create user with underscore - UserTO userWithUnderscore = createUser(UserITCase.getSample("syncope1779_test@syncope.apache.org")).getEntity(); + UserTO userWith = createUser(UserITCase.getSample("syncope1779_test@syncope.apache.org")).getEntity(); // 2 create second user without underscore - createUser(UserITCase.getSample("syncope1779test@syncope.apache.org")).getEntity(); - - // 3. search for user - if (IS_EXT_SEARCH_ENABLED) { - try { - Thread.sleep(2000); - } catch (InterruptedException ex) { - // ignore - } - } - - // Search by username - PagedResult users = USER_SERVICE.search(new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM). - fiql(SyncopeClient.getUserSearchConditionBuilder().is("username").equalTo("syncope1779_*"). - and().is("firstname").equalTo("syncope1779_*"). - and().is("userId").equalTo("syncope1779_*").query()). - build()); - assertEquals(1, users.getResult().size()); - assertEquals(userWithUnderscore.getKey(), users.getResult().get(0).getKey()); - // Search also by attribute - users = USER_SERVICE.search(new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM). - fiql(SyncopeClient.getUserSearchConditionBuilder().is("email").equalTo("syncope1779_*").query()). - build()); - assertEquals(1, users.getResult().size()); - assertEquals(userWithUnderscore.getKey(), users.getResult().get(0).getKey()); - // search for both - users = USER_SERVICE.search(new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM). - fiql(SyncopeClient.getUserSearchConditionBuilder().is("email").equalTo("syncope1779*").query()). - build()); - assertEquals(2, users.getResult().size()); - - users.getResult().forEach(u -> USER_SERVICE.delete(u.getKey())); - - // 4. create any object with underscore + UserTO userWithout = createUser(UserITCase.getSample("syncope1779test@syncope.apache.org")).getEntity(); + // 3. create printer with underscore AnyObjectTO printer = createAnyObject( new AnyObjectCR.Builder(SyncopeConstants.ROOT_REALM, PRINTER, "_syncope1779").build()).getEntity(); - // 5. search for printer + // 4. search if (IS_EXT_SEARCH_ENABLED) { try { Thread.sleep(2000); @@ -1032,14 +1000,38 @@ public void issueSYNCOPE1779() { } } - PagedResult printers = ANY_OBJECT_SERVICE.search( - new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM). - fiql("$type==PRINTER;name==_syncope1779"). - build()); - assertEquals(1, printers.getResult().size()); - assertEquals(printer.getKey(), printers.getResult().get(0).getKey()); + try { + // Search by username + PagedResult users = USER_SERVICE.search(new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM). + fiql(SyncopeClient.getUserSearchConditionBuilder().is("username").equalTo("syncope1779_*"). + and().is("firstname").equalTo("syncope1779_*"). + and().is("userId").equalTo("syncope1779_*").query()). + build()); + assertEquals(1, users.getResult().size()); + assertEquals(userWith.getKey(), users.getResult().get(0).getKey()); + // Search also by attribute + users = USER_SERVICE.search(new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM). + fiql(SyncopeClient.getUserSearchConditionBuilder().is("email").equalTo("syncope1779_*").query()). + build()); + assertEquals(1, users.getResult().size()); + assertEquals(userWith.getKey(), users.getResult().get(0).getKey()); + // search for both + users = USER_SERVICE.search(new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM). + fiql(SyncopeClient.getUserSearchConditionBuilder().is("email").equalTo("syncope1779*").query()). + build()); + assertEquals(2, users.getResult().size()); - printers.getResult().forEach(u -> ANY_OBJECT_SERVICE.delete(u.getKey())); + // search for printer + PagedResult printers = ANY_OBJECT_SERVICE.search( + new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM). + fiql("$type==PRINTER;name==_syncope1779").build()); + assertEquals(1, printers.getResult().size()); + assertEquals(printer.getKey(), printers.getResult().get(0).getKey()); + } finally { + USER_SERVICE.delete(userWith.getKey()); + USER_SERVICE.delete(userWithout.getKey()); + ANY_OBJECT_SERVICE.delete(printer.getKey()); + } } @Test diff --git a/pom.xml b/pom.xml index 6092f99a9a..ca4f1e0e19 100644 --- a/pom.xml +++ b/pom.xml @@ -500,14 +500,14 @@ under the License. 10.1.33 35.0.0.Final - 6.2024.12 + 6.2025.1 4.1.2 17-alpine 9.0 11 23-slim-faststart - 5.26.0 + 5.26.1 42.7.5 9.2.0