From 84dfcf8ac779ed17f52e373233bd78054956548f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francesco=20Chicchiricc=C3=B2?= Date: Fri, 13 Nov 2026 13:29:53 +0100 Subject: [PATCH] Reviewing persistence implementations --- .../cxf/ThreadLocalCleanupOutInterceptor.java | 3 - .../SyncopeAbstractSearchCondVisitor.java | 6 +- .../persistence/api/search/SyncopePage.java | 8 +- .../persistence/api/utils/FormatUtils.java | 48 ++------ .../common/dao/AbstractAnyMatchDAO.java | 28 +++-- .../common/dao/AbstractAnySearchDAO.java | 12 +- .../common/dao/AbstractSearchDAO.java | 4 +- .../persistence/jpa/PersistenceContext.java | 5 +- .../jpa/dao/AbstractJPAAnySearchDAO.java | 3 +- .../jpa/dao/AbstractJPARealmSearchDAO.java | 45 ++++++-- .../persistence/jpa/dao/JPAAuditEventDAO.java | 48 ++++++-- .../jpa/dao/JPAEntityCacheDAO.java | 14 +-- .../core/persistence/jpa/dao/JPARealmDAO.java | 38 +++++-- .../core/persistence/jpa/dao/JPATaskDAO.java | 39 +++++-- .../jpa/dao/MySQLJPAAnySearchDAO.java | 15 ++- .../jpa/dao/MySQLJPARealmSearchDAO.java | 3 - .../jpa/dao/OracleJPAAnySearchDAO.java | 4 - .../jpa/dao/OracleJPARealmSearchDAO.java | 3 - .../persistence/jpa/dao/OrderBySupport.java | 5 - .../jpa/dao/PGJPAAnySearchDAO.java | 15 ++- .../jpa/dao/PGJPARealmSearchDAO.java | 13 +-- .../persistence/jpa/dao/SearchSupport.java | 10 +- .../jpa/dao/repo/GroupRepoExtImpl.java | 46 ++++++-- .../jpa/dao/repo/UserRepoExtImpl.java | 38 ++++++- .../DomainRoutingEntityManagerFactory.java | 6 +- .../src/main/resources/META-INF/indexes.xml | 11 ++ .../resources/META-INF/mariadb/indexes.xml | 11 ++ .../main/resources/META-INF/mysql/indexes.xml | 11 ++ .../resources/META-INF/oracle/indexes.xml | 11 ++ .../core/persistence/jpa/inner/RealmTest.java | 62 +++++++++++ .../neo4j/Neo4jDomainRegistry.java | 2 +- .../persistence/neo4j/PersistenceContext.java | 6 +- .../neo4j/content/ContentLoaderHandler.java | 34 ++++-- .../neo4j/content/XMLContentLoader.java | 9 +- .../persistence/neo4j/dao/AbstractDAO.java | 90 +++++++++------ .../neo4j/dao/Neo4jAnyChecker.java | 84 +++++++------- .../neo4j/dao/Neo4jAnySearchDAO.java | 35 +++--- .../neo4j/dao/Neo4jAuditEventDAO.java | 26 +++-- .../persistence/neo4j/dao/Neo4jBatchDAO.java | 10 +- .../neo4j/dao/Neo4jEntityCacheDAO.java | 5 +- .../persistence/neo4j/dao/Neo4jPolicyDAO.java | 5 +- .../persistence/neo4j/dao/Neo4jRealmDAO.java | 37 ++++++- .../neo4j/dao/Neo4jRealmSearchDAO.java | 20 ++-- .../persistence/neo4j/dao/Neo4jTaskDAO.java | 103 ++++++++++-------- .../neo4j/dao/Neo4jTaskExecDAO.java | 33 +++--- .../neo4j/dao/repo/AbstractAnyRepoExt.java | 25 ----- .../neo4j/dao/repo/AnyObjectRepoExtImpl.java | 4 +- .../neo4j/dao/repo/GroupRepoExtImpl.java | 56 ++++------ .../dao/repo/RelationshipTypeRepoExtImpl.java | 19 +++- .../neo4j/dao/repo/ReportExecRepoExtImpl.java | 2 +- .../neo4j/dao/repo/ReportRepoExtImpl.java | 5 +- .../neo4j/dao/repo/UserRepoExtImpl.java | 61 +++++------ .../neo4j/entity/AbstractAttributable.java | 8 +- .../Neo4jImplementationRelationship.java | 2 +- ...ihip.java => Neo4jSortedRelationship.java} | 2 +- .../neo4j/entity/SortedSetList.java | 22 ++-- .../Neo4jFormPropertyDefRelationship.java | 4 +- .../Neo4jMacroTaskCommandRelationship.java | 4 +- .../neo4j/spring/DomainRoutingDriver.java | 7 +- .../support/SyncopeNeo4jRepository.java | 15 ++- .../src/main/resources/META-INF/indexes.xml | 53 +++++++++ .../persistence/neo4j/inner/RealmTest.java | 62 +++++++++++ pom.xml | 2 +- 63 files changed, 911 insertions(+), 506 deletions(-) rename core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/{Neo4jSortedRelationsihip.java => Neo4jSortedRelationship.java} (93%) diff --git a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/ThreadLocalCleanupOutInterceptor.java b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/ThreadLocalCleanupOutInterceptor.java index 5e8b024ec53..57ff7f40fc8 100644 --- a/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/ThreadLocalCleanupOutInterceptor.java +++ b/core/idrepo/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/ThreadLocalCleanupOutInterceptor.java @@ -22,7 +22,6 @@ import org.apache.cxf.message.Message; import org.apache.cxf.phase.AbstractPhaseInterceptor; import org.apache.cxf.phase.Phase; -import org.apache.syncope.core.persistence.api.utils.FormatUtils; import org.identityconnectors.common.l10n.CurrentLocale; import org.identityconnectors.framework.impl.api.local.ThreadClassLoaderManager; import org.slf4j.MDC; @@ -38,8 +37,6 @@ public ThreadLocalCleanupOutInterceptor() { @Override public void handleMessage(final Message message) throws Fault { - FormatUtils.clear(); - ThreadClassLoaderManager.clearInstance(); CurrentLocale.clear(); diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SyncopeAbstractSearchCondVisitor.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SyncopeAbstractSearchCondVisitor.java index 82449226b9e..9e8266716e8 100644 --- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SyncopeAbstractSearchCondVisitor.java +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SyncopeAbstractSearchCondVisitor.java @@ -50,7 +50,7 @@ public abstract class SyncopeAbstractSearchCondVisitor extends AbstractSearchConditionVisitor { - protected static final ThreadLocal SEARCH_COND = new ThreadLocal<>(); + protected SearchCond searchCond; protected static AttrCond createAttrCond(final String schema) { AttrCond attrCond = SearchableFields.contains(schema) @@ -264,11 +264,11 @@ protected SearchCond visitCompound(final SearchCondition sc) { @Override public void visit(final SearchCondition sc) { - SEARCH_COND.set(sc.getStatement() == null ? visitCompound(sc) : visitPrimitive(sc)); + searchCond = sc.getStatement() == null ? visitCompound(sc) : visitPrimitive(sc); } @Override public SearchCond getQuery() { - return SEARCH_COND.get(); + return searchCond; } } diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SyncopePage.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SyncopePage.java index 8f877c52a8d..2ac9123f6ec 100644 --- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SyncopePage.java +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/search/SyncopePage.java @@ -47,9 +47,15 @@ public int getTotalPages() { @Override public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof SyncopePage other)) { + return false; + } return new EqualsBuilder(). appendSuper(super.equals(obj)). - append(total, total). + append(total, other.total). build(); } diff --git a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/utils/FormatUtils.java b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/utils/FormatUtils.java index 3bd6cfa0d95..a481b0d9ade 100644 --- a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/utils/FormatUtils.java +++ b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/utils/FormatUtils.java @@ -41,12 +41,6 @@ public final class FormatUtils { private static final String NO_CONVERSION_PATTERN = "#,##0.###"; - private static final ThreadLocal DECIMAL_FORMAT = ThreadLocal.withInitial(() -> { - DecimalFormat df = new DecimalFormat(NO_CONVERSION_PATTERN); - df.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ENGLISH)); - return df; - }); - public static final ZoneOffset DEFAULT_OFFSET = OffsetDateTime.now().getOffset(); public static String format(final TemporalAccessor temporal) { @@ -64,20 +58,9 @@ public static String format(final long number) { } public static String format(final long number, final String conversionPattern) { - DecimalFormat df = DECIMAL_FORMAT.get(); - - String previous = df.toPattern(); - if (!previous.equals(conversionPattern)) { - df.applyPattern(conversionPattern); - } - - String formatted = df.format(number); - - if (!previous.equals(conversionPattern)) { - df.applyPattern(previous); - } - - return formatted; + final DecimalFormat df = new DecimalFormat(conversionPattern); + df.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + return df.format(number); } public static String format(final double number) { @@ -85,20 +68,9 @@ public static String format(final double number) { } public static String format(final double number, final String conversionPattern) { - DecimalFormat df = DECIMAL_FORMAT.get(); - - String previous = df.toPattern(); - if (!previous.equals(conversionPattern)) { - df.applyPattern(conversionPattern); - } - - String formatted = df.format(number); - - if (!previous.equals(conversionPattern)) { - df.applyPattern(previous); - } - - return formatted; + final DecimalFormat df = new DecimalFormat(conversionPattern); + df.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + return df.format(number); } public static OffsetDateTime parseDate(final String source) @@ -141,15 +113,11 @@ public static OffsetDateTime parseDate(final String source, final String convers } public static Number parseNumber(final String source, final String conversionPattern) throws ParseException { - DecimalFormat df = DECIMAL_FORMAT.get(); - df.applyPattern(conversionPattern); + final DecimalFormat df = new DecimalFormat(conversionPattern); + df.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ENGLISH)); return df.parse(source); } - public static void clear() { - DECIMAL_FORMAT.remove(); - } - private FormatUtils() { // private empty constructor } diff --git a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAnyMatchDAO.java b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAnyMatchDAO.java index fb656bb5321..1caf2a2123a 100644 --- a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAnyMatchDAO.java +++ b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAnyMatchDAO.java @@ -23,11 +23,13 @@ import jakarta.validation.constraints.Min; import java.beans.PropertyDescriptor; import java.lang.annotation.Annotation; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.Optional; import java.util.Set; import java.util.regex.Pattern; @@ -181,12 +183,6 @@ public boolean matches(final T any, final SearchCond cond) { orElse(null)); } - if (match == null) { - match = cond.asLeaf(AttrCond.class). - map(leaf -> matches(any, leaf, not)). - orElse(null); - } - return BooleanUtils.toBoolean(match); } case AND -> { @@ -299,7 +295,7 @@ protected boolean matches( case LIKE, ILIKE -> { if (schema.getType().isStringClass()) { StringBuilder output = new StringBuilder(); - for (char c : cond.getExpression().toLowerCase().toCharArray()) { + for (char c : cond.getExpression().toLowerCase(Locale.ROOT).toCharArray()) { if (c == '%') { output.append(".*"); } else if (Character.isLetter(c)) { @@ -428,11 +424,19 @@ protected boolean matches(final Any any, final AnyCond cond, final boolean not) boolean foundBooleanMin = false; boolean foundBooleanMax = false; if (Integer.class.equals(pd.getPropertyType())) { - for (Annotation annotation : pd.getPropertyType().getAnnotations()) { - if (Min.class.equals(annotation.annotationType())) { - foundBooleanMin = ((Min) annotation).value() == 0; - } else if (Max.class.equals(annotation.annotationType())) { - foundBooleanMax = ((Max) annotation).value() == 1; + for (Class clazz = any.getClass(); clazz != null; clazz = clazz.getSuperclass()) { + try { + final Field field = clazz.getDeclaredField(pd.getName()); + for (final Annotation annotation : field.getAnnotations()) { + if (Min.class.equals(annotation.annotationType())) { + foundBooleanMin = ((Min) annotation).value() == 0; + } else if (Max.class.equals(annotation.annotationType())) { + foundBooleanMax = ((Max) annotation).value() == 1; + } + } + break; + } catch (final NoSuchFieldException e) { + // continue walking up the class hierarchy } } } diff --git a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAnySearchDAO.java b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAnySearchDAO.java index 52caba13a79..bec90e34dfc 100644 --- a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAnySearchDAO.java +++ b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractAnySearchDAO.java @@ -20,8 +20,10 @@ import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; @@ -272,11 +274,17 @@ protected List buildResult(final List raw, final AnyT map(key -> key instanceof Object[] array ? (String) (array)[0] : ((String) key)). toList(); + Map keyPositions = new HashMap<>(); + for (int i = 0; i < keys.size(); i++) { + keyPositions.put(keys.get(i), i); + } + // sort anys according to keys' sorting, as their ordering is same as raw, e.g. the actual query results List anys = anyUtilsFactory.getInstance(kind).dao().findByKeys(keys).stream(). - sorted(Comparator.comparing(any -> keys.indexOf(any.getKey()))).toList(); + sorted(Comparator.comparingInt(any -> keyPositions.getOrDefault(any.getKey(), -1))).toList(); - keys.stream().filter(key -> anys.stream().noneMatch(any -> key.equals(any.getKey()))). + Set foundKeys = anys.stream().map(Any::getKey).collect(Collectors.toSet()); + keys.stream().filter(key -> !foundKeys.contains(key)). forEach(key -> LOG.error("Could not find {} with id {}, even if returned by native query", kind, key)); return (List) anys; diff --git a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractSearchDAO.java b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractSearchDAO.java index cc61f446b17..36aa3009c91 100644 --- a/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractSearchDAO.java +++ b/core/persistence-common/src/main/java/org/apache/syncope/core/persistence/common/dao/AbstractSearchDAO.java @@ -197,7 +197,7 @@ protected List buildDerAttrValueConditions( // Contains used identifiers in order to avoid replications Set used = new HashSet<>(); - // Create several clauses: one for eanch identifiers + // Create several clauses: one for each identifier for (int i = 0; i < identifiers.size() && !used.contains(identifiers.get(i)); i++) { used.add(identifiers.get(i)); @@ -251,7 +251,7 @@ protected CheckResult check(final AnyCond cond, final Field field, fina PlainSchema schema = entityFactory.newEntity(PlainSchema.class); schema.setKey(field.getName()); for (AttrSchemaType attrSchemaType : AttrSchemaType.values()) { - if (field.getType().isAssignableFrom(attrSchemaType.getType())) { + if (attrSchemaType.getType().isAssignableFrom(field.getType())) { schema.setType(attrSchemaType); } } 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 c961a7e8fc6..a912bbc3657 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 @@ -239,8 +239,9 @@ public CommonEntityManagerFactoryConf commonEMFConf( + "FieldStrategies='" + "java.util.Locale=org.apache.syncope.core.persistence.jpa.openjpa.LocaleValueHandler'"); - jpaPropertyMap.put("openjpa.DataCache", "true"); - jpaPropertyMap.put("openjpa.QueryCache", "true"); + // bound the in-memory caches, otherwise they can grow unbounded on long-running installations + jpaPropertyMap.put("openjpa.DataCache", "true(CacheSize=1000,SoftReferenceSize=100)"); + jpaPropertyMap.put("openjpa.QueryCache", "true(CacheSize=1000,SoftReferenceSize=0)"); jpaPropertyMap.put("openjpa.RemoteCommitProvider", props.getRemoteCommitProvider()); 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 0530f2dd99c..c1a04920ee6 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 @@ -755,7 +755,7 @@ protected long doCount( fillWithParameters(countQuery, parameters); // 5. execute the query and return the result - return ((Number) countQuery.getSingleResult()).intValue(); + return ((Number) countQuery.getSingleResult()).longValue(); } protected abstract void parseOrderByForPlainSchema( @@ -773,7 +773,6 @@ protected void parseOrderByForField( final Sort.Order clause) { item.select = defaultSV(svs).alias() + '.' + fieldName; - item.where = StringUtils.EMPTY; item.orderBy = defaultSV(svs).alias() + '.' + fieldName + ' ' + clause.getDirection().name(); } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPARealmSearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPARealmSearchDAO.java index c50f37ff8aa..04ad2342dca 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPARealmSearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractJPARealmSearchDAO.java @@ -23,8 +23,10 @@ import jakarta.persistence.Query; import jakarta.persistence.TypedQuery; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -56,6 +58,8 @@ public abstract class AbstractJPARealmSearchDAO extends AbstractRealmSearchDAO { + protected static final int IN_CLAUSE_MAX_SIZE = 500; + protected record QueryInfo(RealmSearchNode node, Set plainSchemas) { } @@ -200,13 +204,13 @@ protected Optional getQuery(final SearchCond cond, final List node = cond.asLeaf(AnyCond.class) .map(anyCond -> getQuery(anyCond, not, parameters)) .or(() -> cond.asLeaf(AttrCond.class).map(attrCond -> { - CheckResult checked = check(attrCond); - AttrCondQuery query = getQuery(attrCond, not, checked, parameters); - if (query.addPlainSchemas()) { - plainSchemas.add(checked.schema().getKey()); - } - return query.node(); - })); + CheckResult checked = check(attrCond); + AttrCondQuery query = getQuery(attrCond, not, checked, parameters); + if (query.addPlainSchemas()) { + plainSchemas.add(checked.schema().getKey()); + } + return query.node(); + })); } if (node.isEmpty()) { @@ -423,8 +427,8 @@ protected String buildFrom(final Set plainSchemas, final OrderBySupport protected String buildWhere(final Set bases, final QueryInfo queryInfo, final List parameters) { String fullPaths = bases.stream(). map(base -> "r.fullPath=?" + setParameter(parameters, base) - + " OR r.fullPath LIKE ?" + setParameter( - parameters, SyncopeConstants.ROOT_REALM.equals(base) ? "/%" : base + "/%")). + + " OR r.fullPath LIKE ?" + setParameter( + parameters, SyncopeConstants.ROOT_REALM.equals(base) ? "/%" : base + "/%")). collect(Collectors.joining(" OR ")); RealmSearchNode root; @@ -481,7 +485,6 @@ protected void parseOrderByForField( final Sort.Order clause) { item.select = "r." + fieldName; - item.where = StringUtils.EMPTY; item.orderBy = "r." + fieldName + ' ' + clause.getDirection().name(); } @@ -580,9 +583,27 @@ protected List doSearch(final Set bases, final SearchCond cond, f @SuppressWarnings("unchecked") List keys = query.getResultList().stream(). map(key -> key instanceof Object[] array ? (array)[0] : key). + map(Object::toString). toList(); - return keys.stream().map(k -> entityManager.find(JPARealm.class, k)). - filter(Objects::nonNull).map(Realm.class::cast).toList(); + if (keys.isEmpty()) { + return List.of(); + } + + Map found = new HashMap<>(); + for (int i = 0; i < keys.size(); i += IN_CLAUSE_MAX_SIZE) { + List chunk = keys.subList(i, Math.min(i + IN_CLAUSE_MAX_SIZE, keys.size())); + entityManager.createQuery( + "SELECT e FROM " + JPARealm.class.getSimpleName() + " e WHERE e.id IN :keys", JPARealm.class). + setParameter("keys", chunk). + getResultList(). + forEach(realm -> found.put(realm.getKey(), realm)); + } + + return keys.stream(). + map(found::get). + filter(Objects::nonNull). + map(Realm.class::cast). + toList(); } } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAuditEventDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAuditEventDAO.java index 3b3cd719d22..1a1b22618b2 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAuditEventDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAAuditEventDAO.java @@ -22,7 +22,9 @@ import jakarta.persistence.Query; import java.time.OffsetDateTime; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; @@ -39,6 +41,8 @@ public class JPAAuditEventDAO extends AbstractAuditEventDAO implements AuditEventDAO { + protected static final int IN_CLAUSE_MAX_SIZE = 500; + protected static class AuditEventCriteriaBuilder { protected final StringBuilder query = new StringBuilder(); @@ -52,15 +56,16 @@ protected int setParameter(final List parameters, final Object parameter return parameters.size(); } - protected AuditEventCriteriaBuilder entityKey(final String entityKey) { + protected AuditEventCriteriaBuilder entityKey(final String entityKey, final List parameters) { if (entityKey != null) { checkEntityKey(entityKey); + String pattern = "%\"key\":\"" + entityKey + "\"%"; query.append(andIfNeeded()). - append("(beforeValue LIKE '%\"key\":\"").append(entityKey).append("\"%' OR "). - append("inputs LIKE '%\"key\":\"").append(entityKey).append("\"%' OR "). - append("output LIKE '%\"key\":\"").append(entityKey).append("\"%' OR "). - append("throwable LIKE '%\"key\":\"").append(entityKey).append("\"%')"); + append("(beforeValue LIKE ?").append(setParameter(parameters, pattern)).append(" ESCAPE '#'"). + append(" OR inputs LIKE ?").append(setParameter(parameters, pattern)).append(" ESCAPE '#'"). + append(" OR output LIKE ?").append(setParameter(parameters, pattern)).append(" ESCAPE '#'"). + append(" OR throwable LIKE ?").append(setParameter(parameters, pattern)).append(" ESCAPE '#')"); } return this; } @@ -142,8 +147,8 @@ public AuditEvent save(final AuditEvent auditEvent) { return entityManager.merge(auditEvent); } - protected AuditEventCriteriaBuilder criteriaBuilder(final String entityKey) { - return new AuditEventCriteriaBuilder().entityKey(entityKey); + protected AuditEventCriteriaBuilder criteriaBuilder(final String entityKey, final List parameters) { + return new AuditEventCriteriaBuilder().entityKey(entityKey, parameters); } protected void fillWithParameters(final Query query, final List parameters) { @@ -173,7 +178,7 @@ public long count( List parameters = new ArrayList<>(); String queryString = "SELECT COUNT(0)" + " FROM " + JPAAuditEvent.TABLE - + " WHERE" + criteriaBuilder(entityKey). + + " WHERE" + criteriaBuilder(entityKey, parameters). username(username, parameters). who(who, parameters). opEvent(type, category, subcategory, op, outcome, parameters). @@ -204,7 +209,7 @@ public List search( List parameters = new ArrayList<>(); String queryString = "SELECT id" + " FROM " + JPAAuditEvent.TABLE - + " WHERE" + criteriaBuilder(entityKey). + + " WHERE" + criteriaBuilder(entityKey, parameters). username(username, parameters). who(who, parameters). opEvent(type, category, subcategory, op, outcome, parameters). @@ -231,8 +236,27 @@ public List search( @SuppressWarnings("unchecked") List entries = query.getResultList(); - return entries.stream(). - map(row -> entityManager.find(JPAAuditEvent.class, row.toString())). - filter(Objects::nonNull).map(this::toAuditEventTO).toList(); + List ids = entries.stream().map(Object::toString).toList(); + + if (ids.isEmpty()) { + return List.of(); + } + + Map found = new HashMap<>(); + for (int i = 0; i < ids.size(); i += IN_CLAUSE_MAX_SIZE) { + List chunk = ids.subList(i, Math.min(i + IN_CLAUSE_MAX_SIZE, ids.size())); + entityManager.createQuery( + "SELECT e FROM " + JPAAuditEvent.class.getSimpleName() + " e WHERE e.id IN :ids", + JPAAuditEvent.class). + setParameter("ids", chunk). + getResultList(). + forEach(event -> found.put(event.getKey(), event)); + } + + return ids.stream(). + map(found::get). + filter(Objects::nonNull). + map(this::toAuditEventTO). + toList(); } } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAEntityCacheDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAEntityCacheDAO.java index e95eda19c45..c08f83d74b9 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAEntityCacheDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAEntityCacheDAO.java @@ -81,9 +81,9 @@ public Map getStatistics() { classMap.put("hits", cacheStats.getHitCount(className)); classMap.put("reads", cacheStats.getReadCount(className)); classMap.put("writes", cacheStats.getWriteCount(className)); - storeCache.put("total_hits", cacheStats.getTotalHitCount(className)); - storeCache.put("total_reads", cacheStats.getTotalReadCount(className)); - storeCache.put("total_writes", cacheStats.getTotalWriteCount(className)); + classMap.put("total_hits", cacheStats.getTotalHitCount(className)); + classMap.put("total_reads", cacheStats.getTotalReadCount(className)); + classMap.put("total_writes", cacheStats.getTotalWriteCount(className)); storeCacheDetails.add(classMap); }); @@ -109,10 +109,10 @@ public Map getStatistics() { queryStats.keys().forEach(queryKey -> { Map queryKeyMap = new LinkedHashMap<>(); queryKeyMap.put("query_key", queryKey.toString()); - queryCache.put("hits", queryStats.getHitCount(queryKey)); - queryCache.put("executions", queryStats.getExecutionCount(queryKey)); - queryCache.put("total_hits", queryStats.getTotalHitCount(queryKey)); - queryCache.put("total_executions", queryStats.getTotalExecutionCount(queryKey)); + queryKeyMap.put("hits", queryStats.getHitCount(queryKey)); + queryKeyMap.put("executions", queryStats.getExecutionCount(queryKey)); + queryKeyMap.put("total_hits", queryStats.getTotalHitCount(queryKey)); + queryKeyMap.put("total_executions", queryStats.getTotalExecutionCount(queryKey)); queryCacheDetails.add(queryKeyMap); }); diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java index 70b53a99371..88734b85f92 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPARealmDAO.java @@ -23,7 +23,9 @@ import jakarta.persistence.Query; import jakarta.persistence.TypedQuery; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import org.apache.commons.lang3.Strings; import org.apache.syncope.common.lib.SyncopeConstants; @@ -115,20 +117,40 @@ public Optional findById(final String key) { return Optional.ofNullable(entityManager.find(JPARealm.class, key)); } + protected boolean samePolicy(final Realm realm, final T policy) { + return (policy instanceof AccountPolicy + && realm.getAccountPolicy() == null || policy.equals(realm.getAccountPolicy())) + || (policy instanceof PasswordPolicy + && realm.getPasswordPolicy() == null || policy.equals(realm.getPasswordPolicy())); + } + protected List findSamePolicyChildren(final Realm realm, final T policy) { + // load the whole subtree at once, then walk it in memory: querying children at each level + // results in one query per node for each policy of the realm being changed / deleted + Map> children = new HashMap<>(); + realmSearchDAO.findDescendants(realm.getFullPath(), null).stream(). + filter(child -> child.getParent() != null). + filter(child -> !child.getKey().equals(realm.getKey())). + forEach(child -> children.computeIfAbsent(child.getParent().getKey(), k -> new ArrayList<>()). + add(child)); + List result = new ArrayList<>(); + collectSamePolicyChildren(realm.getKey(), policy, children, result); + return result; + } - realmSearchDAO.findChildren(realm).stream(). - filter(child -> (policy instanceof AccountPolicy - && child.getAccountPolicy() == null || policy.equals(child.getAccountPolicy())) - || (policy instanceof PasswordPolicy - && child.getPasswordPolicy() == null || policy.equals(child.getPasswordPolicy()))). + protected void collectSamePolicyChildren( + final String parentKey, + final T policy, + final Map> children, + final List result) { + + children.getOrDefault(parentKey, List.of()).stream(). + filter(child -> samePolicy(child, policy)). forEach(child -> { result.add(child); - result.addAll(findSamePolicyChildren(child, policy)); + collectSamePolicyChildren(child.getKey(), policy, children, result); }); - - return result; } @Override diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java index b2da95b3f46..5ff196eeeae 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPATaskDAO.java @@ -28,7 +28,9 @@ import java.lang.reflect.Field; import java.time.OffsetDateTime; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -72,6 +74,8 @@ public class JPATaskDAO implements TaskDAO { + protected static final int IN_CLAUSE_MAX_SIZE = 500; + protected static final Logger LOG = LoggerFactory.getLogger(TaskDAO.class); protected final RealmSearchDAO realmSearchDAO; @@ -478,8 +482,9 @@ public > List findAll( queryString.insert(0, "SELECT T.id FROM (").append(") T"); } - queryString.append(toOrderByStatement( - taskUtilsFactory.getInstance(type).getTaskEntity(), pageable.getSort().stream(), "")); + Class> taskEntity = taskUtilsFactory.getInstance(type).getTaskEntity(); + + queryString.append(toOrderByStatement(taskEntity, pageable.getSort().stream(), "")); Query query = entityManager.createNativeQuery(queryString.toString()); @@ -496,9 +501,30 @@ public > List findAll( @SuppressWarnings("unchecked") List raw = query.getResultList(); - raw.forEach(key -> findById(type, key.toString()).ifPresentOrElse( - task -> result.add((T) task), - () -> LOG.error("Could not find task with key {}, even if returned by native query", key))); + List keys = raw.stream().map(Object::toString).toList(); + + if (keys.isEmpty()) { + return List.of(); + } + + Map> found = new HashMap<>(); + for (int i = 0; i < keys.size(); i += IN_CLAUSE_MAX_SIZE) { + List chunk = keys.subList(i, Math.min(i + IN_CLAUSE_MAX_SIZE, keys.size())); + entityManager.createQuery( + "SELECT e FROM " + taskEntity.getSimpleName() + " e WHERE e.id IN :keys", taskEntity). + setParameter("keys", chunk). + getResultList(). + forEach(task -> found.put(task.getKey(), task)); + } + + keys.forEach(key -> { + Task task = found.get(key); + if (task == null) { + LOG.error("Could not find task with key {}, even if returned by native query", key); + } else { + result.add((T) task); + } + }); return result; } @@ -561,8 +587,7 @@ public void delete(final Task task) { @Override public void deleteAll(final ExternalResource resource, final TaskType type) { - findAll(type, resource, null, null, null, Pageable.unpaged()). - stream().map(Task::getKey).forEach(key -> delete(type, key)); + findAll(type, resource, null, null, null, Pageable.unpaged()).forEach(this::delete); } @Override 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 3aa62156eb6..5921b65dc60 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 @@ -72,9 +72,6 @@ protected void parseOrderByForPlainSchema( final PlainSchema schema, final String fieldName) { - // keep track of involvement of non-mandatory schemas in the order by clauses - obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition()); - obs.views.add(svs.field()); item.select = new StringBuilder().append(schema.isUniqueConstraint() @@ -88,7 +85,6 @@ protected void parseOrderByForPlainSchema( append(" AND usa.plainSchema ='").append(fieldName).append("'"). append(" LIMIT 1"). append(") AS ").append(fieldName).toString(); - item.where = "plainSchema = '" + fieldName + '\''; item.orderBy = fieldName + ' ' + clause.getDirection().name(); } @@ -196,7 +192,8 @@ protected AttrCondQuery getQuery( return new AttrCondQuery(true, new AnySearchNode.Leaf( svs.field(), "JSON_SEARCH(" - + "plainAttrs, 'one', '" + checked.schema().getKey() + "', NULL, '$[*].schema'" + + "plainAttrs, 'one', ?" + setParameter(parameters, checked.schema().getKey()) + + ", NULL, '$[*].schema'" + ") IS NOT NULL")); } @@ -204,7 +201,8 @@ protected AttrCondQuery getQuery( return new AttrCondQuery(true, new AnySearchNode.Leaf( svs.field(), "JSON_SEARCH(" - + "plainAttrs, 'one', '" + checked.schema().getKey() + "', NULL, '$[*].schema'" + + "plainAttrs, 'one', ?" + setParameter(parameters, checked.schema().getKey()) + + ", NULL, '$[*].schema'" + ") IS NULL")); } @@ -221,8 +219,9 @@ protected AttrCondQuery getQuery( return new AttrCondQuery(true, new AnySearchNode.Leaf( svs.field(), "JSON_CONTAINS(" - + "plainAttrs, '" + POJOHelper.serialize(List.of(container)).replace("'", "''") - + "')")); + + "plainAttrs, ?" + setParameter( + parameters, POJOHelper.serialize(List.of(container))) + + ")")); } else { AnySearchNode.Leaf node; if (not && checked.schema().isMultivalue()) { diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPARealmSearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPARealmSearchDAO.java index 46d5cf4f8cd..2837bfdada1 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPARealmSearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MySQLJPARealmSearchDAO.java @@ -90,13 +90,10 @@ protected void parseOrderByForPlainSchema( final PlainSchema schema, final String fieldName) { - // keep track of involvement of non-mandatory schemas in the order by clauses - obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition()); item.select = schema.getKey() + "." + (schema.isUniqueConstraint() ? "uniqueValue" : "valuez") + " AS " + schema.getKey(); - item.where = StringUtils.EMPTY; item.orderBy = fieldName + ' ' + clause.getDirection().name(); } 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 7c847053600..8cfc03840c1 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 @@ -99,15 +99,12 @@ protected void parseOrderByForPlainSchema( final PlainSchema schema, final String fieldName) { - // keep track of involvement of non-mandatory schemas in the order by clauses - obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition()); obs.views.add(svs.table()); item.select = schema.getKey() + "." + (schema.isUniqueConstraint() ? "uniqueValue" : "valuez") + " AS " + schema.getKey(); - item.where = StringUtils.EMPTY; item.orderBy = fieldName + ' ' + clause.getDirection().name(); } @@ -119,7 +116,6 @@ protected void parseOrderByForField( final Sort.Order clause) { item.select = svs.table().alias() + '.' + fieldName; - item.where = StringUtils.EMPTY; item.orderBy = svs.table().alias() + '.' + fieldName + ' ' + clause.getDirection().name(); } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPARealmSearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPARealmSearchDAO.java index 7ae1fd9f423..aed0d859f34 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPARealmSearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OracleJPARealmSearchDAO.java @@ -67,13 +67,10 @@ protected void parseOrderByForPlainSchema( final PlainSchema schema, final String fieldName) { - // keep track of involvement of non-mandatory schemas in the order by clauses - obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition()); item.select = schema.getKey() + "." + (schema.isUniqueConstraint() ? "uniqueValue" : "valuez") + " AS " + schema.getKey(); - item.where = StringUtils.EMPTY; item.orderBy = fieldName + ' ' + clause.getDirection().name(); } diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OrderBySupport.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OrderBySupport.java index 0f31c57fa85..e035ffc306f 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OrderBySupport.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/OrderBySupport.java @@ -29,13 +29,10 @@ public static class Item { protected String select; - protected String where; - protected String orderBy; protected boolean isEmpty() { return (select == null || select.isEmpty()) - && (where == null || where.isEmpty()) && (orderBy == null || orderBy.isEmpty()); } } @@ -44,6 +41,4 @@ protected boolean isEmpty() { protected List items = new ArrayList<>(); - protected boolean nonMandatorySchemas = false; - } 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 c1dae44bb86..dc05a084c7d 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 @@ -49,12 +49,16 @@ protected static String escapeForLikeRegex(final String input) { for (char toEscape : REGEX_CHARS.toCharArray()) { output = output.replace(String.valueOf(toEscape), "\\" + toEscape); } - return output.replace("'", "''"); + // a double quote would otherwise terminate the JSON path string; the single quote is doubled for SQL + return output.replace("\"", "\\\"").replace("'", "''"); } protected static String escapeIfString(final String value, final boolean isStr) { return isStr - ? new StringBuilder().append('"').append(value.replace("'", "''")).append('"').toString() + ? new StringBuilder().append('"'). + append(value.replace("\\", "\\\\").replace("\"", "\\\"").replace("'", "''")). + append('"'). + toString() : value; } @@ -100,13 +104,9 @@ protected void parseOrderByForPlainSchema( final PlainSchema schema, final String fieldName) { - // keep track of involvement of non-mandatory schemas in the order by clauses - obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition()); - obs.views.add(svs.table()); item.select = fieldName + " -> 0 AS " + fieldName; - item.where = StringUtils.EMPTY; item.orderBy = fieldName + ' ' + clause.getDirection().name(); } @@ -118,7 +118,6 @@ protected void parseOrderByForField( final Sort.Order clause) { item.select = svs.table().alias() + '.' + fieldName; - item.where = StringUtils.EMPTY; item.orderBy = svs.table().alias() + '.' + fieldName + ' ' + clause.getDirection().name(); } @@ -190,7 +189,7 @@ protected AnySearchNode.Leaf filJSONAttrQuery( if (StringUtils.containsAny(value, REGEX_CHARS) || lower) { clause.append(" like_regex \"^"). - append(escapeForLikeRegex(value).replace("'", "''")). + append(escapeForLikeRegex(value)). append("$\""); } else { clause.append(" == ").append(escapeIfString(value, isStr)); diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPARealmSearchDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPARealmSearchDAO.java index 7bac97a495c..eeb7a4e00b6 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPARealmSearchDAO.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPARealmSearchDAO.java @@ -46,11 +46,14 @@ protected static String escapeForLikeRegex(final String input) { for (char toEscape : REGEX_CHARS.toCharArray()) { output = output.replace(String.valueOf(toEscape), "\\" + toEscape); } - return output.replace("'", "''"); + // a double quote would otherwise terminate the JSON path string; the single quote is doubled for SQL + return output.replace("\"", "\\\"").replace("'", "''"); } protected static String escapeIfString(final String value, final boolean isStr) { - return isStr ? '"' + value.replace("'", "''") + '"' : value; + return isStr + ? '"' + value.replace("\\", "\\\\").replace("\"", "\\\"").replace("'", "''") + '"' + : value; } public PGJPARealmSearchDAO( @@ -73,11 +76,7 @@ protected void parseOrderByForPlainSchema( final PlainSchema schema, final String fieldName) { - // keep track of involvement of non-mandatory schemas in the order by clauses - obs.nonMandatorySchemas = !"true".equals(schema.getMandatoryCondition()); - item.select = fieldName + " -> 0 AS " + fieldName; - item.where = StringUtils.EMPTY; item.orderBy = fieldName + ' ' + clause.getDirection().name(); } @@ -143,7 +142,7 @@ protected RealmSearchNode.Leaf filJSONAttrQuery( append(" ? ("); if (StringUtils.containsAny(value, REGEX_CHARS) || lower) { clause.append("@.").append(key).append(" like_regex \"^"). - append(escapeForLikeRegex(value).replace("'", "''")).append("$\""); + append(escapeForLikeRegex(value)).append("$\""); } else if (isStr) { clause.append("@.").append(key).append(" == ").append(escapeIfString(value, true)); } else { diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/SearchSupport.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/SearchSupport.java index c5fbcdffacb..f47d83361cf 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/SearchSupport.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/SearchSupport.java @@ -31,8 +31,6 @@ public record SearchView(String alias, String name) { protected final AnyTypeKind anyTypeKind; - protected boolean nonMandatorySchemas = false; - public SearchSupport(final AnyTypeKind anyTypeKind) { this.anyTypeKind = anyTypeKind; } @@ -81,16 +79,16 @@ public SearchView field() { public SearchView relationship() { String kind = anyTypeKind == AnyTypeKind.USER ? "u" : anyTypeKind == AnyTypeKind.GROUP ? "g" : "a"; - return new SearchView("sv" + kind + 'm', field().name + '_' + kind + "relationship"); + return new SearchView("sv" + kind + "rel", field().name + '_' + kind + "relationship"); } public SearchView membership() { String kind = anyTypeKind == AnyTypeKind.USER ? "u" : "a"; - return new SearchView("sv" + kind + 'm', field().name + '_' + kind + "membership"); + return new SearchView("sv" + kind + "mem", field().name + '_' + kind + "membership"); } public SearchView role() { - return new SearchView("svr", field().name + "_role"); + return new SearchView("svrole", field().name + "_role"); } public SearchView auxClass() { @@ -98,7 +96,7 @@ public SearchView auxClass() { } public SearchView resource() { - return new SearchView("svr", field().name + "_resource"); + return new SearchView("svres", field().name + "_resource"); } public SearchView groupResource() { 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 8addba85d5d..bba515c036d 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 @@ -224,7 +224,7 @@ public List findAMembers(final String groupKey) { @Override public boolean existsAMembership(final String anyObjectKey, final String groupKey) { return query( - "SELECT COUNT(*) FROM " + JPAAMembership.TABLE + " WHERE group_id=? AND anyobject_it=?", + "SELECT COUNT(*) FROM " + JPAAMembership.TABLE + " WHERE group_id=? AND anyObject_id=?", rs -> { rs.next(); return rs.getLong(1); @@ -283,8 +283,17 @@ public List findManagedUsers(final String key) { result.addAll(query.getResultList()); // (b) see GroupDAO#findManagedUsers - findManagedGroupKeys(key).forEach(group -> findUMembers(group). - forEach(m -> userDAO.findById(m).ifPresent(result::add))); + List managedGroupKeys = findManagedGroupKeys(key).toList(); + if (!managedGroupKeys.isEmpty()) { + List memberKeys = entityManager.createQuery( + "SELECT DISTINCT m.leftEnd.id FROM " + JPAUMembership.class.getSimpleName() + + " m WHERE m.rightEnd.id IN :groupKeys", String.class). + setParameter("groupKeys", managedGroupKeys). + getResultList(); + if (!memberKeys.isEmpty()) { + result.addAll(userDAO.findByKeys(memberKeys)); + } + } return result.stream().distinct().toList(); } @@ -304,11 +313,19 @@ protected Stream findManagedGroupKeys(final String key) { @Override public List findManagedGroups(final String key) { - return findManagedGroupKeys(key). - map(group -> Optional.ofNullable(entityManager.find(JPAGroup.class, group))). - flatMap(Optional::stream). - map(Group.class::cast). - toList(); + List managedGroupKeys = findManagedGroupKeys(key).toList(); + if (managedGroupKeys.isEmpty()) { + return List.of(); + } + + Map found = new HashMap<>(); + entityManager.createQuery( + "SELECT e FROM " + JPAGroup.class.getSimpleName() + " e WHERE e.id IN :keys", Group.class). + setParameter("keys", managedGroupKeys). + getResultList(). + forEach(group -> found.put(group.getKey(), group)); + + return managedGroupKeys.stream().map(found::get).filter(Objects::nonNull).toList(); } @Override @@ -322,8 +339,17 @@ public List findManagedAnyObjects(final String key) { result.addAll(query.getResultList()); // (b) see GroupDAO#findManagedUsers - findManagedGroupKeys(key).forEach(group -> findAMembers(group). - forEach(m -> anyObjectDAO.findById(m).ifPresent(result::add))); + List managedGroupKeys = findManagedGroupKeys(key).toList(); + if (!managedGroupKeys.isEmpty()) { + List memberKeys = entityManager.createQuery( + "SELECT DISTINCT m.leftEnd.id FROM " + JPAAMembership.class.getSimpleName() + + " m WHERE m.rightEnd.id IN :groupKeys", String.class). + setParameter("groupKeys", managedGroupKeys). + getResultList(); + if (!memberKeys.isEmpty()) { + result.addAll(anyObjectDAO.findByKeys(memberKeys)); + } + } return result.stream().distinct().toList(); } 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 2f98374c110..c5fd9693057 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 @@ -168,8 +168,20 @@ public List findManagedUsers(final String key) { result.addAll(users.getResultList()); // (b) see UserDAO#findManagedUsers - findManagedGroupKeys(key).forEach(group -> groupDAO.findUMembers(group). - forEach(m -> Optional.ofNullable(entityManager.find(JPAUser.class, m)).ifPresent(result::add))); + List managedGroupKeys = findManagedGroupKeys(key).toList(); + if (!managedGroupKeys.isEmpty()) { + List memberKeys = entityManager.createQuery( + "SELECT DISTINCT m.leftEnd.id FROM " + JPAUMembership.class.getSimpleName() + + " m WHERE m.rightEnd.id IN :groupKeys", String.class). + setParameter("groupKeys", managedGroupKeys). + getResultList(); + if (!memberKeys.isEmpty()) { + result.addAll(entityManager.createQuery( + "SELECT e FROM " + JPAUser.class.getSimpleName() + " e WHERE e.id IN :keys", User.class). + setParameter("keys", memberKeys). + getResultList()); + } + } // (c) see UserDAO#findManagedUsers // (d) see UserDAO#findManagedUsers @@ -193,7 +205,10 @@ public List findManagedGroups(final String key) { List result = new ArrayList<>(); // (a) see UserDAO#findManagedGroups - findManagedGroupKeys(key).forEach(group -> groupDAO.findById(group).ifPresent(result::add)); + List managedGroupKeys = findManagedGroupKeys(key).toList(); + if (!managedGroupKeys.isEmpty()) { + result.addAll(groupDAO.findByKeys(managedGroupKeys)); + } // (b) see UserDAO#findManagedGroups findUMembershipGroups(key).forEach(g -> result.addAll(groupDAO.findManagedGroups(g))); @@ -212,8 +227,21 @@ public List findManagedAnyObjects(final String key) { result.addAll(anyObjects.getResultList()); // (b) see UserDAO#findManagedAnyObjects - findManagedGroupKeys(key).forEach(group -> groupDAO.findAMembers(group). - forEach(m -> Optional.ofNullable(entityManager.find(JPAAnyObject.class, m)).ifPresent(result::add))); + List managedGroupKeys = findManagedGroupKeys(key).toList(); + if (!managedGroupKeys.isEmpty()) { + List memberKeys = entityManager.createQuery( + "SELECT DISTINCT m.leftEnd.id FROM " + JPAAMembership.class.getSimpleName() + + " m WHERE m.rightEnd.id IN :groupKeys", String.class). + setParameter("groupKeys", managedGroupKeys). + getResultList(); + if (!memberKeys.isEmpty()) { + result.addAll(entityManager.createQuery( + "SELECT e FROM " + JPAAnyObject.class.getSimpleName() + + " e WHERE e.id IN :keys", AnyObject.class). + setParameter("keys", memberKeys). + getResultList()); + } + } Query query = entityManager.createNativeQuery( "SELECT DISTINCT group_id FROM " + JPAAMembership.TABLE + " WHERE anyObject_id=?"); diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/spring/DomainRoutingEntityManagerFactory.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/spring/DomainRoutingEntityManagerFactory.java index 2d71651551a..b780b09f887 100644 --- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/spring/DomainRoutingEntityManagerFactory.java +++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/spring/DomainRoutingEntityManagerFactory.java @@ -152,9 +152,9 @@ public void remove(final String domain) { } protected EntityManagerFactory delegate() { - return delegates.computeIfAbsent(AuthContextUtils.getDomain(), domain -> { - throw new IllegalStateException("Could not find EntityManagerFactory for domain " + domain); - }); + return Optional.ofNullable(delegates.get(AuthContextUtils.getDomain())). + orElseThrow(() -> new IllegalStateException( + "Could not find EntityManagerFactory for domain " + AuthContextUtils.getDomain())); } @Override diff --git a/core/persistence-jpa/src/main/resources/META-INF/indexes.xml b/core/persistence-jpa/src/main/resources/META-INF/indexes.xml index 26420dfd846..e2842b9c5a5 100644 --- a/core/persistence-jpa/src/main/resources/META-INF/indexes.xml +++ b/core/persistence-jpa/src/main/resources/META-INF/indexes.xml @@ -57,4 +57,15 @@ under the License. CREATE INDEX TaskExec4_TaskIdIndex ON NotificationTaskExec(task_id) CREATE INDEX TaskExec5_TaskIdIndex ON SchedTaskExec(task_id) CREATE INDEX ATPullTask_PullTaskIndex ON AnyTemplatePullTask(pullTask_id) + + CREATE INDEX AuditEvent_event_date ON AuditEvent(event_date) + CREATE INDEX AuditEvent_who ON AuditEvent(who) + + CREATE INDEX TaskExec1_EndIndex ON PropagationTaskExec(endDate) + CREATE INDEX TaskExec2_EndIndex ON PullTaskExec(endDate) + CREATE INDEX TaskExec3_EndIndex ON PushTaskExec(endDate) + CREATE INDEX TaskExec4_EndIndex ON NotificationTaskExec(endDate) + CREATE INDEX TaskExec5_EndIndex ON SchedTaskExec(endDate) + CREATE INDEX TaskExec6_EndIndex ON MacroTaskExec(endDate) + CREATE INDEX TaskExec7_EndIndex ON LiveSyncTaskExec(endDate) diff --git a/core/persistence-jpa/src/main/resources/META-INF/mariadb/indexes.xml b/core/persistence-jpa/src/main/resources/META-INF/mariadb/indexes.xml index 0db0870b871..fceecf7a70b 100644 --- a/core/persistence-jpa/src/main/resources/META-INF/mariadb/indexes.xml +++ b/core/persistence-jpa/src/main/resources/META-INF/mariadb/indexes.xml @@ -49,4 +49,15 @@ under the License. CREATE INDEX TaskExec4_TaskIdIndex ON NotificationTaskExec(task_id) CREATE INDEX TaskExec5_TaskIdIndex ON SchedTaskExec(task_id) CREATE INDEX ATPullTask_PullTaskIndex ON AnyTemplatePullTask(pullTask_id) + + CREATE INDEX AuditEvent_event_date ON AuditEvent(event_date) + CREATE INDEX AuditEvent_who ON AuditEvent(who) + + CREATE INDEX TaskExec1_EndIndex ON PropagationTaskExec(endDate) + CREATE INDEX TaskExec2_EndIndex ON PullTaskExec(endDate) + CREATE INDEX TaskExec3_EndIndex ON PushTaskExec(endDate) + CREATE INDEX TaskExec4_EndIndex ON NotificationTaskExec(endDate) + CREATE INDEX TaskExec5_EndIndex ON SchedTaskExec(endDate) + CREATE INDEX TaskExec6_EndIndex ON MacroTaskExec(endDate) + CREATE INDEX TaskExec7_EndIndex ON LiveSyncTaskExec(endDate) diff --git a/core/persistence-jpa/src/main/resources/META-INF/mysql/indexes.xml b/core/persistence-jpa/src/main/resources/META-INF/mysql/indexes.xml index 0db0870b871..fceecf7a70b 100644 --- a/core/persistence-jpa/src/main/resources/META-INF/mysql/indexes.xml +++ b/core/persistence-jpa/src/main/resources/META-INF/mysql/indexes.xml @@ -49,4 +49,15 @@ under the License. CREATE INDEX TaskExec4_TaskIdIndex ON NotificationTaskExec(task_id) CREATE INDEX TaskExec5_TaskIdIndex ON SchedTaskExec(task_id) CREATE INDEX ATPullTask_PullTaskIndex ON AnyTemplatePullTask(pullTask_id) + + CREATE INDEX AuditEvent_event_date ON AuditEvent(event_date) + CREATE INDEX AuditEvent_who ON AuditEvent(who) + + CREATE INDEX TaskExec1_EndIndex ON PropagationTaskExec(endDate) + CREATE INDEX TaskExec2_EndIndex ON PullTaskExec(endDate) + CREATE INDEX TaskExec3_EndIndex ON PushTaskExec(endDate) + CREATE INDEX TaskExec4_EndIndex ON NotificationTaskExec(endDate) + CREATE INDEX TaskExec5_EndIndex ON SchedTaskExec(endDate) + CREATE INDEX TaskExec6_EndIndex ON MacroTaskExec(endDate) + CREATE INDEX TaskExec7_EndIndex ON LiveSyncTaskExec(endDate) diff --git a/core/persistence-jpa/src/main/resources/META-INF/oracle/indexes.xml b/core/persistence-jpa/src/main/resources/META-INF/oracle/indexes.xml index 2f6b094fd62..c4a7ae323cf 100644 --- a/core/persistence-jpa/src/main/resources/META-INF/oracle/indexes.xml +++ b/core/persistence-jpa/src/main/resources/META-INF/oracle/indexes.xml @@ -57,4 +57,15 @@ under the License. CREATE INDEX TaskExec4_TaskIdIndex ON NotificationTaskExec(task_id) CREATE INDEX TaskExec5_TaskIdIndex ON SchedTaskExec(task_id) CREATE INDEX ATPullTask_PullTaskIndex ON AnyTemplatePullTask(pullTask_id) + + CREATE INDEX AuditEvent_event_date ON AuditEvent(event_date) + CREATE INDEX AuditEvent_who ON AuditEvent(who) + + CREATE INDEX TaskExec1_EndIndex ON PropagationTaskExec(endDate) + CREATE INDEX TaskExec2_EndIndex ON PullTaskExec(endDate) + CREATE INDEX TaskExec3_EndIndex ON PushTaskExec(endDate) + CREATE INDEX TaskExec4_EndIndex ON NotificationTaskExec(endDate) + CREATE INDEX TaskExec5_EndIndex ON SchedTaskExec(endDate) + CREATE INDEX TaskExec6_EndIndex ON MacroTaskExec(endDate) + CREATE INDEX TaskExec7_EndIndex ON LiveSyncTaskExec(endDate) diff --git a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RealmTest.java b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RealmTest.java index 2dd63410927..a6e94633414 100644 --- a/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RealmTest.java +++ b/core/persistence-jpa/src/test/java/org/apache/syncope/core/persistence/jpa/inner/RealmTest.java @@ -165,6 +165,68 @@ void saveNullParent() { } } + @Test + void renameAndMoveWithDescendants() { + Realm even = realmSearchDAO.findByFullPath("/even").orElseThrow(); + + // sibling whose name shares the prefix with "two", to prove descendant lookup is prefix-exact + Realm twob = entityFactory.newEntity(Realm.class); + twob.setName("twob"); + twob.setParent(even); + realmDAO.save(twob); + + Realm two = realmSearchDAO.findByFullPath("/even/two").orElseThrow(); + + Realm child = entityFactory.newEntity(Realm.class); + child.setName("child"); + child.setParent(two); + child = realmDAO.save(child); + + Realm grandChild = entityFactory.newEntity(Realm.class); + grandChild.setName("grandChild"); + grandChild.setParent(child); + grandChild = realmDAO.save(grandChild); + assertEquals("/even/two/child/grandChild", grandChild.getFullPath()); + + // rename: fullPath of all descendants is rewritten, sibling left alone + two.setName("due"); + realmDAO.save(two); + + assertTrue(realmSearchDAO.findByFullPath("/even/two").isEmpty()); + assertEquals("/even/due", realmSearchDAO.findByFullPath("/even/due").orElseThrow().getFullPath()); + assertEquals("/even/due/child", realmSearchDAO.findByFullPath("/even/due/child").orElseThrow().getFullPath()); + assertEquals( + "/even/due/child/grandChild", + realmSearchDAO.findByFullPath("/even/due/child/grandChild").orElseThrow().getFullPath()); + assertTrue(realmSearchDAO.findByFullPath("/even/two/child/grandChild").isEmpty()); + assertEquals("/even/twob", realmSearchDAO.findByFullPath("/even/twob").orElseThrow().getFullPath()); + + // descendants are cache-first: without eviction these would return the pre-rename fullPath + assertEquals("/even/due/child", realmDAO.findById(child.getKey()).orElseThrow().getFullPath()); + assertEquals( + "/even/due/child/grandChild", + realmDAO.findById(grandChild.getKey()).orElseThrow().getFullPath()); + + // move: the same subtree is rewritten under a different parent + Realm due = realmSearchDAO.findByFullPath("/even/due").orElseThrow(); + due.setParent(realmSearchDAO.findByFullPath("/odd").orElseThrow()); + realmDAO.save(due); + + assertTrue(realmSearchDAO.findByFullPath("/even/due").isEmpty()); + assertTrue(realmSearchDAO.findByFullPath("/even/due/child/grandChild").isEmpty()); + assertEquals("/odd/due", realmSearchDAO.findByFullPath("/odd/due").orElseThrow().getFullPath()); + assertEquals("/odd/due/child", realmSearchDAO.findByFullPath("/odd/due/child").orElseThrow().getFullPath()); + assertEquals( + "/odd/due/child/grandChild", + realmSearchDAO.findByFullPath("/odd/due/child/grandChild").orElseThrow().getFullPath()); + assertEquals( + "/odd/due/child/grandChild", + realmDAO.findById(grandChild.getKey()).orElseThrow().getFullPath()); + + // still untouched + assertEquals("/even/twob", realmSearchDAO.findByFullPath("/even/twob").orElseThrow().getFullPath()); + } + @Test void delete() { Realm realm = entityFactory.newEntity(Realm.class); diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/Neo4jDomainRegistry.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/Neo4jDomainRegistry.java index 3c998c9f22c..43da184c87d 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/Neo4jDomainRegistry.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/Neo4jDomainRegistry.java @@ -95,7 +95,7 @@ public void unregister(final String domain) { beanFactory().removeBeanDefinition(domain + "ContentXML"); // domainDriver - unregisterSingleton(domain + "Driver"); + unregisterSingleton(domain.toLowerCase() + "Driver"); beanFactory().getBean(DomainHolder.class).getDomains().remove(domain); } 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 bb662b43052..820916ec5e2 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 @@ -874,7 +874,8 @@ public EntityCacheDAO entityCacheDAO( final Cache plainSchemaCache, final Cache realmCache, final Cache roleCache, - final Cache userCache) { + final Cache userCache, + final Cache connInstanceCache) { return new Neo4jEntityCacheDAO( anyTypeCache, @@ -887,7 +888,8 @@ public EntityCacheDAO entityCacheDAO( plainSchemaCache, realmCache, roleCache, - userCache); + userCache, + connInstanceCache); } @ConditionalOnMissingBean diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/content/ContentLoaderHandler.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/content/ContentLoaderHandler.java index 491911aac94..76f25127c32 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/content/ContentLoaderHandler.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/content/ContentLoaderHandler.java @@ -142,6 +142,8 @@ protected static String escape(final String k) { protected final Neo4jMappingContext mappingContext; + protected final Session session; + public ContentLoaderHandler( final Driver driver, final Neo4jMappingContext mappingContext, @@ -152,11 +154,17 @@ public ContentLoaderHandler( super(rootElement, continueOnError, env); this.driver = driver; this.mappingContext = mappingContext; + this.session = driver.session(); + } + + @Override + public void endDocument() { + session.close(); } @Override protected void fetch(final Attributes atts) { - try (Session session = driver.session()) { + try { String value = session.run(atts.getValue("query")).single().get(0).asString(); String key = atts.getValue("key"); fetches.put(key, value); @@ -296,12 +304,22 @@ protected void create(final String qName, final Attributes atts) { if (qName.contains("_")) { String[] split = qName.split("_"); query = parseRelationship(mappingContext.getNodeDescription(split[0]), split[1], atts). - map(rel -> new Query( - "MATCH (a:" + split[0] + " {id: '" + rel.leftId() + "'}), " - + "(b:" + split[1] + " {id: '" + rel.rightId() + "'}) " - + "CREATE (a)-" - + "[:" + rel.type() + (rel.index() == null ? "" : " {index: " + rel.index() + "}") + "]->(b)", - Map.of())); + map(rel -> { + Map props = new HashMap<>(); + props.put("leftId", rel.leftId()); + props.put("rightId", rel.rightId()); + if (rel.index() != null) { + props.put("index", Integer.valueOf(rel.index())); + } + + return new Query( + "MATCH (a:" + split[0] + " {id: $leftId}), " + + "(b:" + split[1] + " {id: $rightId}) " + + "CREATE (a)-" + + "[:" + rel.type() + + (rel.index() == null ? "" : " {index: $index}") + "]->(b)", + props); + }); } else { query = parseNode(mappingContext.getNodeDescription(qName), atts).map(node -> { StringBuilder q = new StringBuilder("CREATE (n:").append(nodelabels(qName)).append(" {"); @@ -317,7 +335,7 @@ protected void create(final String qName, final Attributes atts) { query.ifPresent(q -> { LOG.debug("About to run: {}", q); - try (Session session = driver.session()) { + try { session.run(q.statement(), q.props()); } catch (Exception e) { LOG.error("While processing {}", qName, e); diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/content/XMLContentLoader.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/content/XMLContentLoader.java index e0e8e73c73f..be2112ab833 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/content/XMLContentLoader.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/content/XMLContentLoader.java @@ -91,8 +91,13 @@ protected void createIndexes(final String domain) throws IOException { protected void loadDefaultContent(final String domain, final String contentXML) throws Exception { InputStream in = ctx.getBeanFactory().getBean(contentXML, InputStream.class); try (in) { - saxParser().parse(in, new ContentLoaderHandler( - domainHolder.getDomains().get(domain), mappingContext, ROOT_ELEMENT, true, ctx.getEnvironment())); + ContentLoaderHandler handler = new ContentLoaderHandler( + domainHolder.getDomains().get(domain), mappingContext, ROOT_ELEMENT, true, ctx.getEnvironment()); + try { + saxParser().parse(in, handler); + } finally { + handler.endDocument(); + } LOG.debug("[{}] Default content successfully loaded", domain); } } diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/AbstractDAO.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/AbstractDAO.java index 255abf7ab32..55adbc02d15 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/AbstractDAO.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/AbstractDAO.java @@ -18,24 +18,23 @@ */ package org.apache.syncope.core.persistence.neo4j.dao; +import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.function.Function; import javax.cache.Cache; import org.apache.syncope.core.persistence.api.entity.Entity; import org.apache.syncope.core.persistence.neo4j.entity.AbstractNode; import org.apache.syncope.core.persistence.neo4j.entity.EntityCacheKey; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.Neo4jTemplate; public abstract class AbstractDAO { - protected static final Logger LOG = LoggerFactory.getLogger(AbstractDAO.class); - protected final Neo4jTemplate neo4jTemplate; protected final Neo4jClient neo4jClient; @@ -57,10 +56,10 @@ protected Optional findById( EntityCacheKey cacheKey = EntityCacheKey.of(key); return Optional.ofNullable(cache.get(cacheKey)). or(() -> neo4jTemplate.findById(key, domainType). - map(value -> { - cache.put(cacheKey, value); - return value; - })); + map(value -> { + cache.put(cacheKey, value); + return value; + })); } protected List findByRelationship( @@ -110,9 +109,47 @@ protected List toList( final Class domainType, final Cache cache) { - return result.stream(). - map(found -> findById(found.get(property).toString(), domainType, cache)). - flatMap(Optional::stream).map(n -> (E) n).toList(); + return findAllById( + result.stream().map(found -> found.get(property).toString()).toList(), + domainType, + cache); + } + + @SuppressWarnings("unchecked") + protected List findAllById( + final Collection keys, + final Class domainType, + final Cache cache) { + + if (keys.isEmpty()) { + return List.of(); + } + + List distinctKeys = keys.stream().distinct().toList(); + Map result = new HashMap<>(distinctKeys.size()); + List missing = new ArrayList<>(distinctKeys.size()); + + distinctKeys.forEach(key -> { + N cached = cache == null + ? null + : cache.get(EntityCacheKey.of(key)); + if (cached == null) { + missing.add(key); + } else { + result.put(key, cached); + } + }); + + if (!missing.isEmpty()) { + neo4jTemplate.findAllById(missing, domainType).forEach(node -> { + result.put(node.getKey(), node); + if (cache != null) { + cache.put(EntityCacheKey.of(node.getKey()), node); + } + }); + } + + return keys.stream().map(result::get).filter(Objects::nonNull).map(n -> (E) n).toList(); } protected void cascadeDelete( @@ -120,14 +157,10 @@ protected void cascadeDelete( final String rightNode, final String rightNodeKey) { - try { - neo4jClient.query( - "MATCH (n:" + leftNode + ")-[]-(p:" + rightNode + " {id: $id}) DETACH DELETE n"). - bindAll(Map.of("id", rightNodeKey)). - run(); - } catch (Exception e) { - LOG.error("While removing n via (n:{})-[]-(p:{} {id: {}})", leftNode, rightNode, rightNodeKey, e); - } + neo4jClient.query( + "MATCH (n:" + leftNode + ")-[]-(p:" + rightNode + " {id: $id}) DETACH DELETE n"). + bindAll(Map.of("id", rightNodeKey)). + run(); } protected void deleteRelationship( @@ -137,17 +170,12 @@ protected void deleteRelationship( final String rightNodeKey, final String relationshipType) { - try { - neo4jClient.query( - "MATCH (n:" + leftNode + " {id: $tid})-" - + "[r:" + relationshipType + "]-" - + "(p:" + rightNode + "{id: $iid}) " - + "DELETE r"). - bindAll(Map.of("tid", leftNodeKey, "iid", rightNodeKey)). - run(); - } catch (Exception e) { - LOG.error("While removing r via (n:{} {id: {}})-[r]-(p:{} {id: {}})", - leftNode, leftNodeKey, rightNode, rightNodeKey, e); - } + neo4jClient.query( + "MATCH (n:" + leftNode + " {id: $tid})-" + + "[r:" + relationshipType + "]-" + + "(p:" + rightNode + " {id: $iid}) " + + "DELETE r"). + bindAll(Map.of("tid", leftNodeKey, "iid", rightNodeKey)). + run(); } } diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAnyChecker.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAnyChecker.java index 0aed477c5a2..c8df53212f9 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAnyChecker.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAnyChecker.java @@ -95,44 +95,42 @@ public AllowedSchemas findAllowedSchemas(final Any any, fi switch (any) { case User user -> user.getMemberships().forEach(memb -> memb.getRightEnd().getTypeExtensions().forEach(typeExt -> { - Set classes = new HashSet<>(); + Set classes = gTypeExtensionClasses.computeIfAbsent( + memb.getRightEnd(), group -> new HashSet<>()); typeExt.getAuxClasses().forEach(atc -> anyTypeClassDAO.findById(atc.getKey()). ifPresent(classes::add)); - - gTypeExtensionClasses.put(memb.getRightEnd(), classes); })); case AnyObject anyObject -> anyObject.getMemberships().forEach(memb -> memb.getRightEnd().getTypeExtensions().stream(). filter(typeExt -> any.getType().equals(typeExt.getAnyType())).forEach(typeExt -> { - Set classes = new HashSet<>(); + Set classes = gTypeExtensionClasses.computeIfAbsent( + memb.getRightEnd(), group -> new HashSet<>()); typeExt.getAuxClasses().forEach(atc -> anyTypeClassDAO.findById(atc.getKey()). ifPresent(classes::add)); - - gTypeExtensionClasses.put(memb.getRightEnd(), classes); })); default -> { } } - gTypeExtensionClasses.entrySet().stream().peek( - entry -> result.memberships().put(entry.getKey(), new HashSet<>())). - forEach(entry -> entry.getValue().forEach(atc -> { - - if (reference.equals(PlainSchema.class)) { - atc.getPlainSchemas().stream(). - map(schema -> plainSchemaDAO.findById(schema.getKey())). - flatMap(Optional::stream). - forEach(schema -> result.memberships().get(entry.getKey()).add((S) schema)); - } else if (reference.equals(DerSchema.class)) { - atc.getDerSchemas().stream(). - map(schema -> derSchemaDAO.findById(schema.getKey())). - flatMap(Optional::stream). - forEach(schema -> result.memberships().get(entry.getKey()).add((S) schema)); - } - })); + gTypeExtensionClasses.forEach((group, classes) -> { + Set memberhips = result.memberships().computeIfAbsent(group, g -> new HashSet<>()); + classes.forEach(atc -> { + if (reference.equals(PlainSchema.class)) { + atc.getPlainSchemas().stream(). + map(schema -> plainSchemaDAO.findById(schema.getKey())). + flatMap(Optional::stream). + forEach(schema -> memberhips.add((S) schema)); + } else if (reference.equals(DerSchema.class)) { + atc.getDerSchemas().stream(). + map(schema -> derSchemaDAO.findById(schema.getKey())). + flatMap(Optional::stream). + forEach(schema -> memberhips.add((S) schema)); + } + }); + }); // schemas given by relationship type extensions Map> rTypeExtensionClasses = new HashMap<>(); @@ -140,44 +138,42 @@ public AllowedSchemas findAllowedSchemas(final Any any, fi switch (any) { case User user -> user.getRelationships().forEach(rel -> rel.getType().getTypeExtensions().forEach(typeExt -> { - Set classes = new HashSet<>(); + Set classes = rTypeExtensionClasses.computeIfAbsent( + rel.getType(), relationshipType -> new HashSet<>()); typeExt.getAuxClasses().forEach(atc -> anyTypeClassDAO.findById(atc.getKey()). ifPresent(classes::add)); - - rTypeExtensionClasses.put(rel.getType(), classes); })); case AnyObject anyObject -> anyObject.getRelationships().forEach(rel -> rel.getType().getTypeExtensions().stream(). filter(typeExt -> any.getType().equals(typeExt.getAnyType())).forEach(typeExt -> { - Set classes = new HashSet<>(); + Set classes = rTypeExtensionClasses.computeIfAbsent( + rel.getType(), relationshipType -> new HashSet<>()); typeExt.getAuxClasses().forEach(atc -> anyTypeClassDAO.findById(atc.getKey()). ifPresent(classes::add)); - - rTypeExtensionClasses.put(rel.getType(), classes); })); default -> { } } - rTypeExtensionClasses.entrySet().stream().peek( - entry -> result.relationshipTypes().put(entry.getKey(), new HashSet<>())). - forEach(entry -> entry.getValue().forEach(atc -> { - - if (reference.equals(PlainSchema.class)) { - atc.getPlainSchemas().stream(). - map(schema -> plainSchemaDAO.findById(schema.getKey())). - flatMap(Optional::stream). - forEach(schema -> result.relationshipTypes().get(entry.getKey()).add((S) schema)); - } else if (reference.equals(DerSchema.class)) { - atc.getDerSchemas().stream(). - map(schema -> derSchemaDAO.findById(schema.getKey())). - flatMap(Optional::stream). - forEach(schema -> result.relationshipTypes().get(entry.getKey()).add((S) schema)); - } - })); + rTypeExtensionClasses.forEach((relationshipType, classes) -> { + Set relatioships = result.relationshipTypes().computeIfAbsent(relationshipType, rt -> new HashSet<>()); + classes.forEach(atc -> { + if (reference.equals(PlainSchema.class)) { + atc.getPlainSchemas().stream(). + map(schema -> plainSchemaDAO.findById(schema.getKey())). + flatMap(Optional::stream). + forEach(schema -> relatioships.add((S) schema)); + } else if (reference.equals(DerSchema.class)) { + atc.getDerSchemas().stream(). + map(schema -> derSchemaDAO.findById(schema.getKey())). + flatMap(Optional::stream). + forEach(schema -> relatioships.add((S) schema)); + } + }); + }); return result; } diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAnySearchDAO.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAnySearchDAO.java index 992a06d1480..83a0612fb22 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAnySearchDAO.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAnySearchDAO.java @@ -181,11 +181,9 @@ protected AdminRealmsFilter getAdminRealmsFilter( final Map parameters) { return processRealms(base, recursive, adminRealms, realmKeys -> { - if (realmKeys.isEmpty()) { - return "(n)-[]-(:" + Neo4jRealm.NODE + ")"; - } + parameters.put("adminRealmKeys", realmKeys); - return "(n)-[]-(r:" + Neo4jRealm.NODE + ") WHERE r.id IN $" + setParameter(parameters, realmKeys); + return "(n)-[]-(r:" + Neo4jRealm.NODE + ") WHERE size($adminRealmKeys) = 0 OR r.id IN $adminRealmKeys"; }); } @@ -222,7 +220,7 @@ protected String getQuery( return "MATCH (n) " + "WHERE " + (not ? "NOT " : "") + "EXISTS { MATCH (n)-[]-(r:" + relTypeNode + ")-[]-" + "(t:" + Neo4jRelationshipType.NODE - + " {id: $ " + setParameter(parameters, cond.getRelationshipTypeKey()) + "}) } "; + + " {id: $" + setParameter(parameters, cond.getRelationshipTypeKey()) + "}) } "; } protected String getQuery( @@ -252,8 +250,14 @@ protected String getQuery( final boolean not, final Map parameters) { - cond.setGroup(cond.getGroup().replace("%", ".*")); - List groupKeys = check(cond); + String group = cond.getGroup(); + List groupKeys; + try { + cond.setGroup(group.replace("%", ".*")); + groupKeys = check(cond); + } finally { + cond.setGroup(group); + } String param = setParameter(parameters, groupKeys); return "MATCH (n) " @@ -472,11 +476,6 @@ protected void fillAttrQuery( query.append(')'); return; } - if (not && cond.getType() == AttrCond.Type.ISNULL) { - cond.setType(AttrCond.Type.ISNOTNULL); - fillAttrQuery(query, attrValue, schema, cond, true, parameters); - return; - } boolean lower = schema.getType().isStringClass() && (cond.getType() == AttrCond.Type.IEQ || cond.getType() == AttrCond.Type.ILIKE); @@ -572,16 +571,17 @@ protected AnyCondQuery getQuery( final Map parameters) { if (JAXRSService.PARAM_REALM.equals(cond.getSchema())) { - if (!SyncopeConstants.UUID_PATTERN.matcher(cond.getExpression()).matches()) { - Realm realm = realmSearchDAO.findByFullPath(cond.getExpression()). + String expression = cond.getExpression(); + if (!SyncopeConstants.UUID_PATTERN.matcher(expression).matches()) { + Realm realm = realmSearchDAO.findByFullPath(expression). orElseThrow(() -> new IllegalArgumentException( "Invalid Realm full path: " + cond.getExpression())); - cond.setExpression(realm.getKey()); + expression = realm.getKey(); } return new AnyCondQuery( "MATCH (n)-[]-" - + "(:" + Neo4jRealm.NODE + " {id: $" + setParameter(parameters, cond.getExpression()) + "}) ", + + "(:" + Neo4jRealm.NODE + " {id: $" + setParameter(parameters, expression) + "}) ", null); } @@ -861,7 +861,8 @@ protected void membershipAttrConds( map(mac -> "(EXISTS { " + mac.query() + "} )"). collect(Collectors.joining(" AND "))); - query.append(" AND EXISTS { (m)-[]-(r:Realm) WHERE r.id IN $param0 } "). + query.append(" AND EXISTS { (m)-[]-(r:").append(Neo4jRealm.NODE). + append(") WHERE size($adminRealmKeys) = 0 OR r.id IN $adminRealmKeys } "). append(returnStmt). append(" } "); } diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAuditEventDAO.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAuditEventDAO.java index c37947d7e1e..8077bf70642 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAuditEventDAO.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jAuditEventDAO.java @@ -22,8 +22,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; +import java.util.Objects; import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; import org.apache.syncope.common.lib.to.AuditEventTO; import org.apache.syncope.common.lib.types.OpEvent; @@ -53,11 +54,12 @@ protected AuditEventCriteriaBuilder entityKey(final String entityKey) { if (entityKey != null) { checkEntityKey(entityKey); + String quoted = Pattern.quote(entityKey); query.append(andIfNeeded()). - append("(n.before =~ '.*key.*").append(entityKey).append(".*' OR "). - append("n.inputs =~ '.*key.*").append(entityKey).append(".*' OR "). - append("n.output =~ '.*key.*").append(entityKey).append(".*' OR "). - append("n.throwable =~ '.*key.*").append(entityKey).append(".*')"); + append("(n.before =~ '.*key.*").append(quoted).append(".*' OR "). + append("n.inputs =~ '.*key.*").append(quoted).append(".*' OR "). + append("n.output =~ '.*key.*").append(quoted).append(".*' OR "). + append("n.throwable =~ '.*key.*").append(quoted).append(".*')"); } return this; } @@ -210,9 +212,17 @@ public List search( append(" LIMIT ").append(pageable.getPageSize()); } - return neo4jClient.query(query.toString()). + List keys = neo4jClient.query(query.toString()). bindAll(parameters).fetch().all().stream(). - map(found -> neo4jTemplate.findById(found.get("n.id"), Neo4jAuditEvent.class)). - flatMap(Optional::stream).map(this::toAuditEventTO).toList(); + map(found -> found.get("n.id").toString()).toList(); + if (keys.isEmpty()) { + return List.of(); + } + + Map found = new HashMap<>(); + neo4jTemplate.findAllById(keys, Neo4jAuditEvent.class). + forEach(auditEvent -> found.put(auditEvent.getKey(), auditEvent)); + + return keys.stream().map(found::get).filter(Objects::nonNull).map(this::toAuditEventTO).toList(); } } diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jBatchDAO.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jBatchDAO.java index 1ca887cd11a..f90185f73e9 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jBatchDAO.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jBatchDAO.java @@ -24,7 +24,6 @@ import java.util.Optional; import org.apache.syncope.core.persistence.api.dao.BatchDAO; import org.apache.syncope.core.persistence.api.entity.Batch; -import org.apache.syncope.core.persistence.neo4j.entity.Neo4jAccessToken; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jBatch; import org.apache.syncope.core.persistence.neo4j.spring.NodeValidator; import org.springframework.data.neo4j.core.Neo4jClient; @@ -91,12 +90,11 @@ public void deleteById(final String key) { @Override public long deleteExpired() { - Map result = neo4jClient.query( - "MATCH (n:" + Neo4jAccessToken.NODE + " WHERE n.expirationTime < $now) " + return neo4jClient.query( + "MATCH (n:" + Neo4jBatch.NODE + ") WHERE n.expiryTime < $now " + "DETACH DELETE n " + "RETURN count(*) AS deleted").bindAll(Map.of("now", OffsetDateTime.now())). - fetch().one().orElseGet(() -> Map.of("deleted", 0L)); - - return (long) result.getOrDefault(result.get("deleted"), 0L); + fetch().one().map(result -> ((Number) result.getOrDefault("deleted", 0L)).longValue()). + orElse(0L); } } diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jEntityCacheDAO.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jEntityCacheDAO.java index 44a76bfdc90..4243f363b12 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jEntityCacheDAO.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jEntityCacheDAO.java @@ -27,6 +27,7 @@ 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; +import org.apache.syncope.core.persistence.neo4j.entity.Neo4jConnInstance; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jDelegation; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jDerSchema; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jExternalResource; @@ -53,7 +54,8 @@ public Neo4jEntityCacheDAO( final Cache plainSchemaCache, final Cache realmCache, final Cache roleCache, - final Cache userCache) { + final Cache userCache, + final Cache connInstanceCache) { caches = new HashMap<>(); caches.put(Neo4jAnyTypeClass.class, anyTypeCache); @@ -67,6 +69,7 @@ public Neo4jEntityCacheDAO( caches.put(Neo4jRealm.class, realmCache); caches.put(Neo4jRole.class, roleCache); caches.put(Neo4jUser.class, userCache); + caches.put(Neo4jConnInstance.class, connInstanceCache); } @Override diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jPolicyDAO.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jPolicyDAO.java index 260a78fc6a4..faa80a9ec99 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jPolicyDAO.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jPolicyDAO.java @@ -123,8 +123,9 @@ public Neo4jPolicyDAO( @Override public boolean existsById(final String key) { - return neo4jTemplate.count( - "MATCH (n:" + Neo4jPolicy.NODE + ") WHERE n.id = $key RETURN count(n)", Map.of("key", key)) > 0; + return neo4jClient.query( + "MATCH (n:" + Neo4jPolicy.NODE + ") WHERE n.id = $key RETURN n.id LIMIT 1"). + bindAll(Map.of("key", key)).fetch().one().isPresent(); } @Override diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmDAO.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmDAO.java index 2701d0850f4..ecef6500d54 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmDAO.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmDAO.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; import javax.cache.Cache; import org.apache.commons.lang3.Strings; @@ -134,9 +135,9 @@ protected List findSamePolicyChildren(final Realm real realmSearchDAO.findChildren(realm).stream(). filter(child -> (policy instanceof AccountPolicy - && child.getAccountPolicy() == null || policy.equals(child.getAccountPolicy())) - || (policy instanceof PasswordPolicy - && child.getPasswordPolicy() == null || policy.equals(child.getPasswordPolicy()))). + && child.getAccountPolicy() == null || policy.equals(child.getAccountPolicy())) + || (policy instanceof PasswordPolicy + && child.getPasswordPolicy() == null || policy.equals(child.getPasswordPolicy()))). forEach(child -> { result.add(child); result.addAll(findSamePolicyChildren(child, policy)); @@ -243,8 +244,32 @@ public S save(final S realm) { S merged = neo4jTemplate.save(nodeValidator.validate(realm)); - if (!fullPathAfter.equals(fullPathBefore)) { - realmSearchDAO.findChildren(merged).forEach(this::save); + if (fullPathBefore != null && !fullPathAfter.equals(fullPathBefore)) { + String oldPrefix = Strings.CS.appendIfMissing(fullPathBefore, "/"); + String newPrefix = Strings.CS.appendIfMissing(fullPathAfter, "/"); + + List descendants = neo4jClient.query( + "MATCH (r:" + Neo4jRealm.NODE + ") WHERE r.fullPath STARTS WITH $oldPrefix RETURN r.id"). + bindAll(Map.of("oldPrefix", oldPrefix)). + fetch().all().stream(). + map(found -> found.get("r.id").toString()). + toList(); + + if (!descendants.isEmpty()) { + + neo4jClient.query( + "MATCH (r:" + Neo4jRealm.NODE + ") WHERE r.fullPath STARTS WITH $oldPrefix " + + "SET r.fullPath = $newPrefix + substring(r.fullPath, size($oldPrefix))"). + bindAll(Map.of("oldPrefix", oldPrefix, "newPrefix", newPrefix)). + run(); + + descendants.forEach(key -> cache.remove(EntityCacheKey.of(key))); + + // descendants' attributes are untouched, so no RealmChecker#checkBeforeSave is needed for them + findAllById(descendants, Neo4jRealm.class, cache).forEach(descendant -> publisher.publishEvent( + new EntityLifecycleEvent<>( + this, SyncDeltaType.UPDATE, descendant, AuthContextUtils.getDomain()))); + } } publisher.publishEvent( @@ -276,7 +301,7 @@ public void delete(final Realm realm) { toBeDeleted.setParent(null); - cache.remove(EntityCacheKey.of(realm.getKey())); + cache.remove(EntityCacheKey.of(toBeDeleted.getKey())); neo4jTemplate.deleteById(toBeDeleted.getKey(), Neo4jRealm.class); diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java index d4bf3ceb59b..269ce471e6d 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jRealmSearchDAO.java @@ -191,14 +191,16 @@ public List findDescendants(final String base, final String prefix) { Map parameters = new HashMap<>(); StringBuilder query = new StringBuilder("MATCH (n:").append(Neo4jRealm.NODE).append(") "). - append("WHERE (").append("n.fullPath = $base OR n.fullPath =~ $like").append(')'); + append("WHERE (n.fullPath = $base OR n.fullPath STARTS WITH $basePrefix)"); parameters.put("base", base); - parameters.put("like", SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base + "/.*"); + parameters.put("basePrefix", SyncopeConstants.ROOT_REALM.equals(base) ? SyncopeConstants.ROOT_REALM + : base + '/'); if (prefix != null) { - query.append(" AND (n.fullPath = $prefix OR n.fullPath =~ $likePrefix)"); + query.append(" AND (n.fullPath = $prefix OR n.fullPath STARTS WITH $prefixPrefix)"); parameters.put("prefix", prefix); - parameters.put("likePrefix", SyncopeConstants.ROOT_REALM.equals(prefix) ? "/.*" : prefix + "/.*"); + parameters.put("prefixPrefix", SyncopeConstants.ROOT_REALM.equals(prefix) + ? SyncopeConstants.ROOT_REALM : prefix + '/'); } query.append(" RETURN n.id ORDER BY n.fullPath"); @@ -307,8 +309,9 @@ protected void wrapQuery( String basesClause = bases.stream().map(base -> { int idx = index.incrementAndGet(); parameters.put("base" + idx, base); - parameters.put("like" + idx, SyncopeConstants.ROOT_REALM.equals(base) ? "/.*" : base + "/.*"); - return "n.fullPath = $base" + idx + " OR n.fullPath =~ $like" + idx; + parameters.put("basePrefix" + idx, SyncopeConstants.ROOT_REALM.equals(base) + ? SyncopeConstants.ROOT_REALM : base + '/'); + return "n.fullPath = $base" + idx + " OR n.fullPath STARTS WITH $basePrefix" + idx; }).collect(Collectors.joining(" OR ")); if (query.startsWith("MATCH (n)")) { query.replaceFirst("MATCH (n)", match + " WHERE (EXISTS { MATCH (n)"); @@ -516,11 +519,6 @@ protected void fillAttrQuery( query.append(')'); return; } - if (not && cond.getType() == AttrCond.Type.ISNULL) { - cond.setType(AttrCond.Type.ISNOTNULL); - fillAttrQuery(query, attrValue, schema, cond, true, parameters); - return; - } boolean lower = schema.getType().isStringClass() && (cond.getType() == AttrCond.Type.IEQ || cond.getType() == AttrCond.Type.ILIKE); diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jTaskDAO.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jTaskDAO.java index 7ad3170f401..b0c7a328f26 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jTaskDAO.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jTaskDAO.java @@ -47,6 +47,7 @@ import org.apache.syncope.core.persistence.api.entity.task.PushTask; import org.apache.syncope.core.persistence.api.entity.task.SchedTask; import org.apache.syncope.core.persistence.api.entity.task.Task; +import org.apache.syncope.core.persistence.api.entity.task.TaskExec; import org.apache.syncope.core.persistence.api.entity.task.TaskUtils; import org.apache.syncope.core.persistence.api.entity.task.TaskUtilsFactory; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jExternalResource; @@ -173,27 +174,28 @@ public Optional findByName(final TaskType type, final S @Override public Optional> findById(final String key) { - Optional> task = findById(TaskType.SCHEDULED, key); - if (task.isEmpty()) { - task = findById(TaskType.LIVE_SYNC, key); - } - if (task.isEmpty()) { - task = findById(TaskType.PULL, key); - } - if (task.isEmpty()) { - task = findById(TaskType.PUSH, key); - } - if (task.isEmpty()) { - task = findById(TaskType.MACRO, key); - } - if (task.isEmpty()) { - task = findById(TaskType.PROPAGATION, key); - } - if (task.isEmpty()) { - task = findById(TaskType.NOTIFICATION, key); + // concrete task types first: their labels are more specific than the shared SchedTask one + List types = new ArrayList<>(List.of(TaskType.values())); + types.remove(TaskType.SCHEDULED); + types.add(TaskType.SCHEDULED); + + StringBuilder query = new StringBuilder("CALL () { "); + for (int i = 0; i < types.size(); i++) { + if (i > 0) { + query.append(" UNION ALL "); + } + TaskType type = types.get(i); + query.append("MATCH (n:").append(taskUtilsFactory.getInstance(type).getTaskStorage()). + append(") WHERE n.id = $id RETURN ").append(i).append(" AS priority, '"). + append(type.name()).append("' AS type"); } + query.append(" } RETURN type, priority ORDER BY priority LIMIT 1"); - return task; + Optional type = neo4jClient.query(query.toString()). + bindAll(Map.of("id", key)).fetch().one(). + map(found -> TaskType.valueOf(found.get("type").toString())); + + return type.isEmpty() ? Optional.empty() : findById(type.get(), key); } @Override @@ -358,22 +360,19 @@ protected StringBuilder query( } List> relationships = new ArrayList<>(); - List properties = new ArrayList<>(); + List nodeConditions = new ArrayList<>(); - boolean optionalMatch = true; if (anyTypeKind != null) { - properties.add("n.anyTypeKind = $anyTypeKind"); + nodeConditions.add("n.anyTypeKind = $anyTypeKind"); parameters.put("anyTypeKind", anyTypeKind.name()); - optionalMatch = false; } if (entityKey != null) { - properties.add("n.entityKey = $entityKey"); + nodeConditions.add("n.entityKey = $entityKey"); parameters.put("entityKey", entityKey); - optionalMatch = false; } - relationships.add(Pair.of( - (optionalMatch ? "OPTIONAL " : "") + "MATCH (n)-[]-(p:" + taskUtils.getTaskExecStorage() + ")", null)); + // OPTIONAL: tasks without executions are legitimate search results + relationships.add(Pair.of("OPTIONAL MATCH (n)-[]-(p:" + taskUtils.getTaskExecStorage() + ")", null)); if (resource != null) { relationships.add(Pair.of("MATCH (n)-[]-(e:" + Neo4jExternalResource.NODE + " {id: $eid})", null)); @@ -399,7 +398,7 @@ protected StringBuilder query( String realmCond = realmKeys.map(realm -> { int idx = index.incrementAndGet(); parameters.put("realm" + idx, realm); - return "q.id: $realm," + idx; + return "q.id = $realm" + idx; }).collect(Collectors.joining(" OR ")); relationships.add(Pair.of("MATCH (n)-[]-(q:" + Neo4jRealm.NODE + ")", "(" + realmCond + ")")); @@ -408,17 +407,25 @@ protected StringBuilder query( StringBuilder query = new StringBuilder("MATCH (n:").append(taskUtils.getTaskStorage()).append(")"); if (type == TaskType.SCHEDULED) { - query.append( - " WHERE NOT n:" + Neo4jMacroTask.NODE - + " AND NOT n:" + Neo4jPullTask.NODE - + " AND NOT n:" + Neo4jPushTask.NODE); + nodeConditions.add("NOT n:" + Neo4jMacroTask.NODE); + nodeConditions.add("NOT n:" + Neo4jPullTask.NODE); + nodeConditions.add("NOT n:" + Neo4jPushTask.NODE); + } + + if (!nodeConditions.isEmpty()) { + // conditions on n only must be part of the first MATCH: inside OPTIONAL MATCH they would not filter n + // out, and the planner would not be able to use indexes on n + query.append(" WHERE ").append(String.join(" AND ", nodeConditions)); } query.append(relationships.stream().map(r -> " " + r.getLeft()).collect(Collectors.joining())); - properties.addAll(relationships.stream().filter(r -> r.getRight() != null).map(Pair::getRight).toList()); - if (!properties.isEmpty()) { - query.append(" WHERE ").append(String.join(" AND ", properties)); + List relatedConditions = relationships.stream(). + filter(r -> r.getRight() != null). + map(Pair::getRight). + toList(); + if (!relatedConditions.isEmpty()) { + query.append(" WHERE ").append(String.join(" AND ", relatedConditions)); } return query; @@ -497,7 +504,10 @@ public > List findAll( @Transactional(rollbackFor = { Throwable.class }) @Override public > T save(final T task) { - task.getExecs().forEach(exec -> neo4jTemplate.save(nodeValidator.validate(exec))); + List> execs = task.getExecs().stream().>map(nodeValidator::validate).toList(); + if (!execs.isEmpty()) { + neo4jTemplate.saveAll(execs); + } T saved = neo4jTemplate.save(nodeValidator.validate(task)); @@ -566,15 +576,18 @@ public void delete(final Task task) { switch (task) { case PullTask pullTask -> { remediationDAO.findByPullTask(pullTask).forEach(remediation -> remediation.setPullTask(null)); - pullTask.getTemplates(). - forEach(e -> neo4jTemplate.deleteById(e.getKey(), Neo4jAnyTemplatePullTask.class)); + neo4jTemplate.deleteAllById( + pullTask.getTemplates().stream().map(e -> e.getKey()).toList(), + Neo4jAnyTemplatePullTask.class); } case MacroTask macroTask -> { - macroTask.getCommands(). - forEach(e -> neo4jTemplate.deleteById(e.getKey(), Neo4jMacroTaskCommand.class)); - macroTask.getFormPropertyDefs(). - forEach(e -> neo4jTemplate.deleteById(e.getKey(), Neo4jFormPropertyDef.class)); + neo4jTemplate.deleteAllById( + macroTask.getCommands().stream().map(e -> e.getKey()).toList(), + Neo4jMacroTaskCommand.class); + neo4jTemplate.deleteAllById( + macroTask.getFormPropertyDefs().stream().map(e -> e.getKey()).toList(), + Neo4jFormPropertyDef.class); } default -> { @@ -583,7 +596,9 @@ public void delete(final Task task) { TaskUtils taskUtils = taskUtilsFactory.getInstance(task); - task.getExecs().forEach(exec -> neo4jTemplate.deleteById(exec.getKey(), taskUtils.getTaskExecEntity())); + neo4jTemplate.deleteAllById( + task.getExecs().stream().map(e -> e.getKey()).toList(), + taskUtils.getTaskExecEntity()); neo4jTemplate.deleteById(task.getKey(), taskUtils.getTaskEntity()); } @@ -624,9 +639,9 @@ public List purgePropagations( append(")"); } if (!CollectionUtils.isEmpty(resources)) { + AtomicInteger index = new AtomicInteger(0); query.append(" AND ("). append(resources.stream().map(r -> { - AtomicInteger index = new AtomicInteger(0); int idx = index.incrementAndGet(); parameters.put("rid" + idx, r); return "r.id = $rid" + idx; diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jTaskExecDAO.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jTaskExecDAO.java index f3ebbd9998c..87ae0307142 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jTaskExecDAO.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/Neo4jTaskExecDAO.java @@ -76,24 +76,25 @@ public > Optional> findById(final TaskType type, f @Override public Optional> findById(final String key) { - Optional> task = findById(TaskType.SCHEDULED, key); - if (task.isEmpty()) { - task = findById(TaskType.PULL, key); - } - if (task.isEmpty()) { - task = findById(TaskType.PUSH, key); - } - if (task.isEmpty()) { - task = findById(TaskType.MACRO, key); - } - if (task.isEmpty()) { - task = findById(TaskType.PROPAGATION, key); - } - if (task.isEmpty()) { - task = findById(TaskType.NOTIFICATION, key); + List types = List.of(TaskType.values()); + + StringBuilder query = new StringBuilder("CALL () { "); + for (int i = 0; i < types.size(); i++) { + if (i > 0) { + query.append(" UNION ALL "); + } + TaskType type = types.get(i); + query.append("MATCH (n:").append(taskUtilsFactory.getInstance(type).getTaskExecStorage()). + append(") WHERE n.id = $id RETURN ").append(i).append(" AS priority, '"). + append(type.name()).append("' AS type"); } + query.append(" } RETURN type, priority ORDER BY priority LIMIT 1"); + + Optional type = neo4jClient.query(query.toString()). + bindAll(Map.of("id", key)).fetch().one(). + map(found -> TaskType.valueOf(found.get("type").toString())); - return task; + return type.isEmpty() ? Optional.empty() : findById(type.get(), key); } @SuppressWarnings("unchecked") 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 ea7569315a7..803993828d3 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 @@ -20,11 +20,9 @@ import java.time.OffsetDateTime; import java.time.ZonedDateTime; -import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.regex.Pattern; import javax.cache.Cache; import org.apache.syncope.core.persistence.api.dao.AnyChecker; import org.apache.syncope.core.persistence.api.dao.AnyTypeClassDAO; @@ -52,29 +50,6 @@ public abstract class AbstractAnyRepoExt extends AbstractDAO implements AnyRepoExt { - /** - * 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.getFirst()))) { - if (!token.isEmpty()) { - attrValues.addAll(split(token, literals.subList(1, literals.size()))); - } - } - } - - return attrValues; - } - protected final AnyTypeDAO anyTypeDAO; protected final AnyTypeClassDAO anyTypeClassDAO; 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 dacb8ddb6e4..10d7f202502 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 @@ -266,12 +266,12 @@ public S save(final S anyObject) { Set beforeMembs = before.getMemberships().stream().map(AMembership::getKey). collect(Collectors.toSet()); beforeMembs.removeAll(anyObject.getMemberships().stream().map(AMembership::getKey).toList()); - beforeMembs.forEach(m -> neo4jTemplate.deleteById(m, Neo4jAMembership.class)); + neo4jTemplate.deleteAllById(beforeMembs, Neo4jAMembership.class); Set beforeRels = before.getRelationships().stream().map(ARelationship::getKey). collect(Collectors.toSet()); beforeRels.removeAll(anyObject.getRelationships().stream().map(ARelationship::getKey).toList()); - beforeRels.forEach(r -> neo4jTemplate.deleteById(r, Neo4jARelationship.class)); + neo4jTemplate.deleteAllById(beforeRels, Neo4jARelationship.class); }); S merged = neo4jTemplate.save(nodeValidator.validate(anyObject)); 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 482cdeb8a8f..6874dd00a74 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 @@ -197,12 +197,13 @@ public List findUMembers(final String groupKey) { @Override public boolean existsUMembership(final String userKey, final String groupKey) { - return neo4jTemplate.count( + return neo4jClient.query( "MATCH (u:" + Neo4jUser.NODE + " {id: $userKey})-[]-" + "(n:" + Neo4jUMembership.NODE + ")-[]-" + "(g:" + Neo4jGroup.NODE + " {id: $groupKey}) " - + "RETURN COUNT(n)", - Map.of("userKey", userKey, "groupKey", groupKey)) > 0; + + "RETURN n.id LIMIT 1"). + bindAll(Map.of("userKey", userKey, "groupKey", groupKey)). + fetch().one().isPresent(); } @Override @@ -244,12 +245,13 @@ public List findAMembers(final String groupKey) { @Override public boolean existsAMembership(final String anyObjectKey, final String groupKey) { - return neo4jTemplate.count( + return neo4jClient.query( "MATCH (a:" + Neo4jAnyObject.NODE + " {id: $anyObjectKey})-[]-" + "(n:" + Neo4jAMembership.NODE + ")-[]-" + "(g:" + Neo4jGroup.NODE + " {id: $groupKey}) " - + "RETURN COUNT(n)", - Map.of("anyObjectKey", anyObjectKey, "groupKey", groupKey)) > 0; + + "RETURN n.id LIMIT 1"). + bindAll(Map.of("anyObjectKey", anyObjectKey, "groupKey", groupKey)). + fetch().one().isPresent(); } @Override @@ -273,28 +275,13 @@ protected void securityChecks(final Group group) { @Override public boolean isManager(final String key) { - long users = neo4jTemplate.count( - "MATCH (n:" + Neo4jGroup.NODE + " {id: $key})-" - + "[:" + AbstractAny.GROUP_MANAGER_REL + "]-" - + "(p:" + Neo4jUser.NODE + ") " - + "RETURN COUNT(p.id)", - Map.of("key", key)); - - long groups = neo4jTemplate.count( - "MATCH (n:" + Neo4jGroup.NODE + " {id: $key})-" - + "[:" + AbstractAny.GROUP_MANAGER_REL + "]-" - + "(p:" + Neo4jGroup.NODE + ") " - + "RETURN COUNT(p.id)", - Map.of("key", key)); - - long anyObjects = neo4jTemplate.count( + return neo4jTemplate.count( "MATCH (n:" + Neo4jGroup.NODE + " {id: $key})-" + "[:" + AbstractAny.GROUP_MANAGER_REL + "]-" - + "(p:" + Neo4jAnyObject.NODE + ") " + + "(p) " + + "WHERE p:" + Neo4jUser.NODE + " OR p:" + Neo4jGroup.NODE + " OR p:" + Neo4jAnyObject.NODE + " " + "RETURN COUNT(p.id)", - Map.of("key", key)); - - return users + groups + anyObjects > 0; + Map.of("key", key)) > 0; } @Override @@ -312,8 +299,10 @@ public List findManagedUsers(final String key) { userCache)); // (b) see GroupDAO#findManagedUsers - findManagedGroupKeys(key).forEach(group -> findUMembers(group). - forEach(m -> userDAO.findById(m).ifPresent(result::add))); + result.addAll(findAllById( + findManagedGroupKeys(key).flatMap(group -> findUMembers(group).stream()).toList(), + Neo4jUser.class, + userCache)); return result.stream().distinct().toList(); } @@ -329,10 +318,7 @@ protected Stream findManagedGroupKeys(final String key) { @Override public List findManagedGroups(final String key) { - return findManagedGroupKeys(key).map(group -> findById(group, Neo4jGroup.class, groupCache)). - flatMap(Optional::stream). - map(Group.class::cast). - toList(); + return findAllById(findManagedGroupKeys(key).toList(), Neo4jGroup.class, groupCache); } @Override @@ -350,8 +336,10 @@ public List findManagedAnyObjects(final String key) { anyObjectCache)); // (b) see GroupDAO#findManagedUsers - findManagedGroupKeys(key).forEach(group -> findAMembers(group). - forEach(m -> anyObjectDAO.findById(m).ifPresent(result::add))); + result.addAll(findAllById( + findManagedGroupKeys(key).flatMap(group -> findAMembers(group).stream()).toList(), + Neo4jAnyObject.class, + anyObjectCache)); return result.stream().distinct().toList(); } @@ -397,7 +385,7 @@ public S save(final S group) { Set beforeTypeExts = before.getTypeExtensions().stream().map(GroupTypeExtension::getKey). collect(Collectors.toSet()); beforeTypeExts.removeAll(group.getTypeExtensions().stream().map(GroupTypeExtension::getKey).toList()); - beforeTypeExts.forEach(r -> neo4jTemplate.deleteById(r, Neo4jGroupTypeExtension.class)); + neo4jTemplate.deleteAllById(beforeTypeExts, Neo4jGroupTypeExtension.class); }); S merged = neo4jTemplate.save(nodeValidator.validate(group)); diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/RelationshipTypeRepoExtImpl.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/RelationshipTypeRepoExtImpl.java index 246439c8dff..8601d2b99c6 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/RelationshipTypeRepoExtImpl.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/RelationshipTypeRepoExtImpl.java @@ -89,7 +89,9 @@ public List findTypeExtensions(final AnyTypeClass any @Override public void deleteById(final String key) { neo4jTemplate.findById(key, Neo4jRelationshipType.class).ifPresent(type -> { - findRelationshipsByType(type).stream().peek(relationship -> { + Collection> relationships = findRelationshipsByType(type); + + relationships.forEach(relationship -> { switch (relationship) { case URelationship uRelationship -> uRelationship.getLeftEnd().remove(uRelationship); @@ -105,7 +107,20 @@ public void deleteById(final String key) { } } relationship.setLeftEnd(null); - }).forEach(r -> neo4jTemplate.deleteById(r.getKey(), r.getClass())); + }); + + neo4jTemplate.deleteAllById( + relationships.stream(). + filter(Neo4jURelationship.class::isInstance). + map(Relationship::getKey). + toList(), + Neo4jURelationship.class); + neo4jTemplate.deleteAllById( + relationships.stream(). + filter(Neo4jARelationship.class::isInstance). + map(Relationship::getKey). + toList(), + Neo4jARelationship.class); neo4jTemplate.deleteById(key, Neo4jRelationshipType.class); }); diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/ReportExecRepoExtImpl.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/ReportExecRepoExtImpl.java index 00c75903151..e77c4942a84 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/ReportExecRepoExtImpl.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/ReportExecRepoExtImpl.java @@ -56,7 +56,7 @@ public List findRecent(final int max) { + "[:" + Neo4jReport.REPORT_EXEC_REL + "]-" + "(p:" + Neo4jReport.NODE + " {id: $id}) " + "WHERE n.endDate IS NOT NULL " - + "RETURN n.id ORDER BY n.end DateDESC LIMIT " + max).fetch().all(), + + "RETURN n.id ORDER BY n.endDate DESC LIMIT " + max).fetch().all(), "n.id", Neo4jReportExec.class, null); diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/ReportRepoExtImpl.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/ReportRepoExtImpl.java index 62fd3ec7395..3e8bf7076dc 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/ReportRepoExtImpl.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/dao/repo/ReportRepoExtImpl.java @@ -19,6 +19,7 @@ package org.apache.syncope.core.persistence.neo4j.dao.repo; import org.apache.syncope.core.persistence.api.entity.Report; +import org.apache.syncope.core.persistence.api.entity.ReportExec; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jReport; import org.apache.syncope.core.persistence.neo4j.entity.Neo4jReportExec; import org.springframework.data.neo4j.core.Neo4jTemplate; @@ -38,7 +39,9 @@ public void deleteById(final String key) { @Override public void delete(final Report report) { - report.getExecs().forEach(exec -> neo4jTemplate.deleteById(exec.getKey(), Neo4jReportExec.class)); + neo4jTemplate.deleteAllById( + report.getExecs().stream().map(ReportExec::getKey).toList(), + Neo4jReportExec.class); neo4jTemplate.deleteById(report.getKey(), Neo4jReport.class); } 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 ab02096ff89..6b9a3b57ce6 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 @@ -72,7 +72,6 @@ import org.apache.syncope.core.spring.security.SecurityProperties; import org.springframework.data.neo4j.core.Neo4jClient; import org.springframework.data.neo4j.core.Neo4jTemplate; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; public class UserRepoExtImpl extends AbstractAnyRepoExt implements UserRepoExt { @@ -168,28 +167,13 @@ public Optional findLastChange(final String key) { @Override public boolean isManager(final String key) { - long users = neo4jTemplate.count( - "MATCH (n:" + Neo4jUser.NODE + " {id: $key})-" - + "[:" + AbstractAny.USER_MANAGER_REL + "]-" - + "(p:" + Neo4jUser.NODE + ") " - + "RETURN COUNT(p.id)", - Map.of("key", key)); - - long groups = neo4jTemplate.count( - "MATCH (n:" + Neo4jUser.NODE + " {id: $key})-" - + "[:" + AbstractAny.USER_MANAGER_REL + "]-" - + "(p:" + Neo4jGroup.NODE + ") " - + "RETURN COUNT(p.id)", - Map.of("key", key)); - - long anyObjects = neo4jTemplate.count( + return neo4jTemplate.count( "MATCH (n:" + Neo4jUser.NODE + " {id: $key})-" + "[:" + AbstractAny.USER_MANAGER_REL + "]-" - + "(p:" + Neo4jAnyObject.NODE + ") " + + "(p) " + + "WHERE p:" + Neo4jUser.NODE + " OR p:" + Neo4jGroup.NODE + " OR p:" + Neo4jAnyObject.NODE + " " + "RETURN COUNT(p.id)", - Map.of("key", key)); - - return users + groups + anyObjects > 0; + Map.of("key", key)) > 0; } protected Stream findUMembershipGroups(final String key) { @@ -216,8 +200,10 @@ public List findManagedUsers(final String key) { userCache)); // (b) see UserDAO#findManagedUsers - findManagedGroupKeys(key).forEach(group -> groupDAO.findUMembers(group). - forEach(m -> findById(m).ifPresent(result::add))); + result.addAll(findAllById( + findManagedGroupKeys(key).flatMap(group -> groupDAO.findUMembers(group).stream()).toList(), + Neo4jUser.class, + userCache)); // (c) see UserDAO#findManagedUsers // (d) see UserDAO#findManagedUsers @@ -240,7 +226,7 @@ public List findManagedGroups(final String key) { List result = new ArrayList<>(); // (a) see UserDAO#findManagedGroups - findManagedGroupKeys(key).forEach(group -> groupDAO.findById(group).ifPresent(result::add)); + result.addAll(findAllById(findManagedGroupKeys(key).toList(), Neo4jGroup.class, groupCache)); // (b) see UserDAO#findManagedGroups findUMembershipGroups(key).forEach(g -> result.addAll(groupDAO.findManagedGroups(g))); @@ -263,8 +249,10 @@ public List findManagedAnyObjects(final String key) { anyObjectCache)); // (b) see UserDAO#findManagedAnyObjects - findManagedGroupKeys(key).forEach(group -> groupDAO.findAMembers(group). - forEach(m -> findById(m, Neo4jAnyObject.class, anyObjectCache).ifPresent(result::add))); + result.addAll(findAllById( + findManagedGroupKeys(key).flatMap(group -> groupDAO.findAMembers(group).stream()).toList(), + Neo4jAnyObject.class, + anyObjectCache)); Collection> membershipQueryResult = neo4jClient.query( "MATCH (u:" + Neo4jAnyObject.NODE + " {id: $key})-[]-" @@ -407,17 +395,17 @@ public S save(final S user) { Set beforeMembs = before.getMemberships().stream().map(UMembership::getKey). collect(Collectors.toSet()); beforeMembs.removeAll(user.getMemberships().stream().map(UMembership::getKey).toList()); - beforeMembs.forEach(m -> neo4jTemplate.deleteById(m, Neo4jUMembership.class)); + neo4jTemplate.deleteAllById(beforeMembs, Neo4jUMembership.class); Set beforeRels = before.getRelationships().stream().map(URelationship::getKey). collect(Collectors.toSet()); beforeRels.removeAll(user.getRelationships().stream().map(URelationship::getKey).toList()); - beforeRels.forEach(r -> neo4jTemplate.deleteById(r, Neo4jURelationship.class)); + neo4jTemplate.deleteAllById(beforeRels, Neo4jURelationship.class); Set beforeLAs = before.getLinkedAccounts().stream().map(LinkedAccount::getKey). collect(Collectors.toSet()); beforeLAs.removeAll(user.getLinkedAccounts().stream().map(LinkedAccount::getKey).toList()); - beforeLAs.forEach(la -> neo4jTemplate.deleteById(la, Neo4jLinkedAccount.class)); + neo4jTemplate.deleteAllById(beforeLAs, Neo4jLinkedAccount.class); }); User merged = neo4jTemplate.save(nodeValidator.validate(user)); @@ -456,7 +444,7 @@ public void delete(final User user) { neo4jTemplate.deleteById(user.getKey(), Neo4jUser.class); } - @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true) + @Transactional(readOnly = true) @Override public Collection findAllRoles(final User user) { Set result = new HashSet<>(); @@ -465,7 +453,7 @@ public Collection findAllRoles(final User user) { return result; } - @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true) + @Transactional(readOnly = true) @Override public Collection findAllGroups(final User user) { Set result = new HashSet<>(); @@ -475,19 +463,19 @@ public Collection findAllGroups(final User user) { return result; } - @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true) + @Transactional(readOnly = true) @Override public Collection findAllGroupKeys(final User user) { return findAllGroups(user).stream().map(Group::getKey).toList(); } - @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true) + @Transactional(readOnly = true) @Override public Collection findAllGroupNames(final User user) { return findAllGroups(user).stream().map(Group::getName).toList(); } - @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true) + @Transactional(readOnly = true) @Override public Collection findAllResources(final User user) { Set result = new HashSet<>(); @@ -510,11 +498,12 @@ public boolean linkedAccountExists(final String userKey, final String connObject return false; } - return neo4jTemplate.count( + return neo4jClient.query( "MATCH (n:" + Neo4jUser.NODE + " {id: $id})-[]-(p:" + Neo4jLinkedAccount.NODE + ") " + "WHERE p.connObjectKeyValue = $connObjectKeyValue " - + "RETURN COUNT(p)", - Map.of("id", userKey, "connObjectKeyValue", connObjectKeyValue)) > 0; + + "RETURN p.id LIMIT 1"). + bindAll(Map.of("id", userKey, "connObjectKeyValue", connObjectKeyValue)). + fetch().one().isPresent(); } @Override diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/AbstractAttributable.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/AbstractAttributable.java index 9da6da7ecc8..c268579f23f 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/AbstractAttributable.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/AbstractAttributable.java @@ -62,12 +62,8 @@ protected void doComplete(final Map plainAttrs) { var entry = itor.next(); Optional.ofNullable(entry.getValue()).ifPresent(attr -> { attr.setSchema(entry.getKey()); - if (attr.getSchema() == null) { - itor.remove(); - } else { - attr.getValues().forEach(value -> value.setAttr(attr)); - Optional.ofNullable(attr.getUniqueValue()).ifPresent(value -> value.setAttr(attr)); - } + attr.getValues().forEach(value -> value.setAttr(attr)); + Optional.ofNullable(attr.getUniqueValue()).ifPresent(value -> value.setAttr(attr)); }); } } diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/Neo4jImplementationRelationship.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/Neo4jImplementationRelationship.java index f3aea17f861..b05475fff47 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/Neo4jImplementationRelationship.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/Neo4jImplementationRelationship.java @@ -27,7 +27,7 @@ @RelationshipProperties public class Neo4jImplementationRelationship - extends Neo4jSortedRelationsihip + extends Neo4jSortedRelationship implements Comparable { public static BiFunction builder() { diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/Neo4jSortedRelationsihip.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/Neo4jSortedRelationship.java similarity index 93% rename from core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/Neo4jSortedRelationsihip.java rename to core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/Neo4jSortedRelationship.java index d4d5156f4b8..75c5d71bffb 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/Neo4jSortedRelationsihip.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/Neo4jSortedRelationship.java @@ -20,7 +20,7 @@ import org.apache.syncope.core.persistence.api.entity.Entity; -public abstract class Neo4jSortedRelationsihip { +public abstract class Neo4jSortedRelationship { public abstract int getIndex(); diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/SortedSetList.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/SortedSetList.java index 5a679c82d3f..adaecb87219 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/SortedSetList.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/SortedSetList.java @@ -30,7 +30,7 @@ import java.util.stream.Stream; import org.apache.syncope.core.persistence.api.entity.Entity; -public class SortedSetList> implements List { +public class SortedSetList> implements List { private class SortedSetListIterator implements Iterator { @@ -81,12 +81,12 @@ public long estimateSize() { @Override public int characteristics() { - return ditor.characteristics(); + return ditor.characteristics() & ~Spliterator.SORTED; } @Override public Comparator getComparator() { - throw new UnsupportedOperationException("NOT FOR NOW"); + return null; } } @@ -130,28 +130,28 @@ public Spliterator spliterator() { @Override public Object[] toArray() { return delegate.stream(). - sorted(Comparator.comparing(Neo4jSortedRelationsihip::getIndex)). - map(Neo4jSortedRelationsihip::getEntity).toList().toArray(); + sorted(Comparator.comparing(Neo4jSortedRelationship::getIndex)). + map(Neo4jSortedRelationship::getEntity).toArray(); } @Override public T[] toArray(final T[] a) { return delegate.stream(). - sorted(Comparator.comparing(Neo4jSortedRelationsihip::getIndex)). - map(Neo4jSortedRelationsihip::getEntity).toList().toArray(a); + sorted(Comparator.comparing(Neo4jSortedRelationship::getIndex)). + map(Neo4jSortedRelationship::getEntity).toList().toArray(a); } @Override public boolean add(final E e) { return delegate.add(builder.apply( - delegate.stream().map(Neo4jSortedRelationsihip::getIndex). + delegate.stream().map(Neo4jSortedRelationship::getIndex). max(Comparator.naturalOrder()).orElse(0) + 1, e)); } @Override public boolean remove(final Object o) { - return delegate.removeIf(o::equals); + return delegate.removeIf(r -> r.getEntity().equals(o)); } @Override @@ -194,7 +194,7 @@ public E get(final int index) { for (Iterator itor = delegate.iterator(); idx <= index && itor.hasNext(); idx++) { - Neo4jSortedRelationsihip next = itor.next(); + Neo4jSortedRelationship next = itor.next(); if (idx == index) { return next.getEntity(); } @@ -205,7 +205,7 @@ public E get(final int index) { @Override public Stream stream() { - return delegate.stream().map(Neo4jSortedRelationsihip::getEntity); + return delegate.stream().map(Neo4jSortedRelationship::getEntity); } @Override diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/task/Neo4jFormPropertyDefRelationship.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/task/Neo4jFormPropertyDefRelationship.java index 45607c6c1c9..ae8a2c8466d 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/task/Neo4jFormPropertyDefRelationship.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/task/Neo4jFormPropertyDefRelationship.java @@ -22,14 +22,14 @@ import java.util.function.BiFunction; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; -import org.apache.syncope.core.persistence.neo4j.entity.Neo4jSortedRelationsihip; +import org.apache.syncope.core.persistence.neo4j.entity.Neo4jSortedRelationship; import org.springframework.data.neo4j.core.schema.RelationshipId; import org.springframework.data.neo4j.core.schema.RelationshipProperties; import org.springframework.data.neo4j.core.schema.TargetNode; @RelationshipProperties public class Neo4jFormPropertyDefRelationship - extends Neo4jSortedRelationsihip + extends Neo4jSortedRelationship implements Comparable { public static BiFunction builder() { diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/task/Neo4jMacroTaskCommandRelationship.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/task/Neo4jMacroTaskCommandRelationship.java index a8035535d8a..0dc8ba6395e 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/task/Neo4jMacroTaskCommandRelationship.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/entity/task/Neo4jMacroTaskCommandRelationship.java @@ -21,14 +21,14 @@ import java.util.function.BiFunction; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; -import org.apache.syncope.core.persistence.neo4j.entity.Neo4jSortedRelationsihip; +import org.apache.syncope.core.persistence.neo4j.entity.Neo4jSortedRelationship; import org.springframework.data.neo4j.core.schema.RelationshipId; import org.springframework.data.neo4j.core.schema.RelationshipProperties; import org.springframework.data.neo4j.core.schema.TargetNode; @RelationshipProperties public class Neo4jMacroTaskCommandRelationship - extends Neo4jSortedRelationsihip + extends Neo4jSortedRelationship implements Comparable { public static BiFunction builder() { diff --git a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/spring/DomainRoutingDriver.java b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/spring/DomainRoutingDriver.java index 027e1707d34..6f61b9dd749 100644 --- a/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/spring/DomainRoutingDriver.java +++ b/core/persistence-neo4j/src/main/java/org/apache/syncope/core/persistence/neo4j/spring/DomainRoutingDriver.java @@ -37,9 +37,12 @@ public DomainRoutingDriver(final DomainHolder domainHolder) { } protected Driver delegate() { - return domainHolder.getDomains().computeIfAbsent(AuthContextUtils.getDomain(), domain -> { + final String domain = AuthContextUtils.getDomain(); + final Driver driver = domainHolder.getDomains().get(domain); + if (driver == null) { throw new IllegalStateException("Could not find Driver for domain " + domain); - }); + } + return driver; } @Override diff --git a/core/persistence-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SyncopeNeo4jRepository.java b/core/persistence-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SyncopeNeo4jRepository.java index e0e46f0dbae..5c8b2efd512 100644 --- a/core/persistence-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SyncopeNeo4jRepository.java +++ b/core/persistence-neo4j/src/main/java/org/springframework/data/neo4j/repository/support/SyncopeNeo4jRepository.java @@ -34,7 +34,7 @@ public class SyncopeNeo4jRepository extends SimpleNeo4jRepository protected final Neo4jPersistentEntity entityMetaData; - private NodeValidator nodeValidator; + private volatile NodeValidator nodeValidator; public SyncopeNeo4jRepository( final Neo4jOperations neo4jOperations, @@ -47,12 +47,17 @@ public SyncopeNeo4jRepository( } protected NodeValidator nodeValidator() { - synchronized (this) { - if (nodeValidator == null) { - nodeValidator = ApplicationContextProvider.getApplicationContext().getBean(NodeValidator.class); + NodeValidator validator = nodeValidator; + if (validator == null) { + synchronized (this) { + validator = nodeValidator; + if (validator == null) { + validator = ApplicationContextProvider.getApplicationContext().getBean(NodeValidator.class); + nodeValidator = validator; + } } } - return nodeValidator; + return validator; } @Override diff --git a/core/persistence-neo4j/src/main/resources/META-INF/indexes.xml b/core/persistence-neo4j/src/main/resources/META-INF/indexes.xml index 849e62e62d8..d0a264bb73f 100644 --- a/core/persistence-neo4j/src/main/resources/META-INF/indexes.xml +++ b/core/persistence-neo4j/src/main/resources/META-INF/indexes.xml @@ -65,4 +65,57 @@ under the License. CREATE INDEX SyncopeUser_id FOR (n:SyncopeUser) ON (n.id) CREATE CONSTRAINT SyncopeUser_username FOR (n:SyncopeUser) REQUIRE n.username IS UNIQUE + CREATE INDEX SyncopeUser_token FOR (n:SyncopeUser) ON (n.token) + + CREATE INDEX Realm_name FOR (n:Realm) ON (n.name) + + CREATE INDEX ExternalResource_id FOR (n:ExternalResource) ON (n.id) + CREATE INDEX SyncopeRole_id FOR (n:SyncopeRole) ON (n.id) + CREATE INDEX Implementation_id FOR (n:Implementation) ON (n.id) + CREATE INDEX Delegation_id FOR (n:Delegation) ON (n.id) + CREATE INDEX Delegation_startDate FOR (n:Delegation) ON (n.startDate) + CREATE INDEX Delegation_endDate FOR (n:Delegation) ON (n.endDate) + CREATE INDEX Batch_expiryTime FOR (n:Batch) ON (n.expiryTime) + CREATE INDEX AttrRepo_id FOR (n:AttrRepo) ON (n.id) + CREATE INDEX AuthModule_id FOR (n:AuthModule) ON (n.id) + CREATE INDEX AuthProfile_id FOR (n:AuthProfile) ON (n.id) + CREATE INDEX FIQLQuery_id FOR (n:FIQLQuery) ON (n.id) + CREATE INDEX MailTemplate_id FOR (n:MailTemplate) ON (n.id) + CREATE INDEX Notification_id FOR (n:Notification) ON (n.id) + CREATE INDEX Remediation_id FOR (n:Remediation) ON (n.id) + CREATE INDEX ReportExec_id FOR (n:ReportExec) ON (n.id) + CREATE INDEX ReportExec_endDate FOR (n:ReportExec) ON (n.endDate) + CREATE INDEX RelationshipType_id FOR (n:RelationshipType) ON (n.id) + CREATE INDEX LinkedAccount_connObjectKeyValue FOR (n:LinkedAccount) ON (n.connObjectKeyValue) + + CREATE INDEX Policy_id FOR (n:Policy) ON (n.id) + + CREATE INDEX AuditEvent_when FOR (n:AuditEvent) ON (n.when) + CREATE INDEX AuditEvent_who FOR (n:AuditEvent) ON (n.who) + CREATE INDEX AuditEvent_opEvent FOR (n:AuditEvent) ON (n.opEvent) + + + CREATE INDEX SchedTask_id FOR (n:SchedTask) ON (n.id) + CREATE INDEX PropagationTask_id FOR (n:PropagationTask) ON (n.id) + + CREATE INDEX PropagationTask_anyTypeKind_entityKey FOR (n:PropagationTask) ON (n.anyTypeKind, n.entityKey) + CREATE INDEX NotificationTask_id FOR (n:NotificationTask) ON (n.id) + + CREATE INDEX SchedTaskExec_id FOR (n:SchedTaskExec) ON (n.id) + CREATE INDEX SchedTaskExec_endDate FOR (n:SchedTaskExec) ON (n.endDate) + CREATE INDEX LiveSyncTaskExec_id FOR (n:LiveSyncTaskExec) ON (n.id) + CREATE INDEX LiveSyncTaskExec_endDate FOR (n:LiveSyncTaskExec) ON (n.endDate) + CREATE INDEX PullTaskExec_id FOR (n:PullTaskExec) ON (n.id) + CREATE INDEX PullTaskExec_endDate FOR (n:PullTaskExec) ON (n.endDate) + CREATE INDEX PushTaskExec_id FOR (n:PushTaskExec) ON (n.id) + CREATE INDEX PushTaskExec_endDate FOR (n:PushTaskExec) ON (n.endDate) + CREATE INDEX MacroTaskExec_id FOR (n:MacroTaskExec) ON (n.id) + CREATE INDEX MacroTaskExec_endDate FOR (n:MacroTaskExec) ON (n.endDate) + CREATE INDEX PropagationTaskExec_id FOR (n:PropagationTaskExec) ON (n.id) + CREATE INDEX PropagationTaskExec_endDate FOR (n:PropagationTaskExec) ON (n.endDate) + CREATE INDEX PropagationTaskExec_status FOR (n:PropagationTaskExec) ON (n.status) + CREATE INDEX NotificationTaskExec_id FOR (n:NotificationTaskExec) ON (n.id) + CREATE INDEX NotificationTaskExec_endDate FOR (n:NotificationTaskExec) ON (n.endDate) + + CREATE INDEX NetworkService_type FOR (n:NetworkService) ON (n.type) diff --git a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/RealmTest.java b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/RealmTest.java index b00bc5dc206..8486c2aaa84 100644 --- a/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/RealmTest.java +++ b/core/persistence-neo4j/src/test/java/org/apache/syncope/core/persistence/neo4j/inner/RealmTest.java @@ -174,6 +174,68 @@ void saveNullParent() { } } + @Test + void renameAndMoveWithDescendants() { + Realm even = realmSearchDAO.findByFullPath("/even").orElseThrow(); + + // sibling whose name shares the prefix with "two", to prove descendant lookup is prefix-exact + Realm twob = entityFactory.newEntity(Realm.class); + twob.setName("twob"); + twob.setParent(even); + realmDAO.save(twob); + + Realm two = realmSearchDAO.findByFullPath("/even/two").orElseThrow(); + + Realm child = entityFactory.newEntity(Realm.class); + child.setName("child"); + child.setParent(two); + child = realmDAO.save(child); + + Realm grandChild = entityFactory.newEntity(Realm.class); + grandChild.setName("grandChild"); + grandChild.setParent(child); + grandChild = realmDAO.save(grandChild); + assertEquals("/even/two/child/grandChild", grandChild.getFullPath()); + + // rename: fullPath of all descendants is rewritten, sibling left alone + two.setName("due"); + realmDAO.save(two); + + assertTrue(realmSearchDAO.findByFullPath("/even/two").isEmpty()); + assertEquals("/even/due", realmSearchDAO.findByFullPath("/even/due").orElseThrow().getFullPath()); + assertEquals("/even/due/child", realmSearchDAO.findByFullPath("/even/due/child").orElseThrow().getFullPath()); + assertEquals( + "/even/due/child/grandChild", + realmSearchDAO.findByFullPath("/even/due/child/grandChild").orElseThrow().getFullPath()); + assertTrue(realmSearchDAO.findByFullPath("/even/two/child/grandChild").isEmpty()); + assertEquals("/even/twob", realmSearchDAO.findByFullPath("/even/twob").orElseThrow().getFullPath()); + + // descendants are cache-first: without eviction these would return the pre-rename fullPath + assertEquals("/even/due/child", realmDAO.findById(child.getKey()).orElseThrow().getFullPath()); + assertEquals( + "/even/due/child/grandChild", + realmDAO.findById(grandChild.getKey()).orElseThrow().getFullPath()); + + // move: the same subtree is rewritten under a different parent + Realm due = realmSearchDAO.findByFullPath("/even/due").orElseThrow(); + due.setParent(realmSearchDAO.findByFullPath("/odd").orElseThrow()); + realmDAO.save(due); + + assertTrue(realmSearchDAO.findByFullPath("/even/due").isEmpty()); + assertTrue(realmSearchDAO.findByFullPath("/even/due/child/grandChild").isEmpty()); + assertEquals("/odd/due", realmSearchDAO.findByFullPath("/odd/due").orElseThrow().getFullPath()); + assertEquals("/odd/due/child", realmSearchDAO.findByFullPath("/odd/due/child").orElseThrow().getFullPath()); + assertEquals( + "/odd/due/child/grandChild", + realmSearchDAO.findByFullPath("/odd/due/child/grandChild").orElseThrow().getFullPath()); + assertEquals( + "/odd/due/child/grandChild", + realmDAO.findById(grandChild.getKey()).orElseThrow().getFullPath()); + + // still untouched + assertEquals("/even/twob", realmSearchDAO.findByFullPath("/even/twob").orElseThrow().getFullPath()); + } + @Test void delete() { Realm realm = entityFactory.newEntity(Realm.class); diff --git a/pom.xml b/pom.xml index 354fdef695b..e16de842478 100644 --- a/pom.xml +++ b/pom.xml @@ -525,7 +525,7 @@ under the License. 9805 60000 - 11.0.27 + 11.0.29 18-alpine 9