You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@syncope.apache.org by il...@apache.org on 2025/11/17 11:36:36 UTC

(syncope) branch master updated: [SYNCOPE-1931] Allow for JEXL custom functions

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 745d9b6f39 [SYNCOPE-1931] Allow for JEXL custom functions
745d9b6f39 is described below

commit 745d9b6f390657a61f4bd3b6047c138cde55976f
Author: Francesco Chicchiriccò <il...@apache.org>
AuthorDate: Mon Nov 17 12:10:18 2025 +0100

    [SYNCOPE-1931] Allow for JEXL custom functions
---
 .../syncope/core/logic/AbstractAnyLogic.java       |   2 +-
 .../apache/syncope/core/logic/AnyObjectLogic.java  |   2 +-
 .../org/apache/syncope/core/logic/GroupLogic.java  |   2 +-
 .../syncope/core/logic/IdRepoLogicContext.java     |   2 +-
 .../org/apache/syncope/core/logic/UserLogic.java   |   2 +-
 .../provisioning/api/jexl/EmptyClassLoader.java    |   2 +-
 .../{JexlUtils.java => JexlContextBuilder.java}    | 134 ++--------
 .../{EmptyClassLoader.java => JexlFunctions.java}  |  15 +-
 .../core/provisioning/api/jexl/JexlTools.java      | 116 ++++++++
 .../api/jexl/SyncopeJexlFunctions.java             |   7 +-
 .../core/provisioning/api/jexl}/TemplateUtils.java | 107 ++++----
 .../core/provisioning/api/AbstractTest.java        |  19 ++
 .../core/provisioning/api/jexl/JexlUtilsTest.java  |  72 +++--
 .../provisioning/api/jexl/MailTemplateTest.java    |   6 +-
 .../core/provisioning/api/jexl/MappingTest.java    |  19 +-
 .../provisioning/java/DefaultDerAttrHandler.java   |  43 +--
 .../provisioning/java/DefaultMappingManager.java   |  28 +-
 .../provisioning/java/ProvisioningContext.java     | 110 ++++++--
 .../core/provisioning/java/data/AnyDataBinder.java |   9 +-
 .../java/data/AnyObjectDataBinderImpl.java         |   7 +-
 .../java/data/AttrRepoDataBinderImpl.java          |  12 +-
 .../java/data/AttributableDataBinder.java          |  10 +-
 .../java/data/AuthModuleDataBinderImpl.java        |  12 +-
 .../java/data/GroupDataBinderImpl.java             |   7 +-
 .../java/data/JEXLItemTransformerImpl.java         |  29 +-
 .../java/data/RealmDataBinderImpl.java             |  17 +-
 .../java/data/ResourceDataBinderImpl.java          |  18 +-
 .../java/data/SchemaDataBinderImpl.java            |  12 +-
 .../provisioning/java/data/TaskDataBinderImpl.java |  12 +-
 .../provisioning/java/data/UserDataBinderImpl.java |   7 +-
 .../provisioning/java/job/MacroJobDelegate.java    |  10 +-
 .../notification/DefaultNotificationManager.java   |  14 +-
 .../propagation/DefaultPropagationManager.java     |  10 +-
 .../LDAPMembershipPropagationActions.java          |  18 +-
 .../java/pushpull/SinglePullJobDelegate.java       |   7 +-
 .../provisioning/java/utils/ConnObjectUtils.java   |   1 +
 .../syncope/core/logic/OIDCC4UILogicContext.java   |   2 +-
 .../syncope/core/logic/oidc/OIDCUserManager.java   |   2 +-
 .../java/data/OIDCC4UIProviderDataBinderImpl.java  |  13 +-
 .../java/data/OIDCC4UIProvisioningContext.java     |   7 +-
 .../syncope/core/logic/SAML2SP4UILogicContext.java |   2 +-
 .../core/logic/saml2/SAML2SP4UIUserManager.java    |   2 +-
 .../java/SAML2SP4UIProvisioningContext.java        |   7 +-
 .../java/data/SAML2SP4UIIdPDataBinderImpl.java     |  13 +-
 .../apache/syncope/core/logic/SCIMDataBinder.java  | 294 +++++++++++----------
 .../syncope/core/logic/SCIMLogicContext.java       |   6 +-
 .../syncope/core/logic/SCIMDataBinderTest.java     | 113 ++++----
 .../ext/scimv2/cxf/SCIMv2RESTCXFContext.java       |  50 +++-
 .../scimv2/cxf/service/SCIMGroupServiceImpl.java   |  10 +-
 pom.xml                                            |   4 +-
 .../reference-guide/concepts/implementations.adoc  |   2 +-
 src/main/asciidoc/reference-guide/usage/core.adoc  |  18 +-
 52 files changed, 843 insertions(+), 602 deletions(-)

diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AbstractAnyLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AbstractAnyLogic.java
index 260df311e4..aca80f1b82 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AbstractAnyLogic.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AbstractAnyLogic.java
@@ -38,7 +38,7 @@ import org.apache.syncope.core.persistence.api.dao.RealmSearchDAO;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
 import org.apache.syncope.core.persistence.api.entity.AnyType;
 import org.apache.syncope.core.persistence.api.entity.Realm;
-import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
+import org.apache.syncope.core.provisioning.api.jexl.TemplateUtils;
 import org.apache.syncope.core.spring.implementation.ImplementationManager;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AnyObjectLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AnyObjectLogic.java
index fe248c1709..7f9ec30d89 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AnyObjectLogic.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/AnyObjectLogic.java
@@ -53,7 +53,7 @@ import org.apache.syncope.core.persistence.api.utils.RealmUtils;
 import org.apache.syncope.core.provisioning.api.AnyObjectProvisioningManager;
 import org.apache.syncope.core.provisioning.api.ProvisioningManager;
 import org.apache.syncope.core.provisioning.api.data.AnyObjectDataBinder;
-import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
+import org.apache.syncope.core.provisioning.api.jexl.TemplateUtils;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/GroupLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/GroupLogic.java
index d59b1be994..1acd1039ff 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/GroupLogic.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/GroupLogic.java
@@ -62,10 +62,10 @@ import org.apache.syncope.core.provisioning.api.GroupProvisioningManager;
 import org.apache.syncope.core.provisioning.api.ProvisioningManager;
 import org.apache.syncope.core.provisioning.api.data.GroupDataBinder;
 import org.apache.syncope.core.provisioning.api.data.TaskDataBinder;
+import org.apache.syncope.core.provisioning.api.jexl.TemplateUtils;
 import org.apache.syncope.core.provisioning.api.job.JobManager;
 import org.apache.syncope.core.provisioning.java.job.GroupMemberProvisionTaskJobDelegate;
 import org.apache.syncope.core.provisioning.java.job.SyncopeTaskScheduler;
-import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.apache.syncope.core.spring.security.SecurityProperties;
 import org.springframework.data.domain.Page;
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/IdRepoLogicContext.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/IdRepoLogicContext.java
index 685174bbe6..4d20214dc1 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/IdRepoLogicContext.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/IdRepoLogicContext.java
@@ -86,6 +86,7 @@ import org.apache.syncope.core.provisioning.api.data.SchemaDataBinder;
 import org.apache.syncope.core.provisioning.api.data.SecurityQuestionDataBinder;
 import org.apache.syncope.core.provisioning.api.data.TaskDataBinder;
 import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
+import org.apache.syncope.core.provisioning.api.jexl.TemplateUtils;
 import org.apache.syncope.core.provisioning.api.job.JobManager;
 import org.apache.syncope.core.provisioning.api.notification.NotificationJobDelegate;
 import org.apache.syncope.core.provisioning.api.notification.NotificationManager;
@@ -93,7 +94,6 @@ import org.apache.syncope.core.provisioning.api.propagation.PropagationManager;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskExecutor;
 import org.apache.syncope.core.provisioning.api.rules.RuleProvider;
 import org.apache.syncope.core.provisioning.java.job.SyncopeTaskScheduler;
-import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
 import org.apache.syncope.core.spring.security.SecurityProperties;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java
index 02f9c35f44..87a9fecdac 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/UserLogic.java
@@ -72,9 +72,9 @@ import org.apache.syncope.core.persistence.api.utils.RealmUtils;
 import org.apache.syncope.core.provisioning.api.ProvisioningManager;
 import org.apache.syncope.core.provisioning.api.UserProvisioningManager;
 import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
+import org.apache.syncope.core.provisioning.api.jexl.TemplateUtils;
 import org.apache.syncope.core.provisioning.api.rules.RuleProvider;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
-import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
 import org.apache.syncope.core.spring.policy.AccountPolicyException;
 import org.apache.syncope.core.spring.policy.PasswordPolicyException;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/EmptyClassLoader.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/EmptyClassLoader.java
index d6db5edc15..803f8a245c 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/EmptyClassLoader.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/EmptyClassLoader.java
@@ -21,7 +21,7 @@ package org.apache.syncope.core.provisioning.api.jexl;
 /**
  * A class loader that will throw {@link ClassNotFoundException} for every class name.
  */
-class EmptyClassLoader extends ClassLoader {
+public class EmptyClassLoader extends ClassLoader {
 
     @Override
     public Class<?> loadClass(final String name) throws ClassNotFoundException {
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/JexlUtils.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/JexlContextBuilder.java
similarity index 62%
rename from core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/JexlUtils.java
rename to core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/JexlContextBuilder.java
index d11a0cc30c..269ec784e8 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/JexlUtils.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/JexlContextBuilder.java
@@ -21,7 +21,6 @@ package org.apache.syncope.core.provisioning.api.jexl;
 import java.beans.IntrospectionException;
 import java.beans.Introspector;
 import java.beans.PropertyDescriptor;
-import java.io.StringWriter;
 import java.lang.reflect.Field;
 import java.time.temporal.TemporalAccessor;
 import java.util.Collection;
@@ -30,16 +29,9 @@ import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.Set;
-import org.apache.commons.jexl3.JexlBuilder;
 import org.apache.commons.jexl3.JexlContext;
-import org.apache.commons.jexl3.JexlEngine;
-import org.apache.commons.jexl3.JexlException;
-import org.apache.commons.jexl3.JexlExpression;
-import org.apache.commons.jexl3.JxltEngine;
 import org.apache.commons.jexl3.MapContext;
-import org.apache.commons.jexl3.introspection.JexlPermissions;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.ClassUtils;
 import org.apache.commons.lang3.StringUtils;
@@ -58,101 +50,25 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.util.ReflectionUtils;
 
-/**
- * JEXL <a href="http://commons.apache.org/jexl/reference/index.html">reference</a> is available.
- */
-@SuppressWarnings({ "squid:S3008", "squid:S3776", "squid:S1141" })
-public final class JexlUtils {
+public class JexlContextBuilder {
 
-    private static final Logger LOG = LoggerFactory.getLogger(JexlUtils.class);
+    protected static final Logger LOG = LoggerFactory.getLogger(JexlContextBuilder.class);
 
     private static final String[] IGNORE_FIELDS = { "password", "clearPassword", "serialVersionUID", "class" };
 
     private static final Map<Class<?>, Set<Pair<PropertyDescriptor, Field>>> FIELD_CACHE =
             Collections.synchronizedMap(new HashMap<>());
 
-    private static JexlEngine JEXL_ENGINE;
-
-    private static JxltEngine JXTL_ENGINE;
-
-    private static JexlEngine getJexlEngine() {
-        synchronized (LOG) {
-            if (JEXL_ENGINE == null) {
-                JEXL_ENGINE = new JexlBuilder().
-                        loader(new EmptyClassLoader()).
-                        permissions(JexlPermissions.RESTRICTED.compose("java.time.*", "org.apache.syncope.*")).
-                        namespaces(Map.of("syncope", new SyncopeJexlFunctions())).
-                        cache(512).
-                        silent(false).
-                        strict(false).
-                        create();
-            }
-        }
-
-        return JEXL_ENGINE;
-    }
-
-    private static JxltEngine getJxltEngine() {
-        synchronized (LOG) {
-            if (JXTL_ENGINE == null) {
-                JXTL_ENGINE = getJexlEngine().createJxltEngine(false);
-            }
-        }
-
-        return JXTL_ENGINE;
-    }
-
-    public static boolean isExpressionValid(final String expression) {
-        boolean result;
-        try {
-            getJexlEngine().createExpression(expression);
-            result = true;
-        } catch (JexlException e) {
-            LOG.error("Invalid JEXL expression: {}", expression, e);
-            result = false;
-        }
+    protected final JexlContext jexlContext = new MapContext();
 
-        return result;
+    public JexlContextBuilder with(final String name, final Object value) {
+        jexlContext.set(name, value);
+        return this;
     }
 
-    public static Object evaluateExpr(final String expression, final JexlContext jexlContext) {
-        Object result = null;
-
-        if (StringUtils.isNotBlank(expression) && jexlContext != null) {
-            try {
-                JexlExpression jexlExpression = getJexlEngine().createExpression(expression);
-                result = jexlExpression.evaluate(jexlContext);
-            } catch (Exception e) {
-                LOG.error("Error while evaluating JEXL expression: {}", expression, e);
-            }
-        } else {
-            LOG.debug("Expression not provided or invalid context");
-        }
-
-        return Optional.ofNullable(result).orElse(StringUtils.EMPTY);
-    }
-
-    public static String evaluateTemplate(final String template, final JexlContext jexlContext) {
-        String result = null;
-
-        if (StringUtils.isNotBlank(template) && jexlContext != null) {
-            try {
-                StringWriter writer = new StringWriter();
-                getJxltEngine().createTemplate(template).evaluate(jexlContext, writer);
-                result = writer.toString();
-            } catch (Exception e) {
-                LOG.error("Error while evaluating JEXL template: {}", template, e);
-            }
-        } else {
-            LOG.debug("Template not provided or invalid context");
-        }
-
-        return Optional.ofNullable(result).orElse(template);
-    }
-
-    public static void addFieldsToContext(final Object object, final JexlContext jexlContext) {
+    public JexlContextBuilder fields(final Object object) {
         if (object == null) {
-            return;
+            return this;
         }
 
         Set<Pair<PropertyDescriptor, Field>> cached = FIELD_CACHE.get(object.getClass());
@@ -227,9 +143,11 @@ public final class JexlUtils {
         } else if (object instanceof RealmTO realmTO) {
             jexlContext.set("fullPath", realmTO.getFullPath());
         }
+
+        return this;
     }
 
-    public static void addAttrsToContext(final Collection<Attr> attrs, final JexlContext jexlContext) {
+    public JexlContextBuilder attrs(final Collection<Attr> attrs) {
         attrs.stream().filter(attr -> attr.getSchema() != null).forEach(attr -> {
             Object value;
             if (attr.getValues().isEmpty()) {
@@ -244,9 +162,11 @@ public final class JexlUtils {
 
             jexlContext.set(attr.getSchema(), value);
         });
+
+        return this;
     }
 
-    public static void addPlainAttrsToContext(final Collection<PlainAttr> attrs, final JexlContext jexlContext) {
+    public JexlContextBuilder plainAttrs(final Collection<PlainAttr> attrs) {
         attrs.stream().filter(attr -> attr.getSchema() != null).forEach(attr -> {
             List<String> attrValues = attr.getValuesAsStrings();
             Object value;
@@ -262,13 +182,11 @@ public final class JexlUtils {
 
             jexlContext.set(attr.getSchema(), value);
         });
-    }
 
-    public static void addDerAttrsToContext(
-            final Attributable attributable,
-            final DerAttrHandler derAttrHandler,
-            final JexlContext jexlContext) {
+        return this;
+    }
 
+    public JexlContextBuilder derAttrs(final Attributable attributable, final DerAttrHandler derAttrHandler) {
         Map<DerSchema, String> derAttrs = attributable instanceof Realm realm
                 ? derAttrHandler.getValues(realm)
                 : attributable instanceof Any any
@@ -276,23 +194,11 @@ public final class JexlUtils {
                         : Map.of();
 
         derAttrs.forEach((schema, value) -> jexlContext.set(schema.getKey(), value));
-    }
-
-    public static boolean evaluateMandatoryCondition(
-            final String mandatoryCondition,
-            final Attributable attributable,
-            final DerAttrHandler derAttrHandler) {
-
-        JexlContext jexlContext = new MapContext();
-        addPlainAttrsToContext(attributable.getPlainAttrs(), jexlContext);
-        addDerAttrsToContext(attributable, derAttrHandler, jexlContext);
 
-        return Boolean.parseBoolean(evaluateExpr(mandatoryCondition, jexlContext).toString());
+        return this;
     }
 
-    /**
-     * Private default constructor, for static-only classes.
-     */
-    private JexlUtils() {
+    public JexlContext build() {
+        return jexlContext;
     }
 }
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/EmptyClassLoader.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/JexlFunctions.java
similarity index 61%
copy from core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/EmptyClassLoader.java
copy to core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/JexlFunctions.java
index d6db5edc15..56fd253091 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/EmptyClassLoader.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/JexlFunctions.java
@@ -18,18 +18,7 @@
  */
 package org.apache.syncope.core.provisioning.api.jexl;
 
-/**
- * A class loader that will throw {@link ClassNotFoundException} for every class name.
- */
-class EmptyClassLoader extends ClassLoader {
-
-    @Override
-    public Class<?> loadClass(final String name) throws ClassNotFoundException {
-        throw new ClassNotFoundException("This classloader won't attempt to load " + name);
-    }
+public interface JexlFunctions {
 
-    @Override
-    protected Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
-        throw new ClassNotFoundException("This classloader won't attempt to load " + name);
-    }
+    String getNamespace();
 }
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/JexlTools.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/JexlTools.java
new file mode 100644
index 0000000000..e34df5b5ba
--- /dev/null
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/JexlTools.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.syncope.core.provisioning.api.jexl;
+
+import java.io.StringWriter;
+import java.util.Optional;
+import org.apache.commons.jexl3.JexlContext;
+import org.apache.commons.jexl3.JexlEngine;
+import org.apache.commons.jexl3.JexlException;
+import org.apache.commons.jexl3.JexlExpression;
+import org.apache.commons.jexl3.JxltEngine;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.core.persistence.api.entity.Attributable;
+import org.apache.syncope.core.provisioning.api.DerAttrHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * JEXL <a href="http://commons.apache.org/jexl/reference/index.html">reference</a> is available.
+ */
+@SuppressWarnings({ "squid:S3008", "squid:S3776", "squid:S1141" })
+public class JexlTools {
+
+    protected static final Logger LOG = LoggerFactory.getLogger(JexlTools.class);
+
+    protected final JexlEngine jexlEngine;
+
+    protected final JxltEngine jxltEngine;
+
+    public JexlTools(final JexlEngine jexlEngine) {
+        this.jexlEngine = jexlEngine;
+        this.jxltEngine = jexlEngine.createJxltEngine(false);
+    }
+
+    public JexlTools(final JexlEngine jexlEngine, final JxltEngine jxltEngine) {
+        this.jexlEngine = jexlEngine;
+        this.jxltEngine = jxltEngine;
+    }
+
+    public boolean isExpressionValid(final String expression) {
+        boolean result;
+        try {
+            jexlEngine.createExpression(expression);
+            result = true;
+        } catch (JexlException e) {
+            LOG.error("Invalid JEXL expression: {}", expression, e);
+            result = false;
+        }
+
+        return result;
+    }
+
+    public Object evaluateExpression(final String expression, final JexlContext jexlContext) {
+        Object result = null;
+
+        if (StringUtils.isNotBlank(expression) && jexlContext != null) {
+            try {
+                JexlExpression jexlExpression = jexlEngine.createExpression(expression);
+                result = jexlExpression.evaluate(jexlContext);
+            } catch (Exception e) {
+                LOG.error("Error while evaluating JEXL expression: {}", expression, e);
+            }
+        } else {
+            LOG.debug("Expression not provided or invalid context");
+        }
+
+        return Optional.ofNullable(result).orElse(StringUtils.EMPTY);
+    }
+
+    public String evaluateTemplate(final String template, final JexlContext jexlContext) {
+        String result = null;
+
+        if (StringUtils.isNotBlank(template) && jexlContext != null) {
+            try {
+                StringWriter writer = new StringWriter();
+                jxltEngine.createTemplate(template).evaluate(jexlContext, writer);
+                result = writer.toString();
+            } catch (Exception e) {
+                LOG.error("Error while evaluating JEXL template: {}", template, e);
+            }
+        } else {
+            LOG.debug("Template not provided or invalid context");
+        }
+
+        return Optional.ofNullable(result).orElse(template);
+    }
+
+    public boolean evaluateMandatoryCondition(
+            final String mandatoryCondition,
+            final Attributable attributable,
+            final DerAttrHandler derAttrHandler) {
+
+        JexlContext jexlContext = new JexlContextBuilder().
+                plainAttrs(attributable.getPlainAttrs()).
+                derAttrs(attributable, derAttrHandler).
+                build();
+
+        return Boolean.parseBoolean(evaluateExpression(mandatoryCondition, jexlContext).toString());
+    }
+}
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/SyncopeJexlFunctions.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/SyncopeJexlFunctions.java
index 2aa7f0ff8a..f13571681e 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/SyncopeJexlFunctions.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/SyncopeJexlFunctions.java
@@ -30,7 +30,12 @@ import org.identityconnectors.framework.common.objects.ConnectorObject;
 /**
  * Utility functions for usage with JEXL engine.
  */
-public class SyncopeJexlFunctions {
+public class SyncopeJexlFunctions implements JexlFunctions {
+
+    @Override
+    public String getNamespace() {
+        return "syncope";
+    }
 
     /**
      * Converts realm's full path into the equivalent DN.
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/TemplateUtils.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/TemplateUtils.java
similarity index 83%
rename from core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/TemplateUtils.java
rename to core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/TemplateUtils.java
index 49712cd609..d49c06951f 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/TemplateUtils.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/TemplateUtils.java
@@ -16,12 +16,12 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.java.utils;
+package org.apache.syncope.core.provisioning.api.jexl;
 
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
-import org.apache.commons.jexl3.MapContext;
+import org.apache.commons.jexl3.JexlContext;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.Attr;
 import org.apache.syncope.common.lib.EntityTOUtils;
@@ -37,37 +37,56 @@ import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
-import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
 
 public class TemplateUtils {
 
-    public static void check(final Map<String, AnyTO> templates, final ClientExceptionType clientExceptionType) {
+    protected static void fillRelationships(final GroupableRelatableTO any, final GroupableRelatableTO template) {
+        template.getRelationships().stream().
+                filter(relationship -> any.getRelationship(
+                relationship.getOtherEndKey(), relationship.getOtherEndKey()).isEmpty()).
+                forEachOrdered(relationship -> any.getRelationships().add(relationship));
+    }
+
+    protected static void fillMemberships(final GroupableRelatableTO any, final GroupableRelatableTO template) {
+        template.getMemberships().stream().
+                filter(membership -> any.getMembership(membership.getGroupKey()).isEmpty()).
+                forEachOrdered(membership -> any.getMemberships().add(membership));
+    }
+
+    protected final UserDAO userDAO;
+
+    protected final GroupDAO groupDAO;
+
+    protected final JexlTools jexlTools;
+
+    public TemplateUtils(final UserDAO userDAO, final GroupDAO groupDAO, final JexlTools jexlTools) {
+        this.userDAO = userDAO;
+        this.groupDAO = groupDAO;
+        this.jexlTools = jexlTools;
+    }
+
+    public void check(final Map<String, AnyTO> templates, final ClientExceptionType clientExceptionType) {
         SyncopeClientException sce = SyncopeClientException.build(clientExceptionType);
 
         templates.values().forEach(value -> {
             value.getPlainAttrs().stream().
-                    filter(attrTO -> !attrTO.getValues().isEmpty()
-                    && !JexlUtils.isExpressionValid(attrTO.getValues().getFirst())).
-                    forEachOrdered(attrTO -> sce.getElements().add("Invalid JEXL: " + attrTO.getValues().getFirst()));
+                    filter(attr -> !attr.getValues().isEmpty()
+                    && !jexlTools.isExpressionValid(attr.getValues().getFirst())).
+                    forEach(attr -> sce.getElements().add("Invalid JEXL: " + attr.getValues().getFirst()));
 
             switch (value) {
                 case UserTO template -> {
-                    if (StringUtils.isNotBlank(template.getUsername())
-                            && !JexlUtils.isExpressionValid(template.getUsername())) {
-
+                    if (template.getUsername() != null && !jexlTools.isExpressionValid(template.getUsername())) {
                         sce.getElements().add("Invalid JEXL: " + template.getUsername());
                     }
-                    if (StringUtils.isNotBlank(template.getPassword())
-                            && !JexlUtils.isExpressionValid(template.getPassword())) {
-
+                    if (template.getPassword() != null && !jexlTools.isExpressionValid(template.getPassword())) {
                         sce.getElements().add("Invalid JEXL: " + template.getPassword());
                     }
                 }
                 case GroupTO template -> {
-                    if (StringUtils.isNotBlank(template.getName())
-                            && !JexlUtils.isExpressionValid(template.getName())) {
-
+                    if (template.getName() != null && !jexlTools.isExpressionValid(template.getName())) {
                         sce.getElements().add("Invalid JEXL: " + template.getName());
                     }
                 }
@@ -81,13 +100,13 @@ public class TemplateUtils {
         }
     }
 
-    protected static Attr evaluateAttr(final Attr template, final MapContext jexlContext) {
+    protected Attr evaluateAttr(final Attr template, final JexlContext jexlContext) {
         Attr result = new Attr();
         result.setSchema(template.getSchema());
 
-        if (template.getValues() != null && !template.getValues().isEmpty()) {
+        if (!CollectionUtils.isEmpty(template.getValues())) {
             template.getValues().forEach(value -> {
-                String evaluated = JexlUtils.evaluateExpr(value, jexlContext).toString();
+                String evaluated = jexlTools.evaluateExpression(value, jexlContext).toString();
                 if (StringUtils.isNotBlank(evaluated)) {
                     result.getValues().add(evaluated);
                 }
@@ -97,14 +116,15 @@ public class TemplateUtils {
         return result;
     }
 
-    protected static void fill(final RealmMember realmMember, final RealmMember template) {
-        MapContext jexlContext = new MapContext();
-        JexlUtils.addFieldsToContext(realmMember, jexlContext);
-        JexlUtils.addAttrsToContext(realmMember.getPlainAttrs(), jexlContext);
-        JexlUtils.addAttrsToContext(realmMember.getDerAttrs(), jexlContext);
+    protected void fill(final RealmMember realmMember, final RealmMember template) {
+        JexlContext jexlContext = new JexlContextBuilder().
+                fields(realmMember).
+                attrs(realmMember.getPlainAttrs()).
+                attrs(realmMember.getDerAttrs()).
+                build();
 
         if (template.getRealm() != null) {
-            String evaluated = JexlUtils.evaluateExpr(template.getRealm(), jexlContext).toString();
+            String evaluated = jexlTools.evaluateExpression(template.getRealm(), jexlContext).toString();
             if (StringUtils.isNotBlank(evaluated)) {
                 realmMember.setRealm(evaluated);
             }
@@ -136,36 +156,15 @@ public class TemplateUtils {
         realmMember.getAuxClasses().addAll(template.getAuxClasses());
     }
 
-    protected static void fillRelationships(final GroupableRelatableTO any, final GroupableRelatableTO template) {
-        template.getRelationships().stream().
-                filter(relationship -> any.getRelationship(
-                relationship.getOtherEndKey(), relationship.getOtherEndKey()).isEmpty()).
-                forEachOrdered(relationship -> any.getRelationships().add(relationship));
-    }
-
-    protected static void fillMemberships(final GroupableRelatableTO any, final GroupableRelatableTO template) {
-        template.getMemberships().stream().
-                filter(membership -> any.getMembership(membership.getGroupKey()).isEmpty()).
-                forEachOrdered(membership -> any.getMemberships().add(membership));
-    }
-
-    protected final UserDAO userDAO;
-
-    protected final GroupDAO groupDAO;
-
-    public TemplateUtils(final UserDAO userDAO, final GroupDAO groupDAO) {
-        this.userDAO = userDAO;
-        this.groupDAO = groupDAO;
-    }
-
     @Transactional(readOnly = true)
     public void apply(final RealmMember realmMember, final AnyTO template) {
         fill(realmMember, template);
 
-        MapContext jexlContext = new MapContext();
-        JexlUtils.addFieldsToContext(realmMember, jexlContext);
-        JexlUtils.addAttrsToContext(realmMember.getPlainAttrs(), jexlContext);
-        JexlUtils.addAttrsToContext(realmMember.getDerAttrs(), jexlContext);
+        JexlContext jexlContext = new JexlContextBuilder().
+                fields(realmMember).
+                attrs(realmMember.getPlainAttrs()).
+                attrs(realmMember.getDerAttrs()).
+                build();
 
         switch (template) {
             case AnyObjectTO anyObjectTO -> {
@@ -175,7 +174,7 @@ public class TemplateUtils {
 
             case UserTO userTO -> {
                 if (StringUtils.isNotBlank(userTO.getUsername())) {
-                    String evaluated = JexlUtils.evaluateExpr(userTO.getUsername(), jexlContext).toString();
+                    String evaluated = jexlTools.evaluateExpression(userTO.getUsername(), jexlContext).toString();
                     if (StringUtils.isNotBlank(evaluated)) {
                         switch (realmMember) {
                             case UserTO urm ->
@@ -189,7 +188,7 @@ public class TemplateUtils {
                 }
 
                 if (StringUtils.isNotBlank(userTO.getPassword())) {
-                    String evaluated = JexlUtils.evaluateExpr(userTO.getPassword(), jexlContext).toString();
+                    String evaluated = jexlTools.evaluateExpression(userTO.getPassword(), jexlContext).toString();
                     if (StringUtils.isNotBlank(evaluated)) {
                         switch (realmMember) {
                             case UserTO urm ->
@@ -242,7 +241,7 @@ public class TemplateUtils {
 
             case GroupTO groupTO -> {
                 if (StringUtils.isNotBlank(groupTO.getName())) {
-                    String evaluated = JexlUtils.evaluateExpr(groupTO.getName(), jexlContext).toString();
+                    String evaluated = jexlTools.evaluateExpression(groupTO.getName(), jexlContext).toString();
                     if (StringUtils.isNotBlank(evaluated)) {
                         switch (realmMember) {
                             case GroupTO grm ->
diff --git a/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/AbstractTest.java b/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/AbstractTest.java
index deccfdd36a..f923b6d3f0 100644
--- a/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/AbstractTest.java
+++ b/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/AbstractTest.java
@@ -18,6 +18,13 @@
  */
 package org.apache.syncope.core.provisioning.api;
 
+import java.util.Map;
+import org.apache.commons.jexl3.JexlBuilder;
+import org.apache.commons.jexl3.JexlEngine;
+import org.apache.commons.jexl3.introspection.JexlPermissions;
+import org.apache.syncope.core.provisioning.api.jexl.EmptyClassLoader;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
+import org.apache.syncope.core.provisioning.api.jexl.SyncopeJexlFunctions;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.MockitoAnnotations;
@@ -30,6 +37,18 @@ import org.mockito.quality.Strictness;
 @SuppressWarnings("squid:S2187")
 public class AbstractTest {
 
+    protected JexlTools jexlTools() {
+        JexlEngine jexlEngine = new JexlBuilder().
+                loader(new EmptyClassLoader()).
+                permissions(JexlPermissions.RESTRICTED.compose("java.time.*", "org.apache.syncope.*")).
+                namespaces(Map.of("syncope", new SyncopeJexlFunctions())).
+                cache(512).
+                silent(false).
+                strict(false).
+                create();
+        return new JexlTools(jexlEngine);
+    }
+
     @BeforeEach
     public void init() {
         MockitoAnnotations.openMocks(this);
diff --git a/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/jexl/JexlUtilsTest.java b/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/jexl/JexlUtilsTest.java
index ab943900d9..5ccaad0c27 100644
--- a/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/jexl/JexlUtilsTest.java
+++ b/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/jexl/JexlUtilsTest.java
@@ -47,6 +47,7 @@ import org.apache.syncope.core.provisioning.api.AbstractTest;
 import org.apache.syncope.core.provisioning.api.DerAttrHandler;
 import org.junit.jupiter.api.Test;
 import org.mockito.Mock;
+import org.springframework.test.util.ReflectionTestUtils;
 
 public class JexlUtilsTest extends AbstractTest {
 
@@ -54,78 +55,87 @@ public class JexlUtilsTest extends AbstractTest {
     private JexlContext context;
 
     @Test
-    public void isExpressionValid() {
+    void isExpressionValid() {
         String expression = "6 * 12 + 5 / 2.6";
-        assertTrue(JexlUtils.isExpressionValid(expression));
+        assertTrue(jexlTools().isExpressionValid(expression));
 
         expression = "@inv4lid expression!";
-        assertFalse(JexlUtils.isExpressionValid(expression));
+        assertFalse(jexlTools().isExpressionValid(expression));
     }
 
     @Test
-    public void evaluateExpr() {
+    void evaluateExpr() {
         String expression = null;
-        assertEquals(StringUtils.EMPTY, JexlUtils.evaluateExpr(expression, context));
+        assertEquals(StringUtils.EMPTY, jexlTools().evaluateExpression(expression, context));
 
         expression = "6 * 12 + 5 / 2.6";
         double result = 73.92307692307692;
-        assertEquals(result, JexlUtils.evaluateExpr(expression, context));
+        assertEquals(result, jexlTools().evaluateExpression(expression, context));
     }
 
     @Test
-    public void addFieldsToContext(
+    void builderFields(
             final @Mock Any any,
             final @Mock AnyTO anyTO,
             final @Mock Realm realm,
             final @Mock RealmTO realmTO) {
 
-        JexlUtils.addFieldsToContext(new Exception(), context);
+        JexlContextBuilder builder = new JexlContextBuilder();
+        ReflectionTestUtils.setField(builder, "jexlContext", context);
+
+        builder.fields(new Exception());
         verify(context, times(2)).set(eq("cause"), any());
 
         String testFullPath = "testFullPath";
         when(any.getRealm()).thenReturn(realm);
         when(realm.getFullPath()).thenReturn(testFullPath);
-        JexlUtils.addFieldsToContext(any, context);
+        builder.fields(any);
         verify(context).set("realm", testFullPath);
 
         String testRealm = "testRealm";
         when(anyTO.getRealm()).thenReturn(testRealm);
-        JexlUtils.addFieldsToContext(anyTO, context);
+        builder.fields(anyTO);
         verify(context, times(3)).set("realm", testRealm);
 
         String fullPath = "test/full/path";
         when(realm.getFullPath()).thenReturn(fullPath);
-        JexlUtils.addFieldsToContext(realm, context);
+        builder.fields(realm);
         verify(context, times(2)).set("fullPath", fullPath);
 
         fullPath = "test/full/path2";
         when(realmTO.getFullPath()).thenReturn(fullPath);
-        JexlUtils.addFieldsToContext(realmTO, context);
+        builder.fields(realmTO);
         verify(context, times(2)).set("fullPath", fullPath);
     }
 
     @Test
-    public void addAttrTOsToContext() {
-        String schemaName = "testSchema";
+    void builderAttrs() {
+        String schema = "testSchema";
         String value = "testValue";
         Collection<Attr> attrs = new ArrayList<>();
-        Attr attr = new Attr.Builder(schemaName).build();
+        Attr attr = new Attr.Builder(schema).build();
         attrs.add(attr);
 
-        JexlUtils.addAttrsToContext(attrs, context);
-        verify(context).set(schemaName, StringUtils.EMPTY);
+        JexlContextBuilder builder = new JexlContextBuilder();
+        ReflectionTestUtils.setField(builder, "jexlContext", context);
+
+        builder.attrs(attrs);
+        verify(context).set(schema, StringUtils.EMPTY);
 
-        attr = new Attr.Builder(schemaName).value(value).build();
+        attr = new Attr.Builder(schema).value(value).build();
         attrs.clear();
         attrs.add(attr);
 
-        JexlUtils.addAttrsToContext(attrs, context);
-        verify(context).set(schemaName, value);
+        builder.attrs(attrs);
+        verify(context).set(schema, value);
     }
 
     @Test
-    public void addPlainAttrsToContext(final @Mock Collection<PlainAttr> attrs) {
-        JexlUtils.addPlainAttrsToContext(attrs, context);
+    void builderPlainAttrs(final @Mock Collection<PlainAttr> attrs) {
+        JexlContextBuilder builder = new JexlContextBuilder();
+        ReflectionTestUtils.setField(builder, "jexlContext", context);
+
+        builder.plainAttrs(attrs);
         verify(context, times(0)).set(anyString(), any());
     }
 
@@ -141,12 +151,16 @@ public class JexlUtilsTest extends AbstractTest {
         derAttrs.put(derSchema, expression);
 
         when(derAttrHandler.getValues(any(Any.class))).thenReturn(derAttrs);
-        JexlUtils.addDerAttrsToContext(any, derAttrHandler, context);
+
+        JexlContextBuilder builder = new JexlContextBuilder();
+        ReflectionTestUtils.setField(builder, "jexlContext", context);
+
+        builder.derAttrs(any, derAttrHandler);
         verify(context).set(derAttrs.get(derSchema), expression);
     }
 
     @Test
-    public void evaluateMandatoryCondition(
+    void evaluateMandatoryCondition(
             final @Mock DerAttrHandler derAttrHandler,
             final @Mock Any any,
             final @Mock DerSchema derSchema,
@@ -160,18 +174,18 @@ public class JexlUtilsTest extends AbstractTest {
         when(any.getPlainAttrs()).thenReturn(new ArrayList<>());
         when(derAttrHandler.getValues(any(Any.class))).thenReturn(derAttrs);
 
-        assertTrue(JexlUtils.evaluateMandatoryCondition("true", any, derAttrHandler));
-        assertFalse(JexlUtils.evaluateMandatoryCondition("false", any, derAttrHandler));
+        assertTrue(jexlTools().evaluateMandatoryCondition("true", any, derAttrHandler));
+        assertFalse(jexlTools().evaluateMandatoryCondition("false", any, derAttrHandler));
     }
 
     @Test
-    public void evaluateTemplate() {
+    void evaluateTemplate() {
         byte[] byteArray = "a value".getBytes();
-        String result = JexlUtils.evaluateTemplate(
+        String result = jexlTools().evaluateTemplate(
                 "${syncope:base64Encode(value)}", new MapContext(Map.of("value", byteArray)));
         assertEquals(Base64.getEncoder().encodeToString(byteArray), result);
 
-        result = JexlUtils.evaluateTemplate(
+        result = jexlTools().evaluateTemplate(
                 "${syncope:fullPath2Dn(value, 'ou')}", new MapContext(Map.of("value", "/a/b/c")));
         assertEquals("ou=c,ou=b,ou=a", result);
     }
diff --git a/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/jexl/MailTemplateTest.java b/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/jexl/MailTemplateTest.java
index b93bf591b2..9451bb2bdf 100644
--- a/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/jexl/MailTemplateTest.java
+++ b/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/jexl/MailTemplateTest.java
@@ -66,7 +66,7 @@ public class MailTemplateTest extends AbstractTest {
 
     @Test
     public void confirmPasswordReset() throws IOException {
-        String htmlBody = JexlUtils.evaluateTemplate(CONFIRM_PASSWORD_RESET_TEMPLATE, new MapContext());
+        String htmlBody = jexlTools().evaluateTemplate(CONFIRM_PASSWORD_RESET_TEMPLATE, new MapContext());
         assertNotNull(htmlBody);
     }
 
@@ -84,7 +84,7 @@ public class MailTemplateTest extends AbstractTest {
         input.add(token);
         ctx.put("input", input);
 
-        String textBody = JexlUtils.evaluateTemplate(REQUEST_PASSWORD_RESET_TEMPLATE, new MapContext(ctx));
+        String textBody = jexlTools().evaluateTemplate(REQUEST_PASSWORD_RESET_TEMPLATE, new MapContext(ctx));
 
         assertNotNull(textBody);
         assertTrue(textBody.contains("a password reset was requested for " + username + "."));
@@ -120,7 +120,7 @@ public class MailTemplateTest extends AbstractTest {
 
         ctx.put("events", List.of("event1"));
 
-        String htmlBody = JexlUtils.evaluateTemplate(OPTIN_TEMPLATE, new MapContext(ctx));
+        String htmlBody = jexlTools().evaluateTemplate(OPTIN_TEMPLATE, new MapContext(ctx));
 
         assertNotNull(htmlBody);
 
diff --git a/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/jexl/MappingTest.java b/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/jexl/MappingTest.java
index 9badb8fa4b..fbb7129c1a 100644
--- a/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/jexl/MappingTest.java
+++ b/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/jexl/MappingTest.java
@@ -43,14 +43,13 @@ public class MappingTest extends AbstractTest {
         when(user.getRealm()).thenReturn(realm);
         assertNotNull(user);
 
-        JexlContext jexlContext = new MapContext();
-        JexlUtils.addFieldsToContext(user, jexlContext);
+        JexlContext jexlContext = new JexlContextBuilder().fields(user).build();
 
         String connObjectLink = "'uid=' + username + ',ou=people,o=isp'";
-        assertEquals("uid=rossini,ou=people,o=isp", JexlUtils.evaluateExpr(connObjectLink, jexlContext));
+        assertEquals("uid=rossini,ou=people,o=isp", jexlTools().evaluateExpression(connObjectLink, jexlContext));
 
         connObjectLink = "'uid=' + username + realm.replaceAll('/', ',o=') + ',ou=people,o=isp'";
-        assertEquals("uid=rossini,o=even,ou=people,o=isp", JexlUtils.evaluateExpr(connObjectLink, jexlContext));
+        assertEquals("uid=rossini,o=even,ou=people,o=isp", jexlTools().evaluateExpression(connObjectLink, jexlContext));
     }
 
     @Test
@@ -59,19 +58,17 @@ public class MappingTest extends AbstractTest {
         when(realm.getFullPath()).thenReturn("/even/two");
         assertNotNull(realm);
 
-        JexlContext jexlContext = new MapContext();
-        JexlUtils.addFieldsToContext(realm, jexlContext);
+        JexlContext jexlContext = new JexlContextBuilder().fields(realm).build();
 
         String connObjectLink = "syncope:fullPath2Dn(fullPath, 'ou') + ',o=isp'";
-        assertEquals("ou=two,ou=even,o=isp", JexlUtils.evaluateExpr(connObjectLink, jexlContext));
+        assertEquals("ou=two,ou=even,o=isp", jexlTools().evaluateExpression(connObjectLink, jexlContext));
 
         when(realm.getFullPath()).thenReturn("/even");
         assertNotNull(realm);
 
-        jexlContext = new MapContext();
-        JexlUtils.addFieldsToContext(realm, jexlContext);
+        jexlContext = new JexlContextBuilder().fields(realm).build();
 
-        assertEquals("ou=even,o=isp", JexlUtils.evaluateExpr(connObjectLink, jexlContext));
+        assertEquals("ou=even,o=isp", jexlTools().evaluateExpression(connObjectLink, jexlContext));
     }
 
     @Test
@@ -82,6 +79,6 @@ public class MappingTest extends AbstractTest {
         jexlContext.set("value", now);
 
         String expression = "value.toInstant().toEpochMilli()";
-        assertEquals(now.toInstant().toEpochMilli(), JexlUtils.evaluateExpr(expression, jexlContext));
+        assertEquals(now.toInstant().toEpochMilli(), jexlTools().evaluateExpression(expression, jexlContext));
     }
 }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultDerAttrHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultDerAttrHandler.java
index 3741da47da..cad5cff4ff 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultDerAttrHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultDerAttrHandler.java
@@ -23,7 +23,6 @@ import java.util.HashMap;
 import java.util.Map;
 import java.util.Set;
 import org.apache.commons.jexl3.JexlContext;
-import org.apache.commons.jexl3.MapContext;
 import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.Attributable;
@@ -32,7 +31,8 @@ import org.apache.syncope.core.persistence.api.entity.Groupable;
 import org.apache.syncope.core.persistence.api.entity.Membership;
 import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.provisioning.api.DerAttrHandler;
-import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlContextBuilder;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.transaction.annotation.Transactional;
@@ -42,29 +42,33 @@ public class DefaultDerAttrHandler implements DerAttrHandler {
 
     protected static final Logger LOG = LoggerFactory.getLogger(DerAttrHandler.class);
 
-    protected static Map<DerSchema, String> getValues(
+    protected final AnyUtilsFactory anyUtilsFactory;
+
+    protected final JexlTools jexlTools;
+
+    public DefaultDerAttrHandler(final AnyUtilsFactory anyUtilsFactory, final JexlTools jexlTools) {
+        this.anyUtilsFactory = anyUtilsFactory;
+        this.jexlTools = jexlTools;
+    }
+
+    protected Map<DerSchema, String> getValues(
             final Attributable attributable,
             final Collection<? extends DerSchema> schemas) {
 
         Map<DerSchema, String> result = new HashMap<>(schemas.size());
 
         schemas.forEach(schema -> {
-            JexlContext jexlContext = new MapContext();
-            JexlUtils.addPlainAttrsToContext(attributable.getPlainAttrs(), jexlContext);
-            JexlUtils.addFieldsToContext(attributable, jexlContext);
+            JexlContext jexlContext = new JexlContextBuilder().
+                    plainAttrs(attributable.getPlainAttrs()).
+                    fields(attributable).
+                    build();
 
-            result.put(schema, JexlUtils.evaluateExpr(schema.getExpression(), jexlContext).toString());
+            result.put(schema, jexlTools.evaluateExpression(schema.getExpression(), jexlContext).toString());
         });
 
         return result;
     }
 
-    protected final AnyUtilsFactory anyUtilsFactory;
-
-    public DefaultDerAttrHandler(final AnyUtilsFactory anyUtilsFactory) {
-        this.anyUtilsFactory = anyUtilsFactory;
-    }
-
     @Override
     public String getValue(final Realm realm, final DerSchema schema) {
         if (realm.getAnyTypeClass() == null || !realm.getAnyTypeClass().getDerSchemas().contains(schema)) {
@@ -115,17 +119,18 @@ public class DefaultDerAttrHandler implements DerAttrHandler {
                 anyUtilsFactory.getInstance(any).dao().findAllowedSchemas(any, DerSchema.class).getForSelf());
     }
 
-    protected static Map<DerSchema, String> getValues(
-            final Groupable<?, ?, ?> any, final Membership<?> membership, final Set<DerSchema> schemas) {
+    protected Map<DerSchema, String> getValues(
+            final Groupable<?, ?, ?> groupable, final Membership<?> membership, final Set<DerSchema> schemas) {
 
         Map<DerSchema, String> result = new HashMap<>(schemas.size());
 
         schemas.forEach(schema -> {
-            JexlContext jexlContext = new MapContext();
-            JexlUtils.addPlainAttrsToContext(any.getPlainAttrs(membership), jexlContext);
-            JexlUtils.addFieldsToContext(any, jexlContext);
+            JexlContext jexlContext = new JexlContextBuilder().
+                    plainAttrs(groupable.getPlainAttrs(membership)).
+                    fields(groupable).
+                    build();
 
-            result.put(schema, JexlUtils.evaluateExpr(schema.getExpression(), jexlContext).toString());
+            result.put(schema, jexlTools.evaluateExpression(schema.getExpression(), jexlContext).toString());
         });
 
         return result;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultMappingManager.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultMappingManager.java
index 0b947428ce..76b88c6baf 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultMappingManager.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultMappingManager.java
@@ -28,7 +28,6 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 import org.apache.commons.jexl3.JexlContext;
-import org.apache.commons.jexl3.MapContext;
 import org.apache.commons.lang3.BooleanUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.mutable.Mutable;
@@ -83,7 +82,8 @@ import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.api.MappingManager;
 import org.apache.syncope.core.provisioning.api.PlainAttrGetter;
 import org.apache.syncope.core.provisioning.api.data.ItemTransformer;
-import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlContextBuilder;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.apache.syncope.core.provisioning.java.utils.ConnObjectUtils;
 import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
 import org.identityconnectors.framework.common.FrameworkUtil;
@@ -189,6 +189,8 @@ public class DefaultMappingManager implements MappingManager {
 
     protected final EncryptorManager encryptorManager;
 
+    protected final JexlTools jexlTools;
+
     public DefaultMappingManager(
             final AnyTypeDAO anyTypeDAO,
             final UserDAO userDAO,
@@ -199,7 +201,8 @@ public class DefaultMappingManager implements MappingManager {
             final ImplementationDAO implementationDAO,
             final DerAttrHandler derAttrHandler,
             final IntAttrNameParser intAttrNameParser,
-            final EncryptorManager encryptorManager) {
+            final EncryptorManager encryptorManager,
+            final JexlTools jexlTools) {
 
         this.anyTypeDAO = anyTypeDAO;
         this.userDAO = userDAO;
@@ -211,6 +214,7 @@ public class DefaultMappingManager implements MappingManager {
         this.derAttrHandler = derAttrHandler;
         this.intAttrNameParser = intAttrNameParser;
         this.encryptorManager = encryptorManager;
+        this.jexlTools = jexlTools;
     }
 
     protected List<Implementation> getTransformers(final Item item) {
@@ -242,11 +246,13 @@ public class DefaultMappingManager implements MappingManager {
                 orElse(null);
         String evalConnObjectLink = null;
         if (StringUtils.isNotBlank(connObjectLink)) {
-            JexlContext jexlContext = new MapContext();
-            JexlUtils.addFieldsToContext(any, jexlContext);
-            JexlUtils.addPlainAttrsToContext(any.getPlainAttrs(), jexlContext);
-            JexlUtils.addDerAttrsToContext(any, derAttrHandler, jexlContext);
-            evalConnObjectLink = JexlUtils.evaluateExpr(connObjectLink, jexlContext).toString();
+            JexlContext jexlContext = new JexlContextBuilder().
+                    fields(any).
+                    plainAttrs(any.getPlainAttrs()).
+                    derAttrs(any, derAttrHandler).
+                    build();
+
+            evalConnObjectLink = jexlTools.evaluateExpression(connObjectLink, jexlContext).toString();
         }
 
         return getName(evalConnObjectLink, connObjectKey);
@@ -272,9 +278,9 @@ public class DefaultMappingManager implements MappingManager {
         String connObjectLink = orgUnit.getConnObjectLink();
         String evalConnObjectLink = null;
         if (StringUtils.isNotBlank(connObjectLink)) {
-            JexlContext jexlContext = new MapContext();
-            JexlUtils.addFieldsToContext(realm, jexlContext);
-            evalConnObjectLink = JexlUtils.evaluateExpr(connObjectLink, jexlContext).toString();
+            JexlContext jexlContext = new JexlContextBuilder().fields(realm).build();
+
+            evalConnObjectLink = jexlTools.evaluateExpression(connObjectLink, jexlContext).toString();
         }
 
         return getName(evalConnObjectLink, connObjectKey);
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/ProvisioningContext.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/ProvisioningContext.java
index 3f2980226a..7653817aca 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/ProvisioningContext.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/ProvisioningContext.java
@@ -20,6 +20,11 @@ package org.apache.syncope.core.provisioning.java;
 
 import java.util.List;
 import java.util.concurrent.Executor;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.apache.commons.jexl3.JexlBuilder;
+import org.apache.commons.jexl3.JexlEngine;
+import org.apache.commons.jexl3.introspection.JexlPermissions;
 import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
 import org.apache.syncope.core.persistence.api.DomainHolder;
 import org.apache.syncope.core.persistence.api.EncryptorManager;
@@ -104,6 +109,11 @@ import org.apache.syncope.core.provisioning.api.data.TaskDataBinder;
 import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
 import org.apache.syncope.core.provisioning.api.data.WAConfigDataBinder;
 import org.apache.syncope.core.provisioning.api.data.wa.WAClientAppDataBinder;
+import org.apache.syncope.core.provisioning.api.jexl.EmptyClassLoader;
+import org.apache.syncope.core.provisioning.api.jexl.JexlFunctions;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
+import org.apache.syncope.core.provisioning.api.jexl.SyncopeJexlFunctions;
+import org.apache.syncope.core.provisioning.api.jexl.TemplateUtils;
 import org.apache.syncope.core.provisioning.api.job.JobManager;
 import org.apache.syncope.core.provisioning.api.notification.NotificationJobDelegate;
 import org.apache.syncope.core.provisioning.api.notification.NotificationManager;
@@ -155,7 +165,6 @@ import org.apache.syncope.core.provisioning.java.pushpull.InboundMatcher;
 import org.apache.syncope.core.provisioning.java.pushpull.LiveSyncTaskSaver;
 import org.apache.syncope.core.provisioning.java.pushpull.OutboundMatcher;
 import org.apache.syncope.core.provisioning.java.utils.ConnObjectUtils;
-import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
 import org.apache.syncope.core.spring.security.DefaultCredentialChecker;
 import org.apache.syncope.core.spring.security.PasswordGenerator;
 import org.apache.syncope.core.spring.security.SecurityProperties;
@@ -183,6 +192,27 @@ import org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler;
 @Configuration(proxyBeanMethods = false)
 public class ProvisioningContext {
 
+    @ConditionalOnMissingBean
+    @Bean
+    public SyncopeJexlFunctions syncopeJexlFunctions() {
+        return new SyncopeJexlFunctions();
+    }
+
+    @ConditionalOnMissingBean
+    @Bean
+    public JexlTools jexlTools(final List<JexlFunctions> jexlFunctions) {
+        JexlEngine jexlEngine = new JexlBuilder().
+                loader(new EmptyClassLoader()).
+                permissions(JexlPermissions.RESTRICTED.compose("java.time.*", "org.apache.syncope.*")).
+                namespaces(jexlFunctions.stream().
+                        collect(Collectors.toMap(JexlFunctions::getNamespace, Function.identity()))).
+                cache(512).
+                silent(false).
+                strict(false).
+                create();
+        return new JexlTools(jexlEngine);
+    }
+
     @ConditionalOnMissingBean
     @Bean
     public AsyncConnectorFacade asyncConnectorFacade() {
@@ -361,8 +391,8 @@ public class ProvisioningContext {
 
     @ConditionalOnMissingBean
     @Bean
-    public DerAttrHandler derAttrHandler(final AnyUtilsFactory anyUtilsFactory) {
-        return new DefaultDerAttrHandler(anyUtilsFactory);
+    public DerAttrHandler derAttrHandler(final AnyUtilsFactory anyUtilsFactory, final JexlTools jexlTools) {
+        return new DefaultDerAttrHandler(anyUtilsFactory, jexlTools);
     }
 
     @ConditionalOnMissingBean
@@ -377,7 +407,8 @@ public class ProvisioningContext {
             final ImplementationDAO implementationDAO,
             final DerAttrHandler derAttrHandler,
             final IntAttrNameParser intAttrNameParser,
-            final EncryptorManager encryptorManager) {
+            final EncryptorManager encryptorManager,
+            final JexlTools jexlTools) {
 
         return new DefaultMappingManager(
                 anyTypeDAO,
@@ -389,13 +420,14 @@ public class ProvisioningContext {
                 implementationDAO,
                 derAttrHandler,
                 intAttrNameParser,
-                encryptorManager);
+                encryptorManager,
+                jexlTools);
     }
 
     @ConditionalOnMissingBean
     @Bean
-    public TemplateUtils templateUtils(final UserDAO userDAO, final GroupDAO groupDAO) {
-        return new TemplateUtils(userDAO, groupDAO);
+    public TemplateUtils templateUtils(final UserDAO userDAO, final GroupDAO groupDAO, final JexlTools jexlTools) {
+        return new TemplateUtils(userDAO, groupDAO, jexlTools);
     }
 
     @ConditionalOnMissingBean
@@ -429,7 +461,8 @@ public class ProvisioningContext {
             final ExternalResourceDAO resourceDAO,
             final ConnObjectUtils connObjectUtils,
             final MappingManager mappingManager,
-            final DerAttrHandler derAttrHandler) {
+            final DerAttrHandler derAttrHandler,
+            final JexlTools jexlTools) {
 
         return new DefaultPropagationManager(
                 resourceDAO,
@@ -437,7 +470,8 @@ public class ProvisioningContext {
                 connObjectUtils,
                 mappingManager,
                 derAttrHandler,
-                anyUtilsFactory);
+                anyUtilsFactory,
+                jexlTools);
     }
 
     @ConditionalOnMissingBean
@@ -558,7 +592,8 @@ public class ProvisioningContext {
             final AnyObjectDataBinder anyObjectDataBinder,
             final ConfParamOps confParamOps,
             final DerAttrHandler derAttrHandler,
-            final IntAttrNameParser intAttrNameParser) {
+            final IntAttrNameParser intAttrNameParser,
+            final JexlTools jexlTools) {
 
         return new DefaultNotificationManager(
                 derSchemaDAO,
@@ -576,7 +611,8 @@ public class ProvisioningContext {
                 confParamOps,
                 entityFactory,
                 intAttrNameParser,
-                searchCondVisitor);
+                searchCondVisitor,
+                jexlTools);
     }
 
     /**
@@ -696,7 +732,8 @@ public class ProvisioningContext {
             final MappingManager mappingManager,
             final IntAttrNameParser intAttrNameParser,
             final OutboundMatcher outboundMatcher,
-            final PlainAttrValidationManager validator) {
+            final PlainAttrValidationManager validator,
+            final JexlTools jexlTools) {
 
         return new AnyObjectDataBinderImpl(
                 anyTypeDAO,
@@ -714,7 +751,8 @@ public class ProvisioningContext {
                 mappingManager,
                 intAttrNameParser,
                 outboundMatcher,
-                validator);
+                validator,
+                jexlTools);
     }
 
     @ConditionalOnMissingBean
@@ -755,14 +793,14 @@ public class ProvisioningContext {
 
     @ConditionalOnMissingBean
     @Bean
-    public AuthModuleDataBinder authModuleDataBinder(final EntityFactory entityFactory) {
-        return new AuthModuleDataBinderImpl(entityFactory);
+    public AuthModuleDataBinder authModuleDataBinder(final EntityFactory entityFactory, final JexlTools jexlTools) {
+        return new AuthModuleDataBinderImpl(entityFactory, jexlTools);
     }
 
     @ConditionalOnMissingBean
     @Bean
-    public AttrRepoDataBinder attrRepoDataBinder(final EntityFactory entityFactory) {
-        return new AttrRepoDataBinderImpl(entityFactory);
+    public AttrRepoDataBinder attrRepoDataBinder(final EntityFactory entityFactory, final JexlTools jexlTools) {
+        return new AttrRepoDataBinderImpl(entityFactory, jexlTools);
     }
 
     @ConditionalOnMissingBean
@@ -848,7 +886,8 @@ public class ProvisioningContext {
             final MappingManager mappingManager,
             final IntAttrNameParser intAttrNameParser,
             final OutboundMatcher outboundMatcher,
-            final PlainAttrValidationManager validator) {
+            final PlainAttrValidationManager validator,
+            final JexlTools jexlTools) {
 
         return new GroupDataBinderImpl(
                 anyTypeDAO,
@@ -867,7 +906,8 @@ public class ProvisioningContext {
                 intAttrNameParser,
                 outboundMatcher,
                 searchCondVisitor,
-                validator);
+                validator,
+                jexlTools);
     }
 
     @ConditionalOnMissingBean
@@ -926,7 +966,9 @@ public class ProvisioningContext {
             final DerAttrHandler derAttrHandler,
             final PlainAttrValidationManager validator,
             final MappingManager mappingManager,
-            final IntAttrNameParser intAttrNameParser) {
+            final IntAttrNameParser intAttrNameParser,
+            final JexlTools jexlTools,
+            final TemplateUtils templateUtils) {
 
         return new RealmDataBinderImpl(
                 anyTypeDAO,
@@ -940,7 +982,9 @@ public class ProvisioningContext {
                 derAttrHandler,
                 validator,
                 mappingManager,
-                intAttrNameParser);
+                intAttrNameParser,
+                jexlTools,
+                templateUtils);
     }
 
     @ConditionalOnMissingBean
@@ -979,7 +1023,8 @@ public class ProvisioningContext {
             final ImplementationDAO implementationDAO,
             final PlainSchemaDAO plainSchemaDAO,
             final IntAttrNameParser intAttrNameParser,
-            final PropagationTaskExecutor propagationTaskExecutor) {
+            final PropagationTaskExecutor propagationTaskExecutor,
+            final JexlTools jexlTools) {
 
         return new ResourceDataBinderImpl(
                 anyTypeDAO,
@@ -990,7 +1035,8 @@ public class ProvisioningContext {
                 plainSchemaDAO,
                 entityFactory,
                 intAttrNameParser,
-                propagationTaskExecutor);
+                propagationTaskExecutor,
+                jexlTools);
     }
 
     @ConditionalOnMissingBean
@@ -1032,7 +1078,8 @@ public class ProvisioningContext {
             final DerSchemaDAO derSchemaDAO,
             final ExternalResourceDAO resourceDAO,
             final AnyTypeDAO anyTypeDAO,
-            final ImplementationDAO implementationDAO) {
+            final ImplementationDAO implementationDAO,
+            final JexlTools jexlTools) {
 
         return new SchemaDataBinderImpl(
                 anyTypeClassDAO,
@@ -1042,7 +1089,8 @@ public class ProvisioningContext {
                 anyTypeDAO,
                 implementationDAO,
                 entityFactory,
-                anyUtilsFactory);
+                anyUtilsFactory,
+                jexlTools);
     }
 
     @ConditionalOnMissingBean
@@ -1061,7 +1109,8 @@ public class ProvisioningContext {
             final TaskExecDAO taskExecDAO,
             final AnyTypeDAO anyTypeDAO,
             final ImplementationDAO implementationDAO,
-            final SyncopeTaskScheduler scheduler) {
+            final SyncopeTaskScheduler scheduler,
+            final TemplateUtils templateUtils) {
 
         return new TaskDataBinderImpl(
                 realmSearchDAO,
@@ -1071,7 +1120,8 @@ public class ProvisioningContext {
                 implementationDAO,
                 entityFactory,
                 scheduler,
-                taskUtilsFactory);
+                taskUtilsFactory,
+                templateUtils);
     }
 
     @ConditionalOnMissingBean
@@ -1098,7 +1148,8 @@ public class ProvisioningContext {
             final SecurityQuestionDAO securityQuestionDAO,
             final AccessTokenDAO accessTokenDAO,
             final DelegationDAO delegationDAO,
-            final ConfParamOps confParamOps) {
+            final ConfParamOps confParamOps,
+            final JexlTools jexlTools) {
 
         return new UserDataBinderImpl(
                 anyTypeDAO,
@@ -1122,7 +1173,8 @@ public class ProvisioningContext {
                 accessTokenDAO,
                 delegationDAO,
                 confParamOps,
-                securityProperties);
+                securityProperties,
+                jexlTools);
     }
 
     @ConditionalOnMissingBean
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyDataBinder.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyDataBinder.java
index b803b511ba..e46db5fe01 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyDataBinder.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyDataBinder.java
@@ -83,7 +83,7 @@ import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.api.MappingManager;
 import org.apache.syncope.core.provisioning.api.PlainAttrGetter;
 import org.apache.syncope.core.provisioning.api.PropagationByResource;
-import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.apache.syncope.core.provisioning.java.pushpull.OutboundMatcher;
 import org.apache.syncope.core.provisioning.java.utils.ConnObjectUtils;
 import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
@@ -186,9 +186,10 @@ abstract class AnyDataBinder extends AttributableDataBinder {
             final MappingManager mappingManager,
             final IntAttrNameParser intAttrNameParser,
             final OutboundMatcher outboundMatcher,
-            final PlainAttrValidationManager validator) {
+            final PlainAttrValidationManager validator,
+            final JexlTools jexlTools) {
 
-        super(plainSchemaDAO, validator, derAttrHandler, mappingManager, intAttrNameParser);
+        super(plainSchemaDAO, validator, derAttrHandler, mappingManager, intAttrNameParser, jexlTools);
         this.anyTypeDAO = anyTypeDAO;
         this.realmSearchDAO = realmSearchDAO;
         this.anyTypeClassDAO = anyTypeClassDAO;
@@ -273,7 +274,7 @@ abstract class AnyDataBinder extends AttributableDataBinder {
                         AccountGetter.DEFAULT,
                         PlainAttrGetter.DEFAULT);
                 if (intValues.values().isEmpty()
-                        && JexlUtils.evaluateMandatoryCondition(item.getMandatoryCondition(), any, derAttrHandler)) {
+                        && jexlTools.evaluateMandatoryCondition(item.getMandatoryCondition(), any, derAttrHandler)) {
 
                     missingAttrNames.add(item.getIntAttrName());
                 }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyObjectDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyObjectDataBinderImpl.java
index bec1013f37..ae09217b26 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyObjectDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AnyObjectDataBinderImpl.java
@@ -62,6 +62,7 @@ import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.api.MappingManager;
 import org.apache.syncope.core.provisioning.api.PropagationByResource;
 import org.apache.syncope.core.provisioning.api.data.AnyObjectDataBinder;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.apache.syncope.core.provisioning.java.pushpull.OutboundMatcher;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -84,7 +85,8 @@ public class AnyObjectDataBinderImpl extends AnyDataBinder implements AnyObjectD
             final MappingManager mappingManager,
             final IntAttrNameParser intAttrNameParser,
             final OutboundMatcher outboundMatcher,
-            final PlainAttrValidationManager validator) {
+            final PlainAttrValidationManager validator,
+            final JexlTools jexlTools) {
 
         super(anyTypeDAO,
                 realmSearchDAO,
@@ -101,7 +103,8 @@ public class AnyObjectDataBinderImpl extends AnyDataBinder implements AnyObjectD
                 mappingManager,
                 intAttrNameParser,
                 outboundMatcher,
-                validator);
+                validator,
+                jexlTools);
     }
 
     @Transactional(readOnly = true)
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AttrRepoDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AttrRepoDataBinderImpl.java
index be8566aacb..da1a80feae 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AttrRepoDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AttrRepoDataBinderImpl.java
@@ -27,7 +27,7 @@ import org.apache.syncope.common.lib.types.MappingPurpose;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.am.AttrRepo;
 import org.apache.syncope.core.provisioning.api.data.AttrRepoDataBinder;
-import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -37,8 +37,11 @@ public class AttrRepoDataBinderImpl implements AttrRepoDataBinder {
 
     protected final EntityFactory entityFactory;
 
-    public AttrRepoDataBinderImpl(final EntityFactory entityFactory) {
+    protected final JexlTools jexlTools;
+
+    public AttrRepoDataBinderImpl(final EntityFactory entityFactory, final JexlTools jexlTools) {
         this.entityFactory = entityFactory;
+        this.jexlTools = jexlTools;
     }
 
     protected void populateItems(final AttrRepoTO attrRepoTO, final AttrRepo attrRepo) {
@@ -56,9 +59,8 @@ public class AttrRepoDataBinderImpl implements AttrRepoDataBinder {
                 requiredValuesMissing.getElements().add("intAttrName");
                 scce.addException(requiredValuesMissing);
             } else {
-                // no mandatory condition implies mandatory condition false
-                if (!JexlUtils.isExpressionValid(itemTO.getMandatoryCondition() == null
-                        ? "false" : itemTO.getMandatoryCondition())) {
+                if (itemTO.getMandatoryCondition() != null
+                        && !jexlTools.isExpressionValid(itemTO.getMandatoryCondition())) {
 
                     SyncopeClientException invalidMandatoryCondition =
                             SyncopeClientException.build(ClientExceptionType.InvalidValues);
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AttributableDataBinder.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AttributableDataBinder.java
index 63527412a0..eac3acf877 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AttributableDataBinder.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AttributableDataBinder.java
@@ -35,7 +35,7 @@ import org.apache.syncope.core.persistence.api.entity.PlainSchema;
 import org.apache.syncope.core.provisioning.api.DerAttrHandler;
 import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.api.MappingManager;
-import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.apache.syncope.core.spring.implementation.ImplementationManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -54,6 +54,8 @@ abstract class AttributableDataBinder {
 
     protected final IntAttrNameParser intAttrNameParser;
 
+    protected final JexlTools jexlTools;
+
     private final Map<String, DropdownValueProvider> dropdownValueProviders = new ConcurrentHashMap<>();
 
     protected AttributableDataBinder(
@@ -61,13 +63,15 @@ abstract class AttributableDataBinder {
             final PlainAttrValidationManager validator,
             final DerAttrHandler derAttrHandler,
             final MappingManager mappingManager,
-            final IntAttrNameParser intAttrNameParser) {
+            final IntAttrNameParser intAttrNameParser,
+            final JexlTools jexlTools) {
 
         this.plainSchemaDAO = plainSchemaDAO;
         this.validator = validator;
         this.derAttrHandler = derAttrHandler;
         this.mappingManager = mappingManager;
         this.intAttrNameParser = intAttrNameParser;
+        this.jexlTools = jexlTools;
     }
 
     protected Optional<PlainSchema> getPlainSchema(final String schemaName) {
@@ -95,7 +99,7 @@ abstract class AttributableDataBinder {
 
         if (attr == null
                 && !schema.isReadonly()
-                && JexlUtils.evaluateMandatoryCondition(schema.getMandatoryCondition(), attributable, derAttrHandler)) {
+                && jexlTools.evaluateMandatoryCondition(schema.getMandatoryCondition(), attributable, derAttrHandler)) {
 
             LOG.error("Mandatory schema {} not provided with values", schema.getKey());
 
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuthModuleDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuthModuleDataBinderImpl.java
index 49a3ef8c2e..3a006249cd 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuthModuleDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AuthModuleDataBinderImpl.java
@@ -27,7 +27,7 @@ import org.apache.syncope.common.lib.types.MappingPurpose;
 import org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.am.AuthModule;
 import org.apache.syncope.core.provisioning.api.data.AuthModuleDataBinder;
-import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -37,8 +37,11 @@ public class AuthModuleDataBinderImpl implements AuthModuleDataBinder {
 
     protected final EntityFactory entityFactory;
 
-    public AuthModuleDataBinderImpl(final EntityFactory entityFactory) {
+    protected final JexlTools jexlTools;
+
+    public AuthModuleDataBinderImpl(final EntityFactory entityFactory, final JexlTools jexlTools) {
         this.entityFactory = entityFactory;
+        this.jexlTools = jexlTools;
     }
 
     protected void populateItems(final AuthModuleTO authModuleTO, final AuthModule authModule) {
@@ -56,9 +59,8 @@ public class AuthModuleDataBinderImpl implements AuthModuleDataBinder {
                 requiredValuesMissing.getElements().add("intAttrName");
                 scce.addException(requiredValuesMissing);
             } else {
-                // no mandatory condition implies mandatory condition false
-                if (!JexlUtils.isExpressionValid(itemTO.getMandatoryCondition() == null
-                        ? "false" : itemTO.getMandatoryCondition())) {
+                if (itemTO.getMandatoryCondition() != null
+                        && !jexlTools.isExpressionValid(itemTO.getMandatoryCondition())) {
 
                     SyncopeClientException invalidMandatoryCondition =
                             SyncopeClientException.build(ClientExceptionType.InvalidValues);
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java
index e8508c4e6e..b459ea7ef3 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/GroupDataBinderImpl.java
@@ -67,6 +67,7 @@ import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.api.MappingManager;
 import org.apache.syncope.core.provisioning.api.PropagationByResource;
 import org.apache.syncope.core.provisioning.api.data.GroupDataBinder;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.apache.syncope.core.provisioning.java.pushpull.OutboundMatcher;
 import org.springframework.data.domain.Pageable;
 import org.springframework.transaction.annotation.Transactional;
@@ -93,7 +94,8 @@ public class GroupDataBinderImpl extends AnyDataBinder implements GroupDataBinde
             final IntAttrNameParser intAttrNameParser,
             final OutboundMatcher outboundMatcher,
             final SearchCondVisitor searchCondVisitor,
-            final PlainAttrValidationManager validator) {
+            final PlainAttrValidationManager validator,
+            final JexlTools jexlTools) {
 
         super(anyTypeDAO,
                 realmSearchDAO,
@@ -110,7 +112,8 @@ public class GroupDataBinderImpl extends AnyDataBinder implements GroupDataBinde
                 mappingManager,
                 intAttrNameParser,
                 outboundMatcher,
-                validator);
+                validator,
+                jexlTools);
 
         this.searchCondVisitor = searchCondVisitor;
     }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/JEXLItemTransformerImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/JEXLItemTransformerImpl.java
index 75964a410a..1679e3916c 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/JEXLItemTransformerImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/JEXLItemTransformerImpl.java
@@ -22,7 +22,6 @@ import java.time.OffsetDateTime;
 import java.util.ArrayList;
 import java.util.List;
 import org.apache.commons.jexl3.JexlContext;
-import org.apache.commons.jexl3.MapContext;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.mutable.Mutable;
 import org.apache.commons.lang3.mutable.MutableObject;
@@ -35,11 +34,15 @@ import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
 import org.apache.syncope.core.provisioning.api.DerAttrHandler;
 import org.apache.syncope.core.provisioning.api.MappingManager;
 import org.apache.syncope.core.provisioning.api.data.JEXLItemTransformer;
-import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlContextBuilder;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.springframework.beans.factory.annotation.Autowired;
 
 public class JEXLItemTransformerImpl implements JEXLItemTransformer {
 
+    @Autowired
+    private JexlTools jexlTools;
+
     @Autowired
     private DerAttrHandler derAttrHandler;
 
@@ -62,12 +65,13 @@ public class JEXLItemTransformerImpl implements JEXLItemTransformer {
             final AttrSchemaType schemaType,
             final PlainAttrValue value) {
 
-        JexlContext jexlContext = new MapContext();
+        JexlContextBuilder builder = new JexlContextBuilder();
         if (any != null) {
-            JexlUtils.addFieldsToContext(any, jexlContext);
-            JexlUtils.addPlainAttrsToContext(any.getPlainAttrs(), jexlContext);
-            JexlUtils.addDerAttrsToContext(any, derAttrHandler, jexlContext);
+            builder.fields(any).
+                    plainAttrs(any.getPlainAttrs()).
+                    derAttrs(any, derAttrHandler);
         }
+        JexlContext jexlContext = builder.build();
 
         Object oValue;
         switch (schemaType) {
@@ -100,7 +104,7 @@ public class JEXLItemTransformerImpl implements JEXLItemTransformer {
         }
         jexlContext.set("value", oValue);
 
-        Object tValue = JexlUtils.evaluateExpr(propagationJEXL, jexlContext);
+        Object tValue = jexlTools.evaluateExpression(propagationJEXL, jexlContext);
 
         value.setBinaryValue(null);
         value.setBooleanValue(null);
@@ -172,15 +176,14 @@ public class JEXLItemTransformerImpl implements JEXLItemTransformer {
         if (StringUtils.isNotBlank(pullJEXL) && values != null) {
             List<Object> newValues = new ArrayList<>(values.size());
             values.forEach(value -> {
-                JexlContext jexlContext = new MapContext();
-                jexlContext.set("value", value);
-                JexlUtils.addFieldsToContext(entityTO, jexlContext);
+                JexlContextBuilder builder = new JexlContextBuilder().with("value", value).fields(entityTO);
                 if (entityTO instanceof AnyTO anyTO) {
-                    JexlUtils.addAttrsToContext(anyTO.getPlainAttrs(), jexlContext);
-                    JexlUtils.addAttrsToContext(anyTO.getDerAttrs(), jexlContext);
+                    builder.attrs(anyTO.getPlainAttrs());
+                    builder.attrs(anyTO.getDerAttrs());
                 }
+                JexlContext jexlContext = builder.build();
 
-                newValues.add(JexlUtils.evaluateExpr(pullJEXL, jexlContext));
+                newValues.add(jexlTools.evaluateExpression(pullJEXL, jexlContext));
             });
 
             return newValues;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RealmDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RealmDataBinderImpl.java
index e12f3c4bfa..519f7feb6f 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RealmDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/RealmDataBinderImpl.java
@@ -57,9 +57,9 @@ import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.api.MappingManager;
 import org.apache.syncope.core.provisioning.api.PropagationByResource;
 import org.apache.syncope.core.provisioning.api.data.RealmDataBinder;
-import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
+import org.apache.syncope.core.provisioning.api.jexl.TemplateUtils;
 import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
-import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
 
 public class RealmDataBinderImpl extends AttributableDataBinder implements RealmDataBinder {
 
@@ -77,6 +77,8 @@ public class RealmDataBinderImpl extends AttributableDataBinder implements Realm
 
     protected final EntityFactory entityFactory;
 
+    protected final TemplateUtils templateUtils;
+
     public RealmDataBinderImpl(
             final AnyTypeDAO anyTypeDAO,
             final AnyTypeClassDAO anyTypeClassDAO,
@@ -89,9 +91,11 @@ public class RealmDataBinderImpl extends AttributableDataBinder implements Realm
             final DerAttrHandler derAttrHandler,
             final PlainAttrValidationManager validator,
             final MappingManager mappingManager,
-            final IntAttrNameParser intAttrNameParser) {
+            final IntAttrNameParser intAttrNameParser,
+            final JexlTools jexlTools,
+            final TemplateUtils templateUtils) {
 
-        super(plainSchemaDAO, validator, derAttrHandler, mappingManager, intAttrNameParser);
+        super(plainSchemaDAO, validator, derAttrHandler, mappingManager, intAttrNameParser, jexlTools);
         this.anyTypeDAO = anyTypeDAO;
         this.anyTypeClassDAO = anyTypeClassDAO;
         this.implementationDAO = implementationDAO;
@@ -99,6 +103,7 @@ public class RealmDataBinderImpl extends AttributableDataBinder implements Realm
         this.policyDAO = policyDAO;
         this.resourceDAO = resourceDAO;
         this.entityFactory = entityFactory;
+        this.templateUtils = templateUtils;
     }
 
     protected void fill(
@@ -175,7 +180,7 @@ public class RealmDataBinderImpl extends AttributableDataBinder implements Realm
                 removeIf(implementation -> !realmTO.getActions().contains(implementation.getKey()));
 
         // validate JEXL expressions from templates and proceed if fine
-        TemplateUtils.check(realmTO.getTemplates(), ClientExceptionType.InvalidRealm);
+        templateUtils.check(realmTO.getTemplates(), ClientExceptionType.InvalidRealm);
         realmTO.getTemplates().forEach((key, template) -> anyTypeDAO.findById(key).ifPresentOrElse(
                 type -> {
                     AnyTemplateRealm anyTemplate = realm.getTemplate(type).orElse(null);
@@ -225,7 +230,7 @@ public class RealmDataBinderImpl extends AttributableDataBinder implements Realm
                         schemaType,
                         realm);
                 if (intValues.values().isEmpty()
-                        && JexlUtils.evaluateMandatoryCondition(item.getMandatoryCondition(), realm, derAttrHandler)) {
+                        && jexlTools.evaluateMandatoryCondition(item.getMandatoryCondition(), realm, derAttrHandler)) {
 
                     missingAttrNames.add(item.getIntAttrName());
                 }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderImpl.java
index a83fb9fcdc..168195496c 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderImpl.java
@@ -60,7 +60,7 @@ import org.apache.syncope.core.persistence.api.entity.policy.PushPolicy;
 import org.apache.syncope.core.provisioning.api.IntAttrName;
 import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.api.data.ResourceDataBinder;
-import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskExecutor;
 import org.identityconnectors.framework.common.objects.ObjectClass;
 import org.slf4j.Logger;
@@ -88,6 +88,8 @@ public class ResourceDataBinderImpl implements ResourceDataBinder {
 
     protected final PropagationTaskExecutor propagationTaskExecutor;
 
+    protected final JexlTools jexlTools;
+
     public ResourceDataBinderImpl(
             final AnyTypeDAO anyTypeDAO,
             final ConnInstanceDAO connInstanceDAO,
@@ -97,7 +99,8 @@ public class ResourceDataBinderImpl implements ResourceDataBinder {
             final PlainSchemaDAO plainSchemaDAO,
             final EntityFactory entityFactory,
             final IntAttrNameParser intAttrNameParser,
-            final PropagationTaskExecutor propagationTaskExecutor) {
+            final PropagationTaskExecutor propagationTaskExecutor,
+            final JexlTools jexlTools) {
 
         this.anyTypeDAO = anyTypeDAO;
         this.connInstanceDAO = connInstanceDAO;
@@ -108,6 +111,7 @@ public class ResourceDataBinderImpl implements ResourceDataBinder {
         this.entityFactory = entityFactory;
         this.intAttrNameParser = intAttrNameParser;
         this.propagationTaskExecutor = propagationTaskExecutor;
+        this.jexlTools = jexlTools;
     }
 
     @Override
@@ -265,9 +269,8 @@ public class ResourceDataBinderImpl implements ResourceDataBinder {
                         LOG.error("Only 'name' and 'fullpath' are supported for Realms");
                         invalidMapping.getElements().add("Only 'name' and 'fullpath' are supported for Realms");
                     } else {
-                        // no mandatory condition implies mandatory condition false
-                        if (!JexlUtils.isExpressionValid(itemTO.getMandatoryCondition() == null
-                                ? "false" : itemTO.getMandatoryCondition())) {
+                        if (itemTO.getMandatoryCondition() != null
+                                && !jexlTools.isExpressionValid(itemTO.getMandatoryCondition())) {
 
                             SyncopeClientException invalidMandatoryCondition = SyncopeClientException.build(
                                     ClientExceptionType.InvalidValues);
@@ -415,9 +418,8 @@ public class ResourceDataBinderImpl implements ResourceDataBinder {
                     }
 
                     if (allowed) {
-                        // no mandatory condition implies mandatory condition false
-                        if (!JexlUtils.isExpressionValid(itemTO.getMandatoryCondition() == null
-                                ? "false" : itemTO.getMandatoryCondition())) {
+                        if (itemTO.getMandatoryCondition() != null
+                                && !jexlTools.isExpressionValid(itemTO.getMandatoryCondition())) {
 
                             SyncopeClientException invalidMandatoryCondition = SyncopeClientException.build(
                                     ClientExceptionType.InvalidValues);
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SchemaDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SchemaDataBinderImpl.java
index b447d1dcda..0c8b5e687e 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SchemaDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SchemaDataBinderImpl.java
@@ -41,7 +41,7 @@ import org.apache.syncope.core.persistence.api.entity.EntityFactory;
 import org.apache.syncope.core.persistence.api.entity.Implementation;
 import org.apache.syncope.core.persistence.api.entity.PlainSchema;
 import org.apache.syncope.core.provisioning.api.data.SchemaDataBinder;
-import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -65,6 +65,8 @@ public class SchemaDataBinderImpl implements SchemaDataBinder {
 
     protected final AnyUtilsFactory anyUtilsFactory;
 
+    protected final JexlTools jexlTools;
+
     public SchemaDataBinderImpl(
             final AnyTypeClassDAO anyTypeClassDAO,
             final PlainSchemaDAO plainSchemaDAO,
@@ -73,7 +75,8 @@ public class SchemaDataBinderImpl implements SchemaDataBinder {
             final AnyTypeDAO anyTypeDAO,
             final ImplementationDAO implementationDAO,
             final EntityFactory entityFactory,
-            final AnyUtilsFactory anyUtilsFactory) {
+            final AnyUtilsFactory anyUtilsFactory,
+            final JexlTools jexlTools) {
 
         this.anyTypeClassDAO = anyTypeClassDAO;
         this.plainSchemaDAO = plainSchemaDAO;
@@ -83,11 +86,12 @@ public class SchemaDataBinderImpl implements SchemaDataBinder {
         this.implementationDAO = implementationDAO;
         this.entityFactory = entityFactory;
         this.anyUtilsFactory = anyUtilsFactory;
+        this.jexlTools = jexlTools;
     }
 
     // --------------- PLAIN -----------------
     protected PlainSchema fill(final PlainSchema schema, final PlainSchemaTO schemaTO) {
-        if (!JexlUtils.isExpressionValid(schemaTO.getMandatoryCondition())) {
+        if (!jexlTools.isExpressionValid(schemaTO.getMandatoryCondition())) {
             SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.InvalidValues);
             sce.getElements().add(schemaTO.getMandatoryCondition());
             throw sce;
@@ -225,7 +229,7 @@ public class SchemaDataBinderImpl implements SchemaDataBinder {
             requiredValuesMissing.getElements().add("expression");
 
             scce.addException(requiredValuesMissing);
-        } else if (!JexlUtils.isExpressionValid(schemaTO.getExpression())) {
+        } else if (!jexlTools.isExpressionValid(schemaTO.getExpression())) {
             SyncopeClientException e = SyncopeClientException.build(ClientExceptionType.InvalidValues);
             e.getElements().add(schemaTO.getExpression());
 
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/TaskDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/TaskDataBinderImpl.java
index 441f999d12..48036dbf3c 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/TaskDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/TaskDataBinderImpl.java
@@ -76,6 +76,7 @@ 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.provisioning.api.data.TaskDataBinder;
+import org.apache.syncope.core.provisioning.api.jexl.TemplateUtils;
 import org.apache.syncope.core.provisioning.api.job.JobNamer;
 import org.apache.syncope.core.provisioning.api.macro.MacroActions;
 import org.apache.syncope.core.provisioning.java.job.MacroJobDelegate;
@@ -83,7 +84,6 @@ import org.apache.syncope.core.provisioning.java.job.SyncopeTaskScheduler;
 import org.apache.syncope.core.provisioning.java.pushpull.LiveSyncJobDelegate;
 import org.apache.syncope.core.provisioning.java.pushpull.PullJobDelegate;
 import org.apache.syncope.core.provisioning.java.pushpull.PushJobDelegate;
-import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
 import org.apache.syncope.core.spring.implementation.ImplementationManager;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.slf4j.Logger;
@@ -109,6 +109,8 @@ public class TaskDataBinderImpl extends AbstractExecutableDatabinder implements
 
     protected final TaskUtilsFactory taskUtilsFactory;
 
+    protected final TemplateUtils templateUtils;
+
     protected final Map<String, MacroActions> perContextMacroActions = new ConcurrentHashMap<>();
 
     public TaskDataBinderImpl(
@@ -119,7 +121,8 @@ public class TaskDataBinderImpl extends AbstractExecutableDatabinder implements
             final ImplementationDAO implementationDAO,
             final EntityFactory entityFactory,
             final SyncopeTaskScheduler scheduler,
-            final TaskUtilsFactory taskUtilsFactory) {
+            final TaskUtilsFactory taskUtilsFactory,
+            final TemplateUtils templateUtils) {
 
         this.realmSearchDAO = realmSearchDAO;
         this.resourceDAO = resourceDAO;
@@ -129,6 +132,7 @@ public class TaskDataBinderImpl extends AbstractExecutableDatabinder implements
         this.entityFactory = entityFactory;
         this.scheduler = scheduler;
         this.taskUtilsFactory = taskUtilsFactory;
+        this.templateUtils = templateUtils;
     }
 
     protected void fill(final ProvisioningTask<?> provisioningTask, final ProvisioningTaskTO provisioningTaskTO) {
@@ -207,7 +211,7 @@ public class TaskDataBinderImpl extends AbstractExecutableDatabinder implements
                 }
 
                 // validate JEXL expressions from templates and proceed if fine
-                TemplateUtils.check(liveSyncTaskTO.getTemplates(), ClientExceptionType.InvalidLiveSyncTask);
+                templateUtils.check(liveSyncTaskTO.getTemplates(), ClientExceptionType.InvalidLiveSyncTask);
                 liveSyncTaskTO.getTemplates().forEach((type, template) -> anyTypeDAO.findById(type).ifPresentOrElse(
                         anyType -> {
                             AnyTemplateLiveSyncTask anyTemplate = liveSyncTask.getTemplate(anyType.getKey()).
@@ -255,7 +259,7 @@ public class TaskDataBinderImpl extends AbstractExecutableDatabinder implements
                 }
 
                 // validate JEXL expressions from templates and proceed if fine
-                TemplateUtils.check(pullTaskTO.getTemplates(), ClientExceptionType.InvalidPullTask);
+                templateUtils.check(pullTaskTO.getTemplates(), ClientExceptionType.InvalidPullTask);
                 pullTaskTO.getTemplates().forEach((type, template) -> anyTypeDAO.findById(type).ifPresentOrElse(
                         anyType -> {
                             AnyTemplatePullTask anyTemplate = pullTask.getTemplate(anyType.getKey()).orElse(null);
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java
index 82bd53dbab..910335fb93 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/UserDataBinderImpl.java
@@ -85,6 +85,7 @@ import org.apache.syncope.core.provisioning.api.PropagationByResource;
 import org.apache.syncope.core.provisioning.api.UserWorkflowResult;
 import org.apache.syncope.core.provisioning.api.UserWorkflowResult.PropagationInfo;
 import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.apache.syncope.core.provisioning.java.pushpull.OutboundMatcher;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.apache.syncope.core.spring.security.SecurityProperties;
@@ -127,7 +128,8 @@ public class UserDataBinderImpl extends AnyDataBinder implements UserDataBinder
             final AccessTokenDAO accessTokenDAO,
             final DelegationDAO delegationDAO,
             final ConfParamOps confParamOps,
-            final SecurityProperties securityProperties) {
+            final SecurityProperties securityProperties,
+            final JexlTools jexlTools) {
 
         super(anyTypeDAO,
                 realmSearchDAO,
@@ -144,7 +146,8 @@ public class UserDataBinderImpl extends AnyDataBinder implements UserDataBinder
                 mappingManager,
                 intAttrNameParser,
                 outboundMatcher,
-                validator);
+                validator,
+                jexlTools);
 
         this.roleDAO = roleDAO;
         this.securityQuestionDAO = securityQuestionDAO;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/MacroJobDelegate.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/MacroJobDelegate.java
index 628bde58a2..fb7201f0d3 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/MacroJobDelegate.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/MacroJobDelegate.java
@@ -52,7 +52,7 @@ import org.apache.syncope.core.persistence.api.entity.task.MacroTask;
 import org.apache.syncope.core.persistence.api.entity.task.MacroTaskCommand;
 import org.apache.syncope.core.persistence.api.entity.task.TaskExec;
 import org.apache.syncope.core.persistence.api.utils.FormatUtils;
-import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.apache.syncope.core.provisioning.api.job.JobExecutionContext;
 import org.apache.syncope.core.provisioning.api.job.JobExecutionException;
 import org.apache.syncope.core.provisioning.api.macro.Command;
@@ -78,6 +78,9 @@ public class MacroJobDelegate extends AbstractSchedTaskJobDelegate<MacroTask> {
     @Resource(name = "batchExecutor")
     protected AsyncTaskExecutor taskExecutor;
 
+    @Autowired
+    protected JexlTools jexlTools;
+
     protected final Map<String, MacroActions> perContextActions = new ConcurrentHashMap<>();
 
     protected final Map<String, Command<?>> perContextCommands = new ConcurrentHashMap<>();
@@ -299,14 +302,13 @@ public class MacroJobDelegate extends AbstractSchedTaskJobDelegate<MacroTask> {
             } else {
                 args = command.getArgs();
 
-                jexlContext.ifPresent(jc -> ReflectionUtils.doWithFields(
-                        args.getClass(),
+                jexlContext.ifPresent(jc -> ReflectionUtils.doWithFields(args.getClass(),
                         field -> {
                             if (String.class.equals(field.getType())) {
                                 field.setAccessible(true);
                                 Object value = field.get(args);
                                 if (value instanceof final String s) {
-                                    field.set(args, JexlUtils.evaluateTemplate(s, jc));
+                                    field.set(args, jexlTools.evaluateTemplate(s, jc));
                                 }
                             }
                         },
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/DefaultNotificationManager.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/DefaultNotificationManager.java
index 0ceaa46fe7..3fb10bea32 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/DefaultNotificationManager.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/notification/DefaultNotificationManager.java
@@ -68,7 +68,7 @@ import org.apache.syncope.core.provisioning.api.data.AnyObjectDataBinder;
 import org.apache.syncope.core.provisioning.api.data.GroupDataBinder;
 import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
 import org.apache.syncope.core.provisioning.api.event.AfterHandlingEvent;
-import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.apache.syncope.core.provisioning.api.notification.NotificationManager;
 import org.apache.syncope.core.provisioning.api.notification.RecipientsProvider;
 import org.apache.syncope.core.spring.implementation.ImplementationManager;
@@ -114,6 +114,8 @@ public class DefaultNotificationManager implements NotificationManager {
 
     protected final SearchCondVisitor searchCondVisitor;
 
+    protected final JexlTools jexlTools;
+
     protected Optional<RecipientsProvider> perContextRecipientsProvider = Optional.empty();
 
     public DefaultNotificationManager(
@@ -132,7 +134,8 @@ public class DefaultNotificationManager implements NotificationManager {
             final ConfParamOps confParamOps,
             final EntityFactory entityFactory,
             final IntAttrNameParser intAttrNameParser,
-            final SearchCondVisitor searchCondVisitor) {
+            final SearchCondVisitor searchCondVisitor,
+            final JexlTools jexlTools) {
 
         this.derSchemaDAO = derSchemaDAO;
         this.notificationDAO = notificationDAO;
@@ -150,6 +153,7 @@ public class DefaultNotificationManager implements NotificationManager {
         this.entityFactory = entityFactory;
         this.intAttrNameParser = intAttrNameParser;
         this.searchCondVisitor = searchCondVisitor;
+        this.jexlTools = jexlTools;
     }
 
     @Transactional(readOnly = true)
@@ -222,13 +226,13 @@ public class DefaultNotificationManager implements NotificationManager {
         task.setTraceLevel(notification.getTraceLevel());
         task.getRecipients().addAll(recipientEmails);
         task.setSender(notification.getSender());
-        task.setSubject(JexlUtils.evaluateTemplate(notification.getSubject(), ctx));
+        task.setSubject(jexlTools.evaluateTemplate(notification.getSubject(), ctx));
 
         if (StringUtils.isNotBlank(notification.getTemplate().getTextTemplate())) {
-            task.setTextBody(JexlUtils.evaluateTemplate(notification.getTemplate().getTextTemplate(), ctx));
+            task.setTextBody(jexlTools.evaluateTemplate(notification.getTemplate().getTextTemplate(), ctx));
         }
         if (StringUtils.isNotBlank(notification.getTemplate().getHTMLTemplate())) {
-            task.setHtmlBody(JexlUtils.evaluateTemplate(notification.getTemplate().getHTMLTemplate(), ctx));
+            task.setHtmlBody(jexlTools.evaluateTemplate(notification.getTemplate().getHTMLTemplate(), ctx));
         }
 
         return task;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationManager.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationManager.java
index 48af445ea6..8b0d5d9751 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationManager.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/DefaultPropagationManager.java
@@ -55,7 +55,7 @@ import org.apache.syncope.core.provisioning.api.DerAttrHandler;
 import org.apache.syncope.core.provisioning.api.MappingManager;
 import org.apache.syncope.core.provisioning.api.PropagationByResource;
 import org.apache.syncope.core.provisioning.api.UserWorkflowResult;
-import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationManager;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
 import org.apache.syncope.core.provisioning.java.utils.ConnObjectUtils;
@@ -94,13 +94,16 @@ public class DefaultPropagationManager implements PropagationManager {
 
     protected final AnyUtilsFactory anyUtilsFactory;
 
+    protected final JexlTools jexlTools;
+
     public DefaultPropagationManager(
             final ExternalResourceDAO resourceDAO,
             final EntityFactory entityFactory,
             final ConnObjectUtils connObjectUtils,
             final MappingManager mappingManager,
             final DerAttrHandler derAttrHandler,
-            final AnyUtilsFactory anyUtilsFactory) {
+            final AnyUtilsFactory anyUtilsFactory,
+            final JexlTools jexlTools) {
 
         this.resourceDAO = resourceDAO;
         this.entityFactory = entityFactory;
@@ -108,6 +111,7 @@ public class DefaultPropagationManager implements PropagationManager {
         this.mappingManager = mappingManager;
         this.derAttrHandler = derAttrHandler;
         this.anyUtilsFactory = anyUtilsFactory;
+        this.jexlTools = jexlTools;
     }
 
     @Override
@@ -385,7 +389,7 @@ public class DefaultPropagationManager implements PropagationManager {
         List<String> mandatoryMissing = new ArrayList<>();
         List<String> mandatoryNullOrEmpty = new ArrayList<>();
         mappingItems.filter(item -> !item.isConnObjectKey()
-                && JexlUtils.evaluateMandatoryCondition(item.getMandatoryCondition(), any, derAttrHandler)).
+                && jexlTools.evaluateMandatoryCondition(item.getMandatoryCondition(), any, derAttrHandler)).
                 forEach(item -> {
 
                     Attribute attr = AttributeUtil.find(item.getExtAttrName(), preparedAttrs.attributes());
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/LDAPMembershipPropagationActions.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/LDAPMembershipPropagationActions.java
index c2de716df7..d4c5f310e4 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/LDAPMembershipPropagationActions.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/LDAPMembershipPropagationActions.java
@@ -25,7 +25,6 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.TreeSet;
 import org.apache.commons.jexl3.JexlContext;
-import org.apache.commons.jexl3.MapContext;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.request.MembershipUR;
 import org.apache.syncope.common.lib.request.UserUR;
@@ -41,7 +40,8 @@ import org.apache.syncope.core.persistence.api.entity.group.Group;
 import org.apache.syncope.core.persistence.api.entity.task.PropagationData;
 import org.apache.syncope.core.persistence.api.entity.user.User;
 import org.apache.syncope.core.provisioning.api.DerAttrHandler;
-import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlContextBuilder;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationActions;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
 import org.apache.syncope.core.spring.implementation.InstanceScope;
@@ -66,6 +66,9 @@ public class LDAPMembershipPropagationActions implements PropagationActions {
 
     protected static final Logger LOG = LoggerFactory.getLogger(LDAPMembershipPropagationActions.class);
 
+    @Autowired
+    protected JexlTools jexlTools;
+
     @Autowired
     protected DerAttrHandler derAttrHandler;
 
@@ -87,12 +90,13 @@ public class LDAPMembershipPropagationActions implements PropagationActions {
     protected String evaluateGroupConnObjectLink(final String connObjectLinkTemplate, final Group group) {
         LOG.debug("Evaluating connObjectLink for {}", group);
 
-        JexlContext jexlContext = new MapContext();
-        JexlUtils.addFieldsToContext(group, jexlContext);
-        JexlUtils.addPlainAttrsToContext(group.getPlainAttrs(), jexlContext);
-        JexlUtils.addDerAttrsToContext(group, derAttrHandler, jexlContext);
+        JexlContext jexlContext = new JexlContextBuilder().
+                fields(group).
+                plainAttrs(group.getPlainAttrs()).
+                derAttrs(group, derAttrHandler).
+                build();
 
-        return JexlUtils.evaluateExpr(connObjectLinkTemplate, jexlContext).toString();
+        return jexlTools.evaluateExpression(connObjectLinkTemplate, jexlContext).toString();
     }
 
     protected void buildManagedGroupConnObjectLinks(
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/SinglePullJobDelegate.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/SinglePullJobDelegate.java
index 329ca5b328..5fdbd767df 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/SinglePullJobDelegate.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/SinglePullJobDelegate.java
@@ -39,6 +39,7 @@ import org.apache.syncope.core.persistence.api.entity.ExternalResource;
 import org.apache.syncope.core.persistence.api.entity.task.AnyTemplatePullTask;
 import org.apache.syncope.core.persistence.api.entity.task.PullTask;
 import org.apache.syncope.core.provisioning.api.Connector;
+import org.apache.syncope.core.provisioning.api.jexl.TemplateUtils;
 import org.apache.syncope.core.provisioning.api.job.JobExecutionException;
 import org.apache.syncope.core.provisioning.api.pushpull.InboundActions;
 import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningProfile;
@@ -46,7 +47,6 @@ import org.apache.syncope.core.provisioning.api.pushpull.ReconFilterBuilder;
 import org.apache.syncope.core.provisioning.api.pushpull.SyncopePullResultHandler;
 import org.apache.syncope.core.provisioning.api.pushpull.SyncopeSinglePullExecutor;
 import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
-import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
 import org.identityconnectors.framework.common.objects.ObjectClass;
 import org.springframework.beans.factory.annotation.Autowired;
 
@@ -58,6 +58,9 @@ public class SinglePullJobDelegate extends PullJobDelegate implements SyncopeSin
     @Autowired
     protected RealmSearchDAO realmSearchDAO;
 
+    @Autowired
+    protected TemplateUtils templateUtils;
+
     @Override
     public List<ProvisioningReport> pull(
             final ExternalResource resource,
@@ -89,7 +92,7 @@ public class SinglePullJobDelegate extends PullJobDelegate implements SyncopeSin
             task.setRemediation(pullTaskTO.isRemediation());
 
             // validate JEXL expressions from templates and proceed if fine
-            TemplateUtils.check(pullTaskTO.getTemplates(), ClientExceptionType.InvalidPullTask);
+            templateUtils.check(pullTaskTO.getTemplates(), ClientExceptionType.InvalidPullTask);
             pullTaskTO.getTemplates().forEach((type, template) -> anyTypeDAO.findById(type).ifPresentOrElse(
                     anyType -> {
                         AnyTemplatePullTask anyTemplate = task.getTemplate(anyType.getKey()).orElse(null);
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/ConnObjectUtils.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/ConnObjectUtils.java
index df79c2ecad..bde38d1b7f 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/ConnObjectUtils.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/ConnObjectUtils.java
@@ -50,6 +50,7 @@ import org.apache.syncope.core.persistence.api.entity.policy.PasswordPolicy;
 import org.apache.syncope.core.persistence.api.entity.task.InboundTask;
 import org.apache.syncope.core.persistence.api.entity.user.User;
 import org.apache.syncope.core.provisioning.api.MappingManager;
+import org.apache.syncope.core.provisioning.api.jexl.TemplateUtils;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 import org.apache.syncope.core.spring.security.PasswordGenerator;
 import org.identityconnectors.common.security.GuardedByteArray;
diff --git a/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/OIDCC4UILogicContext.java b/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/OIDCC4UILogicContext.java
index c3c2e88318..be6614e234 100644
--- a/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/OIDCC4UILogicContext.java
+++ b/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/OIDCC4UILogicContext.java
@@ -31,8 +31,8 @@ import org.apache.syncope.core.provisioning.api.UserProvisioningManager;
 import org.apache.syncope.core.provisioning.api.data.AccessTokenDataBinder;
 import org.apache.syncope.core.provisioning.api.data.OIDCC4UIProviderDataBinder;
 import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
+import org.apache.syncope.core.provisioning.api.jexl.TemplateUtils;
 import org.apache.syncope.core.provisioning.java.pushpull.InboundMatcher;
-import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
 import org.apache.syncope.core.spring.security.AuthDataAccessor;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
diff --git a/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCUserManager.java b/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCUserManager.java
index 9fc7a35d32..48a8a29a5a 100644
--- a/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCUserManager.java
+++ b/ext/oidcc4ui/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCUserManager.java
@@ -48,9 +48,9 @@ import org.apache.syncope.core.provisioning.api.ProvisioningManager;
 import org.apache.syncope.core.provisioning.api.UserProvisioningManager;
 import org.apache.syncope.core.provisioning.api.data.ItemTransformer;
 import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
+import org.apache.syncope.core.provisioning.api.jexl.TemplateUtils;
 import org.apache.syncope.core.provisioning.java.pushpull.InboundMatcher;
 import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
-import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
 import org.apache.syncope.core.spring.implementation.ImplementationManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
diff --git a/ext/oidcc4ui/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/OIDCC4UIProviderDataBinderImpl.java b/ext/oidcc4ui/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/OIDCC4UIProviderDataBinderImpl.java
index b78c2b4e8b..76f09062ba 100644
--- a/ext/oidcc4ui/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/OIDCC4UIProviderDataBinderImpl.java
+++ b/ext/oidcc4ui/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/OIDCC4UIProviderDataBinderImpl.java
@@ -38,7 +38,7 @@ import org.apache.syncope.core.persistence.api.entity.OIDCC4UIUserTemplate;
 import org.apache.syncope.core.provisioning.api.IntAttrName;
 import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.api.data.OIDCC4UIProviderDataBinder;
-import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -56,18 +56,22 @@ public class OIDCC4UIProviderDataBinderImpl implements OIDCC4UIProviderDataBinde
 
     protected final IntAttrNameParser intAttrNameParser;
 
+    protected final JexlTools jexlTools;
+
     public OIDCC4UIProviderDataBinderImpl(
             final AnyTypeDAO anyTypeDAO,
             final OIDCC4UIProviderDAO oidcOPDAO,
             final ImplementationDAO implementationDAO,
             final OIDCC4UIEntityFactory entityFactory,
-            final IntAttrNameParser intAttrNameParser) {
+            final IntAttrNameParser intAttrNameParser,
+            final JexlTools jexlTools) {
 
         this.anyTypeDAO = anyTypeDAO;
         this.oidcOPDAO = oidcOPDAO;
         this.implementationDAO = implementationDAO;
         this.entityFactory = entityFactory;
         this.intAttrNameParser = intAttrNameParser;
+        this.jexlTools = jexlTools;
     }
 
     @Override
@@ -101,9 +105,8 @@ public class OIDCC4UIProviderDataBinderImpl implements OIDCC4UIProviderDataBinde
                     LOG.error("'{}' not existing", itemTO.getIntAttrName());
                     invalidMapping.getElements().add('\'' + itemTO.getIntAttrName() + "' not existing");
                 } else {
-                    // no mandatory condition implies mandatory condition false
-                    if (!JexlUtils.isExpressionValid(itemTO.getMandatoryCondition() == null
-                            ? "false" : itemTO.getMandatoryCondition())) {
+                    if (itemTO.getMandatoryCondition() != null
+                            && !jexlTools.isExpressionValid(itemTO.getMandatoryCondition())) {
 
                         SyncopeClientException invalidMandatoryCondition = SyncopeClientException.build(
                                 ClientExceptionType.InvalidValues);
diff --git a/ext/oidcc4ui/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/OIDCC4UIProvisioningContext.java b/ext/oidcc4ui/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/OIDCC4UIProvisioningContext.java
index 844584a090..5dc7811a79 100644
--- a/ext/oidcc4ui/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/OIDCC4UIProvisioningContext.java
+++ b/ext/oidcc4ui/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/OIDCC4UIProvisioningContext.java
@@ -24,6 +24,7 @@ import org.apache.syncope.core.persistence.api.dao.OIDCC4UIProviderDAO;
 import org.apache.syncope.core.persistence.api.entity.OIDCC4UIEntityFactory;
 import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.api.data.OIDCC4UIProviderDataBinder;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -38,13 +39,15 @@ public class OIDCC4UIProvisioningContext {
             final OIDCC4UIProviderDAO oidcOPDAO,
             final ImplementationDAO implementationDAO,
             final OIDCC4UIEntityFactory entityFactory,
-            final IntAttrNameParser intAttrNameParser) {
+            final IntAttrNameParser intAttrNameParser,
+            final JexlTools jexlTools) {
 
         return new OIDCC4UIProviderDataBinderImpl(
                 anyTypeDAO,
                 oidcOPDAO,
                 implementationDAO,
                 entityFactory,
-                intAttrNameParser);
+                intAttrNameParser,
+                jexlTools);
     }
 }
diff --git a/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/SAML2SP4UILogicContext.java b/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/SAML2SP4UILogicContext.java
index 10a084b613..51f439cace 100644
--- a/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/SAML2SP4UILogicContext.java
+++ b/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/SAML2SP4UILogicContext.java
@@ -30,8 +30,8 @@ import org.apache.syncope.core.provisioning.api.UserProvisioningManager;
 import org.apache.syncope.core.provisioning.api.data.AccessTokenDataBinder;
 import org.apache.syncope.core.provisioning.api.data.SAML2SP4UIIdPDataBinder;
 import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
+import org.apache.syncope.core.provisioning.api.jexl.TemplateUtils;
 import org.apache.syncope.core.provisioning.java.pushpull.InboundMatcher;
-import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
 import org.apache.syncope.core.spring.security.AuthDataAccessor;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
diff --git a/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2SP4UIUserManager.java b/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2SP4UIUserManager.java
index 3c6bbe1d49..8459b88ad0 100644
--- a/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2SP4UIUserManager.java
+++ b/ext/saml2sp4ui/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2SP4UIUserManager.java
@@ -50,9 +50,9 @@ import org.apache.syncope.core.provisioning.api.SAML2SP4UIIdPActions;
 import org.apache.syncope.core.provisioning.api.UserProvisioningManager;
 import org.apache.syncope.core.provisioning.api.data.ItemTransformer;
 import org.apache.syncope.core.provisioning.api.data.UserDataBinder;
+import org.apache.syncope.core.provisioning.api.jexl.TemplateUtils;
 import org.apache.syncope.core.provisioning.java.pushpull.InboundMatcher;
 import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
-import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
 import org.apache.syncope.core.spring.implementation.ImplementationManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
diff --git a/ext/saml2sp4ui/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/SAML2SP4UIProvisioningContext.java b/ext/saml2sp4ui/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/SAML2SP4UIProvisioningContext.java
index 47296eb0db..fa7cfa65be 100644
--- a/ext/saml2sp4ui/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/SAML2SP4UIProvisioningContext.java
+++ b/ext/saml2sp4ui/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/SAML2SP4UIProvisioningContext.java
@@ -24,6 +24,7 @@ import org.apache.syncope.core.persistence.api.dao.SAML2SP4UIIdPDAO;
 import org.apache.syncope.core.persistence.api.entity.SAML2SP4UIEntityFactory;
 import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.api.data.SAML2SP4UIIdPDataBinder;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.apache.syncope.core.provisioning.java.data.SAML2SP4UIIdPDataBinderImpl;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.annotation.Bean;
@@ -39,13 +40,15 @@ public class SAML2SP4UIProvisioningContext {
             final SAML2SP4UIIdPDAO idapDAO,
             final ImplementationDAO implementationDAO,
             final SAML2SP4UIEntityFactory entityFactory,
-            final IntAttrNameParser intAttrNameParser) {
+            final IntAttrNameParser intAttrNameParser,
+            final JexlTools jexlTools) {
 
         return new SAML2SP4UIIdPDataBinderImpl(
                 anyTypeDAO,
                 idapDAO,
                 implementationDAO,
                 entityFactory,
-                intAttrNameParser);
+                intAttrNameParser,
+                jexlTools);
     }
 }
diff --git a/ext/saml2sp4ui/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2SP4UIIdPDataBinderImpl.java b/ext/saml2sp4ui/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2SP4UIIdPDataBinderImpl.java
index ddb21fcf54..5bf8c4d66f 100644
--- a/ext/saml2sp4ui/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2SP4UIIdPDataBinderImpl.java
+++ b/ext/saml2sp4ui/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2SP4UIIdPDataBinderImpl.java
@@ -39,7 +39,7 @@ import org.apache.syncope.core.persistence.api.entity.SAML2SP4UIUserTemplate;
 import org.apache.syncope.core.provisioning.api.IntAttrName;
 import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.api.data.SAML2SP4UIIdPDataBinder;
-import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -57,18 +57,22 @@ public class SAML2SP4UIIdPDataBinderImpl implements SAML2SP4UIIdPDataBinder {
 
     protected final IntAttrNameParser intAttrNameParser;
 
+    protected final JexlTools jexlTools;
+
     public SAML2SP4UIIdPDataBinderImpl(
             final AnyTypeDAO anyTypeDAO,
             final SAML2SP4UIIdPDAO idapDAO,
             final ImplementationDAO implementationDAO,
             final SAML2SP4UIEntityFactory entityFactory,
-            final IntAttrNameParser intAttrNameParser) {
+            final IntAttrNameParser intAttrNameParser,
+            final JexlTools jexlTools) {
 
         this.anyTypeDAO = anyTypeDAO;
         this.idapDAO = idapDAO;
         this.implementationDAO = implementationDAO;
         this.entityFactory = entityFactory;
         this.intAttrNameParser = intAttrNameParser;
+        this.jexlTools = jexlTools;
     }
 
     @Override
@@ -102,9 +106,8 @@ public class SAML2SP4UIIdPDataBinderImpl implements SAML2SP4UIIdPDataBinder {
                     LOG.error("'{}' not existing", itemTO.getIntAttrName());
                     invalidMapping.getElements().add('\'' + itemTO.getIntAttrName() + "' not existing");
                 } else {
-                    // no mandatory condition implies mandatory condition false
-                    if (!JexlUtils.isExpressionValid(itemTO.getMandatoryCondition() == null
-                            ? "false" : itemTO.getMandatoryCondition())) {
+                    if (itemTO.getMandatoryCondition() != null
+                            && !jexlTools.isExpressionValid(itemTO.getMandatoryCondition())) {
 
                         SyncopeClientException invalidMandatoryCondition = SyncopeClientException.build(
                                 ClientExceptionType.InvalidValues);
diff --git a/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMDataBinder.java b/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMDataBinder.java
index 22eb5dda26..c7a95330e9 100644
--- a/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMDataBinder.java
+++ b/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMDataBinder.java
@@ -69,7 +69,8 @@ import org.apache.syncope.core.persistence.api.dao.search.MembershipCond;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
 import org.apache.syncope.core.persistence.api.entity.ExternalResource;
 import org.apache.syncope.core.persistence.api.entity.user.UMembership;
-import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlContextBuilder;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.apache.syncope.core.spring.security.AuthDataAccessor;
 import org.apache.syncope.ext.scimv2.api.BadRequestException;
 import org.apache.syncope.ext.scimv2.api.data.Group;
@@ -139,16 +140,20 @@ public class SCIMDataBinder {
 
     protected final GroupDAO groupDAO;
 
+    protected final JexlTools jexlTools;
+
     public SCIMDataBinder(
             final SCIMConfManager confManager,
             final UserLogic userLogic,
             final AuthDataAccessor authDataAccessor,
-            final GroupDAO groupDAO) {
+            final GroupDAO groupDAO,
+            final JexlTools jexlTools) {
 
         this.confManager = confManager;
         this.userLogic = userLogic;
         this.authDataAccessor = authDataAccessor;
         this.groupDAO = groupDAO;
+        this.jexlTools = jexlTools;
     }
 
     protected <E extends Enum<?>> void fill(
@@ -524,11 +529,10 @@ public class SCIMDataBinder {
         }
 
         switch (schema) {
-            case "name":
+            case "name" ->
                 groupTO.setName(value);
-                break;
 
-            default:
+            default ->
                 groupTO.getPlainAttrs().add(new Attr.Builder(schema).value(value).build());
         }
     }
@@ -568,12 +572,10 @@ public class SCIMDataBinder {
 
         values.stream().filter(value -> value.getType() != null).forEach(value -> confs.stream().
                 filter(object -> value.getType().equals(object.getType().name())
-                && attrPatches.stream().noneMatch(attrPatch ->
-                attrPatch.getAttr().getSchema().equals(object.getValue()))).findFirst().
-                ifPresent(conf -> attrs.add(
-                new Attr.Builder(conf.getValue()).value(value.getValue()).build())));
+                && attrPatches.stream().noneMatch(p -> p.getAttr().getSchema().equals(object.getValue()))).findFirst().
+                ifPresent(conf -> attrs.add(new Attr.Builder(conf.getValue()).value(value.getValue()).build())));
     }
-    
+
     public void populateUserUR(
             final UserUR userUR,
             final UserTO before,
@@ -720,48 +722,48 @@ public class SCIMDataBinder {
                     forEach(address -> conf.getUserConf().getAddresses().stream().
                     filter(object -> address.getType().equals(object.getType().name())).findFirst().
                     ifPresent(addressConf -> {
-                setAttribute(
-                        before,
-                        userUR,
-                        addressConf.getFormatted(),
-                        address.getFormatted(),
-                        op);
+                        setAttribute(
+                                before,
+                                userUR,
+                                addressConf.getFormatted(),
+                                address.getFormatted(),
+                                op);
 
-                setAttribute(
-                        before,
-                        userUR,
-                        addressConf.getStreetAddress(),
-                        address.getStreetAddress(),
-                        op);
+                        setAttribute(
+                                before,
+                                userUR,
+                                addressConf.getStreetAddress(),
+                                address.getStreetAddress(),
+                                op);
 
-                setAttribute(
-                        before,
-                        userUR,
-                        addressConf.getLocality(),
-                        address.getLocality(),
-                        op);
+                        setAttribute(
+                                before,
+                                userUR,
+                                addressConf.getLocality(),
+                                address.getLocality(),
+                                op);
 
-                setAttribute(
-                        before,
-                        userUR,
-                        addressConf.getRegion(),
-                        address.getRegion(),
-                        op);
+                        setAttribute(
+                                before,
+                                userUR,
+                                addressConf.getRegion(),
+                                address.getRegion(),
+                                op);
 
-                setAttribute(
-                        before,
-                        userUR,
-                        addressConf.getPostalCode(),
-                        address.getPostalCode(),
-                        op);
+                        setAttribute(
+                                before,
+                                userUR,
+                                addressConf.getPostalCode(),
+                                address.getPostalCode(),
+                                op);
 
-                setAttribute(
-                        before,
-                        userUR,
-                        addressConf.getCountry(),
-                        address.getCountry(),
-                        op);
-            }));
+                        setAttribute(
+                                before,
+                                userUR,
+                                addressConf.getCountry(),
+                                address.getCountry(),
+                                op);
+                    }));
 
             for (int i = 0; i < user.getX509Certificates().size(); i++) {
                 Value certificate = user.getX509Certificates().get(i);
@@ -827,23 +829,15 @@ public class SCIMDataBinder {
                     before, userUR, syncopeAttr, user.getExtensionInfo().getAttributes().get(scimAttr), op));
         }
 
-        user.getGroups().forEach(group -> {
-            if (before.getMembership(group.getValue()).isEmpty()
-                    && userUR.getMemberships().stream().noneMatch(membershipUR ->
-                    membershipUR.getGroup().equals(group.getValue()))) {
-                userUR.getMemberships().add(new MembershipUR.Builder(group.getValue())
-                        .operation(PatchOperation.ADD_REPLACE).build());
-            }
-        });
+        user.getGroups().stream().
+                filter(group -> before.getMembership(group.getValue()).isEmpty()
+                && userUR.getMemberships().stream().noneMatch(m -> m.getGroup().equals(group.getValue()))).
+                forEach(group -> userUR.getMemberships().add(new MembershipUR.Builder(group.getValue()).build()));
 
-        user.getRoles().forEach(role -> {
-            if (!before.getRoles().contains(role.getValue())
-                    && userUR.getRoles().stream().noneMatch(roleUR ->
-                    roleUR.getValue().equals(role.getValue()))) {
-                userUR.getRoles().add(new StringPatchItem.Builder()
-                        .value(role.getValue()).operation(PatchOperation.ADD_REPLACE).build());
-            }
-        });
+        user.getRoles().stream().
+                filter(role -> !before.getRoles().contains(role.getValue())
+                && userUR.getRoles().stream().noneMatch(roleUR -> roleUR.getValue().equals(role.getValue()))).
+                forEach(role -> userUR.getRoles().add(new StringPatchItem.Builder().value(role.getValue()).build()));
     }
 
     public UserTO toUserTO(final SCIMUser user, final boolean checkSchemas) {
@@ -1068,28 +1062,29 @@ public class SCIMDataBinder {
         switch (schema) {
             case "username" -> {
                 if (!value.equals(before.getUsername()) && userUR.getUsername() == null) {
-                    userUR.setUsername(
-                            new StringReplacePatchItem.Builder().value(value).operation(PatchOperation.ADD_REPLACE)
-                                    .build());
+                    userUR.setUsername(new StringReplacePatchItem.Builder().value(value).build());
                 }
             }
 
             default -> {
                 if ((before.getPlainAttr(schema).isEmpty()
                         || !value.equals(before.getPlainAttr(schema).get().getValues().getFirst()))
-                        && userUR.getPlainAttrs().stream().noneMatch(attrPatch ->
-                        attrPatch.getAttr().getSchema().equals(schema))
+                        && userUR.getPlainAttrs().stream().noneMatch(p -> p.getAttr().getSchema().equals(schema))
                         && op.getOp() != PatchOp.remove) {
-                    userUR.getPlainAttrs()
-                            .add(new AttrPatch.Builder(new Attr.Builder(schema).value(value).build()).operation(
-                                    PatchOperation.ADD_REPLACE).build());
+
+                    userUR.getPlainAttrs().add(
+                            new AttrPatch.Builder(new Attr.Builder(schema).value(value).build()).
+                                    operation(PatchOperation.ADD_REPLACE).
+                                    build());
                 }
                 if (before.getPlainAttr(schema).isPresent()
-                        && userUR.getPlainAttrs().stream().noneMatch(attrPatch ->
-                        attrPatch.getAttr().getSchema().equals(schema))
+                        && userUR.getPlainAttrs().stream().noneMatch(p -> p.getAttr().getSchema().equals(schema))
                         && op.getOp() == PatchOp.remove) {
-                    userUR.getPlainAttrs().add(new AttrPatch.Builder(new Attr.Builder(schema).build()).operation(
-                            PatchOperation.DELETE).build());
+
+                    userUR.getPlainAttrs().add(
+                            new AttrPatch.Builder(new Attr.Builder(schema).build()).
+                                    operation(PatchOperation.DELETE).
+                                    build());
                 }
             }
         }
@@ -1114,7 +1109,7 @@ public class SCIMDataBinder {
             final SCIMPatchOperation op) {
 
         confs.stream().
-                filter(conf -> BooleanUtils.toBoolean(JexlUtils.evaluateExpr(
+                filter(conf -> BooleanUtils.toBoolean(jexlTools.evaluateExpression(
                 filter2JexlExpression(op.getPath().getFilter()),
                 new MapContext(Map.of("type", conf.getType().name()))).toString())).findFirst().
                 ifPresent(conf -> {
@@ -1194,7 +1189,7 @@ public class SCIMDataBinder {
                     if (group != null && before.getMembership(groupKey).isEmpty()) {
                         List<? extends ExternalResource> filteredResources = group.getResources().stream()
                                 .filter(resource -> resource.getProvisions().stream()
-                                        .anyMatch(provision -> AnyTypeKind.USER.name().equals(provision.getAnyType())))
+                                .anyMatch(provision -> AnyTypeKind.USER.name().equals(provision.getAnyType())))
                                 .toList();
                         filteredResources.forEach(resource -> resources.add(resource.getKey()));
                         if (!filteredResources.isEmpty()) {
@@ -1215,9 +1210,9 @@ public class SCIMDataBinder {
 
             switch (op.getPath().getAttribute()) {
                 case "externalId" ->
-                        setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
-                                conf.getUserConf().getExternalId(),
-                                op);
+                    setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                            conf.getUserConf().getExternalId(),
+                            op);
 
                 case "userName" -> {
                     if (op.getOp() != PatchOp.remove && !CollectionUtils.isEmpty(op.getValue())) {
@@ -1279,37 +1274,38 @@ public class SCIMDataBinder {
                 }
 
                 case "displayName" ->
-                        setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
-                                conf.getUserConf().getDisplayName(),
-                                op);
+                    setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                            conf.getUserConf().getDisplayName(),
+                            op);
 
                 case "nickName" ->
-                        setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
-                                conf.getUserConf().getNickName(), op);
+                    setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                            conf.getUserConf().getNickName(), op);
 
                 case "profileUrl" ->
-                        setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
-                                conf.getUserConf().getProfileUrl(),
-                                op);
+                    setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                            conf.getUserConf().getProfileUrl(),
+                            op);
 
                 case "title" ->
-                        setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
-                                conf.getUserConf().getTitle(), op);
+                    setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                            conf.getUserConf().getTitle(), op);
 
                 case "userType" ->
-                        setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
-                                conf.getUserConf().getUserType(), op);
+                    setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                            conf.getUserConf().getUserType(), op);
 
-                case "preferredLanguage" -> setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
-                        conf.getUserConf().getPreferredLanguage(), op);
+                case "preferredLanguage" ->
+                    setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                            conf.getUserConf().getPreferredLanguage(), op);
 
                 case "locale" ->
-                        setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
-                                conf.getUserConf().getLocale(), op);
+                    setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                            conf.getUserConf().getLocale(), op);
 
                 case "timezone" ->
-                        setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
-                                conf.getUserConf().getTimezone(), op);
+                    setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                            conf.getUserConf().getTimezone(), op);
 
                 case "emails" -> {
                     if (!CollectionUtils.isEmpty(op.getValue())
@@ -1361,54 +1357,62 @@ public class SCIMDataBinder {
                 }
 
                 case "addresses" -> {
-                    if (!CollectionUtils.isEmpty(op.getValue()) && op.getValue()
-                            .getFirst() instanceof final SCIMUser after) {
-                        after.getAddresses().stream().filter(address -> address.getType() != null).forEach(
+                    if (!CollectionUtils.isEmpty(op.getValue())
+                            && op.getValue().getFirst() instanceof final SCIMUser after) {
+
+                        after.getAddresses().stream().
+                                filter(address -> address.getType() != null).forEach(
                                 address -> conf.getUserConf().getAddresses().stream()
-                                        .filter(object -> address.getType().equals(object.getType().name())).findFirst()
-                                        .ifPresent(addressConf ->
-                                                setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
-                                                        addressConf, op)));
+                                        .filter(ac -> address.getType().equals(ac.getType().name())).findFirst()
+                                        .ifPresent(ac -> setAttribute(userURs.get(numberUR.get().intValue()).
+                                        getPlainAttrs(), ac, op)));
                     } else if (op.getPath().getFilter() != null) {
-                        conf.getUserConf().getAddresses().stream().filter(addressConf -> BooleanUtils.toBoolean(
-                                        JexlUtils.evaluateExpr(filter2JexlExpression(op.getPath().getFilter()),
-                                                new MapContext(Map.of("type", addressConf.getType().name()))).
-                                                toString()))
-                                .findFirst().ifPresent(addressConf ->
-                                        setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
-                                                addressConf, op));
+                        conf.getUserConf().getAddresses().stream().
+                                filter(address -> BooleanUtils.toBoolean(
+                                jexlTools.evaluateExpression(filter2JexlExpression(op.getPath().getFilter()),
+                                        new JexlContextBuilder().with("type", address.getType().name()).build()).
+                                        toString())).findFirst()
+                                .ifPresent(ac -> setAttribute(userURs.get(numberUR.get().intValue()).
+                                getPlainAttrs(), ac, op));
                     }
                 }
 
-                case "employeeNumber" -> setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
-                        Optional.ofNullable(conf.getEnterpriseUserConf()).map(SCIMEnterpriseUserConf::getEmployeeNumber)
-                                .orElse(null), op);
-
-                case "costCenter" -> setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
-                        Optional.ofNullable(conf.getEnterpriseUserConf()).map(SCIMEnterpriseUserConf::getCostCenter)
-                                .orElse(null), op);
-
-                case "organization" -> setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
-                        Optional.ofNullable(conf.getEnterpriseUserConf()).map(SCIMEnterpriseUserConf::getOrganization)
-                                .orElse(null), op);
-
-                case "division" -> setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
-                        Optional.ofNullable(conf.getEnterpriseUserConf()).map(SCIMEnterpriseUserConf::getDivision)
-                                .orElse(null), op);
-
-                case "department" -> setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
-                        Optional.ofNullable(conf.getEnterpriseUserConf()).map(SCIMEnterpriseUserConf::getDepartment)
-                                .orElse(null), op);
-
-                case "manager" -> setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
-                        Optional.ofNullable(conf.getEnterpriseUserConf()).map(SCIMEnterpriseUserConf::getManager)
-                                .map(SCIMManagerConf::getKey).orElse(null), op);
+                case "employeeNumber" ->
+                    setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                            Optional.ofNullable(conf.getEnterpriseUserConf()).
+                                    map(SCIMEnterpriseUserConf::getEmployeeNumber).orElse(null), op);
+
+                case "costCenter" ->
+                    setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                            Optional.ofNullable(conf.getEnterpriseUserConf()).
+                                    map(SCIMEnterpriseUserConf::getCostCenter).orElse(null), op);
+
+                case "organization" ->
+                    setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                            Optional.ofNullable(conf.getEnterpriseUserConf()).
+                                    map(SCIMEnterpriseUserConf::getOrganization).orElse(null), op);
+
+                case "division" ->
+                    setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                            Optional.ofNullable(conf.getEnterpriseUserConf()).
+                                    map(SCIMEnterpriseUserConf::getDivision).orElse(null), op);
+
+                case "department" ->
+                    setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                            Optional.ofNullable(conf.getEnterpriseUserConf()).
+                                    map(SCIMEnterpriseUserConf::getDepartment).orElse(null), op);
+
+                case "manager" ->
+                    setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
+                            Optional.ofNullable(conf.getEnterpriseUserConf()).
+                                    map(SCIMEnterpriseUserConf::getManager).map(SCIMManagerConf::getKey).
+                                    orElse(null), op);
 
                 default -> {
                     Optional.ofNullable(conf.getExtensionUserConf())
                             .flatMap(schema -> Optional.ofNullable(schema.asMap().get(op.getPath().getAttribute())))
                             .ifPresent(schema -> setAttribute(userURs.get(numberUR.get().intValue()).getPlainAttrs(),
-                                    schema, op));
+                            schema, op));
                 }
             }
         });
@@ -1579,7 +1583,7 @@ public class SCIMDataBinder {
                 schemas,
                 new Meta(
                         "urn:ietf:params:scim:schemas:extension:syncope:2.0:"
-                                + scimExtensionAnyObjectConf.getType(),
+                        + scimExtensionAnyObjectConf.getType(),
                         anyObjectTO.getCreationDate(),
                         Optional.ofNullable(anyObjectTO.getLastChangeDate()).orElse(anyObjectTO.getCreationDate()),
                         anyObjectTO.getETagValue(),
@@ -1614,14 +1618,12 @@ public class SCIMDataBinder {
     public AnyObjectTO toAnyObjectTO(final SCIMAnyObject anyObject, final boolean checkSchemas) {
         SCIMConf conf = confManager.get();
         Set<String> expectedSchemas = new HashSet<>();
-        Optional<SCIMExtensionAnyObjectConf> scimExtensionAnyObjectConf =
-                conf.getExtensionAnyObjectsConf().stream()
-                        .filter(scimExtAnyObjectConf ->
-                                scimExtAnyObjectConf.getType().equals(anyObject.getExtensionUrn()
-                                        .substring(anyObject.getExtensionUrn().lastIndexOf(':') + 1)))
-                        .findFirst();
-        scimExtensionAnyObjectConf.ifPresent(scimExtAnyObjectConf ->
-                expectedSchemas.add("urn:ietf:params:scim:schemas:extension:syncope:2.0:"
+        Optional<SCIMExtensionAnyObjectConf> scimExtensionAnyObjectConf = conf.getExtensionAnyObjectsConf().stream()
+                .filter(ec -> ec.getType().equals(anyObject.getExtensionUrn()
+                .substring(anyObject.getExtensionUrn().lastIndexOf(':') + 1)))
+                .findFirst();
+        scimExtensionAnyObjectConf.ifPresent(scimExtAnyObjectConf
+                -> expectedSchemas.add("urn:ietf:params:scim:schemas:extension:syncope:2.0:"
                         + scimExtAnyObjectConf.getType()));
         if (checkSchemas
                 && (!anyObject.getSchemas().containsAll(expectedSchemas)
@@ -1683,7 +1685,7 @@ public class SCIMDataBinder {
                 setAttribute(anyObjectUR.getPlainAttrs(), scimExtensionAnyObjectConf.get().getExternalId(), op);
             }
             scimExtensionAnyObjectConf.flatMap(extensionAnyObjectConf -> Optional.of(extensionAnyObjectConf)
-                            .flatMap(schema -> Optional.ofNullable(schema.asMap().get(op.getPath().getAttribute()))))
+                    .flatMap(schema -> Optional.ofNullable(schema.asMap().get(op.getPath().getAttribute()))))
                     .ifPresent(schema -> setAttribute(anyObjectUR.getPlainAttrs(), schema, op));
         }
 
diff --git a/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMLogicContext.java b/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMLogicContext.java
index 968dd69dfc..63f21217e2 100644
--- a/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMLogicContext.java
+++ b/ext/scimv2/logic/src/main/java/org/apache/syncope/core/logic/SCIMLogicContext.java
@@ -22,6 +22,7 @@ import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
 import org.apache.syncope.core.logic.init.SCIMLoader;
 import org.apache.syncope.core.logic.scim.SCIMConfManager;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.apache.syncope.core.spring.security.AuthDataAccessor;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.annotation.Bean;
@@ -48,9 +49,10 @@ public class SCIMLogicContext {
             final SCIMConfManager confManager,
             final UserLogic userLogic,
             final AuthDataAccessor authDataAccessor,
-            final GroupDAO groupDAO) {
+            final GroupDAO groupDAO,
+            final JexlTools jexlTools) {
 
-        return new SCIMDataBinder(confManager, userLogic, authDataAccessor,  groupDAO);
+        return new SCIMDataBinder(confManager, userLogic, authDataAccessor, groupDAO, jexlTools);
     }
 
     @ConditionalOnMissingBean
diff --git a/ext/scimv2/logic/src/test/java/org/apache/syncope/core/logic/SCIMDataBinderTest.java b/ext/scimv2/logic/src/test/java/org/apache/syncope/core/logic/SCIMDataBinderTest.java
index 0a88e094b3..87f3af282b 100644
--- a/ext/scimv2/logic/src/test/java/org/apache/syncope/core/logic/SCIMDataBinderTest.java
+++ b/ext/scimv2/logic/src/test/java/org/apache/syncope/core/logic/SCIMDataBinderTest.java
@@ -30,6 +30,9 @@ import java.util.List;
 import java.util.Optional;
 import java.util.UUID;
 import java.util.stream.Stream;
+import org.apache.commons.jexl3.JexlBuilder;
+import org.apache.commons.jexl3.JexlEngine;
+import org.apache.commons.jexl3.introspection.JexlPermissions;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.Attr;
 import org.apache.syncope.common.lib.SyncopeConstants;
@@ -46,6 +49,8 @@ import org.apache.syncope.common.lib.types.StatusRType;
 import org.apache.syncope.core.logic.scim.SCIMConfManager;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.entity.ExternalResource;
+import org.apache.syncope.core.provisioning.api.jexl.EmptyClassLoader;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.apache.syncope.core.spring.security.AuthDataAccessor;
 import org.apache.syncope.ext.scimv2.api.data.Group;
 import org.apache.syncope.ext.scimv2.api.data.SCIMPatchOp;
@@ -85,7 +90,17 @@ class SCIMDataBinderTest {
         UserLogic userLogic = mock(UserLogic.class);
         AuthDataAccessor authDataAccessor = mock(AuthDataAccessor.class);
         groupDAO = mock(GroupDAO.class);
-        dataBinder = new SCIMDataBinder(scimConfManager, userLogic, authDataAccessor, groupDAO);
+
+        JexlEngine jexlEngine = new JexlBuilder().
+                loader(new EmptyClassLoader()).
+                permissions(JexlPermissions.RESTRICTED.compose("java.time.*", "org.apache.syncope.*")).
+                cache(512).
+                silent(false).
+                strict(false).
+                create();
+        JexlTools jexlTools = new JexlTools(jexlEngine);
+
+        dataBinder = new SCIMDataBinder(scimConfManager, userLogic, authDataAccessor, groupDAO, jexlTools);
     }
 
     @ParameterizedTest
@@ -116,10 +131,10 @@ class SCIMDataBinderTest {
         assertNull(result.getRight());
         assertEquals(1, result.getLeft().size());
         assertEquals(1, result.getLeft().getFirst().getPlainAttrs().size());
-        assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch ->
-                PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
-                        && attrPatch.getAttr().getSchema().equals("surname")
-                        && attrPatch.getAttr().getValues().contains("Rossini")));
+        assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch
+                -> PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
+                && attrPatch.getAttr().getSchema().equals("surname")
+                && attrPatch.getAttr().getValues().contains("Rossini")));
 
         operations.clear();
         operations.add(getOperation("name", "givenName", PatchOp.add, "Gioacchino"));
@@ -130,14 +145,14 @@ class SCIMDataBinderTest {
         assertNull(result.getRight());
         assertEquals(1, result.getLeft().size());
         assertEquals(2, result.getLeft().getFirst().getPlainAttrs().size());
-        assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch ->
-                PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
-                        && attrPatch.getAttr().getSchema().equals("firstname")
-                        && attrPatch.getAttr().getValues().contains("Gioacchino")));
-        assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch ->
-                PatchOperation.DELETE.equals(attrPatch.getOperation())
-                        && attrPatch.getAttr().getSchema().equals("surname")
-                        &&  attrPatch.getAttr().getValues().isEmpty()));
+        assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch
+                -> PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
+                && attrPatch.getAttr().getSchema().equals("firstname")
+                && attrPatch.getAttr().getValues().contains("Gioacchino")));
+        assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch
+                -> PatchOperation.DELETE.equals(attrPatch.getOperation())
+                && attrPatch.getAttr().getSchema().equals("surname")
+                && attrPatch.getAttr().getValues().isEmpty()));
 
         operations.clear();
         operations.add(getOperation("name", "familyName", PatchOp.add, "Verdi"));
@@ -151,14 +166,14 @@ class SCIMDataBinderTest {
         assertEquals(2, result.getLeft().getFirst().getPlainAttrs().size());
         assertEquals(PatchOperation.ADD_REPLACE, result.getLeft().getFirst().getUsername().getOperation());
         assertEquals("gverdi", result.getLeft().getFirst().getUsername().getValue());
-        assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch ->
-                PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
-                        && attrPatch.getAttr().getSchema().equals("surname")
-                        && attrPatch.getAttr().getValues().contains("Verdi")));
-        assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch ->
-                PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
-                        && attrPatch.getAttr().getSchema().equals("firstname")
-                        && attrPatch.getAttr().getValues().contains("Giuseppe")));
+        assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch
+                -> PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
+                && attrPatch.getAttr().getSchema().equals("surname")
+                && attrPatch.getAttr().getValues().contains("Verdi")));
+        assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch
+                -> PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
+                && attrPatch.getAttr().getSchema().equals("firstname")
+                && attrPatch.getAttr().getValues().contains("Giuseppe")));
 
         operations.clear();
         operations.add(getOperation("name", "familyName", PatchOp.replace, "Puccini"));
@@ -172,14 +187,14 @@ class SCIMDataBinderTest {
         assertEquals(StatusRType.REACTIVATE, result.getRight().getType());
         assertEquals(1, result.getLeft().size());
         assertEquals(2, result.getLeft().getFirst().getPlainAttrs().size());
-        assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch ->
-                PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
-                        && attrPatch.getAttr().getSchema().equals("surname")
-                        && attrPatch.getAttr().getValues().contains("Puccini")));
-        assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch ->
-                PatchOperation.DELETE.equals(attrPatch.getOperation())
-                        && attrPatch.getAttr().getSchema().equals("firstname")
-                        &&  attrPatch.getAttr().getValues().isEmpty()));
+        assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch
+                -> PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
+                && attrPatch.getAttr().getSchema().equals("surname")
+                && attrPatch.getAttr().getValues().contains("Puccini")));
+        assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch
+                -> PatchOperation.DELETE.equals(attrPatch.getOperation())
+                && attrPatch.getAttr().getSchema().equals("firstname")
+                && attrPatch.getAttr().getValues().isEmpty()));
 
         UserTO userTO = new UserTO();
         userTO.setUsername("bellini");
@@ -211,10 +226,10 @@ class SCIMDataBinderTest {
         assertEquals(PatchOperation.ADD_REPLACE, result.getLeft().getFirst().getUsername().getOperation());
         assertEquals("bellini", result.getLeft().getFirst().getUsername().getValue());
         assertEquals(1, result.getLeft().getFirst().getPlainAttrs().size());
-        assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch ->
-                PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
-                        && attrPatch.getAttr().getSchema().equals("surname")
-                        && attrPatch.getAttr().getValues().contains("Bellini")));
+        assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch
+                -> PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
+                && attrPatch.getAttr().getSchema().equals("surname")
+                && attrPatch.getAttr().getValues().contains("Bellini")));
 
         userTO.setUsername("bellini");
         userTO.setSuspended(true);
@@ -230,14 +245,14 @@ class SCIMDataBinderTest {
         assertEquals(1, result.getLeft().size());
         assertNull(result.getLeft().getFirst().getUsername());
         assertEquals(1, result.getLeft().getFirst().getPlainAttrs().size());
-        assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch ->
-                PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
-                        && attrPatch.getAttr().getSchema().equals("firstname")
-                        && attrPatch.getAttr().getValues().contains("Gioacchino")));
+        assertTrue(result.getLeft().getFirst().getPlainAttrs().stream().anyMatch(attrPatch
+                -> PatchOperation.ADD_REPLACE.equals(attrPatch.getOperation())
+                && attrPatch.getAttr().getSchema().equals("firstname")
+                && attrPatch.getAttr().getValues().contains("Gioacchino")));
         assertEquals(1, result.getLeft().getFirst().getRoles().size());
-        assertTrue(result.getLeft().getFirst().getRoles().stream().anyMatch(role ->
-                PatchOperation.ADD_REPLACE.equals(role.getOperation())
-                        && role.getValue().equals("User reviewer")));
+        assertTrue(result.getLeft().getFirst().getRoles().stream().anyMatch(role
+                -> PatchOperation.ADD_REPLACE.equals(role.getOperation())
+                && role.getValue().equals("User reviewer")));
 
         userTO = new UserTO();
         Group group = new Group("37d15e4c-cdc1-460b-a591-8505c8133806", null, "root", null);
@@ -302,16 +317,16 @@ class SCIMDataBinderTest {
         assertEquals(3, result.getLeft().size());
         assertTrue(result.getLeft().get(0).isEmpty());
         assertEquals(2, result.getLeft().get(1).getMemberships().size());
-        assertTrue(result.getLeft().get(1).getMemberships().stream().anyMatch(membershipUR ->
-                PatchOperation.ADD_REPLACE.equals(membershipUR.getOperation())
-                        && membershipUR.getGroup().equals("37d15e4c-cdc1-460b-a591-8505c8133806")));
-        assertTrue(result.getLeft().get(1).getMemberships().stream().anyMatch(membershipUR ->
-                PatchOperation.ADD_REPLACE.equals(membershipUR.getOperation())
-                        && membershipUR.getGroup().equals("29f96485-729e-4d31-88a1-6fc60e4677f3")));
+        assertTrue(result.getLeft().get(1).getMemberships().stream().anyMatch(membershipUR
+                -> PatchOperation.ADD_REPLACE.equals(membershipUR.getOperation())
+                && membershipUR.getGroup().equals("37d15e4c-cdc1-460b-a591-8505c8133806")));
+        assertTrue(result.getLeft().get(1).getMemberships().stream().anyMatch(membershipUR
+                -> PatchOperation.ADD_REPLACE.equals(membershipUR.getOperation())
+                && membershipUR.getGroup().equals("29f96485-729e-4d31-88a1-6fc60e4677f3")));
         assertEquals(1, result.getLeft().get(2).getMemberships().size());
-        assertTrue(result.getLeft().get(2).getMemberships().stream().anyMatch(membershipUR ->
-                PatchOperation.ADD_REPLACE.equals(membershipUR.getOperation())
-                        && membershipUR.getGroup().equals("f779c0d4-633b-4be5-8f57-32eb478a3ca5")));
+        assertTrue(result.getLeft().get(2).getMemberships().stream().anyMatch(membershipUR
+                -> PatchOperation.ADD_REPLACE.equals(membershipUR.getOperation())
+                && membershipUR.getGroup().equals("f779c0d4-633b-4be5-8f57-32eb478a3ca5")));
     }
 
     private SCIMPatchOperation getOperation(
diff --git a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/SCIMv2RESTCXFContext.java b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/SCIMv2RESTCXFContext.java
index 15bc81232b..f5a0a228c3 100644
--- a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/SCIMv2RESTCXFContext.java
+++ b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/SCIMv2RESTCXFContext.java
@@ -38,6 +38,7 @@ import org.apache.syncope.core.logic.scim.SCIMConfManager;
 import org.apache.syncope.core.persistence.api.dao.AnyObjectDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.apache.syncope.ext.scimv2.api.service.SCIMAnyObjectService;
 import org.apache.syncope.ext.scimv2.api.service.SCIMGroupService;
 import org.apache.syncope.ext.scimv2.api.service.SCIMService;
@@ -122,8 +123,16 @@ public class SCIMv2RESTCXFContext {
             final SCIMConfManager confManager,
             final SCIMLogic scimLogic) {
 
-        return new SCIMServiceImpl(userDAO, groupDAO, anyObjectDAO, userLogic, groupLogic, anyObjectLogic, binder,
-                confManager, scimLogic);
+        return new SCIMServiceImpl(
+                userDAO,
+                groupDAO,
+                anyObjectDAO,
+                userLogic,
+                groupLogic,
+                anyObjectLogic,
+                binder,
+                confManager,
+                scimLogic);
     }
 
     @ConditionalOnMissingBean
@@ -136,10 +145,19 @@ public class SCIMv2RESTCXFContext {
             final GroupLogic groupLogic,
             final AnyObjectLogic anyObjectLogic,
             final SCIMDataBinder binder,
-            final SCIMConfManager confManager) {
-
-        return new SCIMGroupServiceImpl(userDAO, groupDAO, anyObjectDAO, userLogic, groupLogic, anyObjectLogic, binder,
-                confManager);
+            final SCIMConfManager confManager,
+            final JexlTools jexlTools) {
+
+        return new SCIMGroupServiceImpl(
+                userDAO,
+                groupDAO,
+                anyObjectDAO,
+                userLogic,
+                groupLogic,
+                anyObjectLogic,
+                binder,
+                confManager,
+                jexlTools);
     }
 
     @ConditionalOnMissingBean
@@ -154,7 +172,14 @@ public class SCIMv2RESTCXFContext {
             final SCIMDataBinder binder,
             final SCIMConfManager confManager) {
 
-        return new SCIMUserServiceImpl(userDAO, groupDAO, anyObjectDAO, userLogic, groupLogic, anyObjectLogic, binder,
+        return new SCIMUserServiceImpl(
+                userDAO,
+                groupDAO,
+                anyObjectDAO,
+                userLogic,
+                groupLogic,
+                anyObjectLogic,
+                binder,
                 confManager);
     }
 
@@ -170,7 +195,14 @@ public class SCIMv2RESTCXFContext {
             final SCIMDataBinder binder,
             final SCIMConfManager confManager) {
 
-        return new SCIMAnyObjectServiceImpl(userDAO, groupDAO, anyObjectDAO, userLogic, groupLogic, anyObjectLogic,
-                binder, confManager);
+        return new SCIMAnyObjectServiceImpl(
+                userDAO,
+                groupDAO,
+                anyObjectDAO,
+                userLogic,
+                groupLogic,
+                anyObjectLogic,
+                binder,
+                confManager);
     }
 }
diff --git a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMGroupServiceImpl.java b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMGroupServiceImpl.java
index 1266596644..24343ff89b 100644
--- a/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMGroupServiceImpl.java
+++ b/ext/scimv2/scim-rest-cxf/src/main/java/org/apache/syncope/ext/scimv2/cxf/service/SCIMGroupServiceImpl.java
@@ -50,7 +50,7 @@ import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.dao.search.MembershipCond;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
-import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlTools;
 import org.apache.syncope.ext.scimv2.api.BadRequestException;
 import org.apache.syncope.ext.scimv2.api.data.ListResponse;
 import org.apache.syncope.ext.scimv2.api.data.Member;
@@ -68,6 +68,8 @@ import org.springframework.util.CollectionUtils;
 
 public class SCIMGroupServiceImpl extends AbstractSCIMService<SCIMGroup> implements SCIMGroupService {
 
+    protected final JexlTools jexlTools;
+
     public SCIMGroupServiceImpl(
             final UserDAO userDAO,
             final GroupDAO groupDAO,
@@ -76,9 +78,11 @@ public class SCIMGroupServiceImpl extends AbstractSCIMService<SCIMGroup> impleme
             final GroupLogic groupLogic,
             final AnyObjectLogic anyObjectLogic,
             final SCIMDataBinder binder,
-            final SCIMConfManager confManager) {
+            final SCIMConfManager confManager,
+            final JexlTools jexlTools) {
 
         super(userDAO, groupDAO, anyObjectDAO, userLogic, groupLogic, anyObjectLogic, binder, confManager);
+        this.jexlTools = jexlTools;
     }
 
     private void changeMembership(final String user, final String group, final PatchOp patchOp) {
@@ -157,7 +161,7 @@ public class SCIMGroupServiceImpl extends AbstractSCIMService<SCIMGroup> impleme
                 if (CollectionUtils.isEmpty(op.getValue())) {
                     members(id).stream().filter(member -> op.getPath().getFilter() == null
                             ? true
-                            : BooleanUtils.toBoolean(JexlUtils.evaluateExpr(
+                            : BooleanUtils.toBoolean(jexlTools.evaluateExpression(
                                     SCIMDataBinder.filter2JexlExpression(op.getPath().getFilter()),
                                     new MapContext(Map.of("value", member))).toString())).
                             forEach(member -> changeMembership(member, id, op.getOp()));
diff --git a/pom.xml b/pom.xml
index 6fa7a86d41..e0bc5bf019 100644
--- a/pom.xml
+++ b/pom.xml
@@ -457,7 +457,7 @@ under the License.
 
     <debezium.version>3.0</debezium.version>
 
-    <commons-lang3.version>3.19.0</commons-lang3.version>
+    <commons-lang3.version>3.20.0</commons-lang3.version>
     <commons-jexl.version>3.6.0</commons-jexl.version>
     <commons-text.version>1.14.0</commons-text.version>
 
@@ -524,7 +524,7 @@ under the License.
     <cargo.deployable.ping.timeout>60000</cargo.deployable.ping.timeout>
 
     <tomcat.version>11.0.14</tomcat.version>
-    <wildfly.version>38.0.0.Final</wildfly.version>
+    <wildfly.version>38.0.1.Final</wildfly.version>
     <payara.version>6.2025.11</payara.version>
     <jakarta.faces.version>4.1.4</jakarta.faces.version>
 
diff --git a/src/main/asciidoc/reference-guide/concepts/implementations.adoc b/src/main/asciidoc/reference-guide/concepts/implementations.adoc
index d74ed98c1d..1c7ec5f3fd 100644
--- a/src/main/asciidoc/reference-guide/concepts/implementations.adoc
+++ b/src/main/asciidoc/reference-guide/concepts/implementations.adoc
@@ -41,7 +41,7 @@ ifeval::["{snapshotOrRelease}" == "release"]
 https://github.com/ilgrosso/syncope/blob/syncope-{docVersion}/core/spring/src/main/resources/META-INF/groovy.blacklist[groovy.blacklist^]
 endif::[]
 ifeval::["{snapshotOrRelease}" == "snapshot"]
-https://github.com/ilgrosso/syncope/blob/4_0_X/core/spring/src/main/resources/META-INF/groovy.blacklist[groovy.blacklist^]
+https://github.com/ilgrosso/syncope/blob/master/core/spring/src/main/resources/META-INF/groovy.blacklist[groovy.blacklist^]
 endif::[]
 .
 ====
diff --git a/src/main/asciidoc/reference-guide/usage/core.adoc b/src/main/asciidoc/reference-guide/usage/core.adoc
index 1710e90d91..93264fda9b 100644
--- a/src/main/asciidoc/reference-guide/usage/core.adoc
+++ b/src/main/asciidoc/reference-guide/usage/core.adoc
@@ -66,12 +66,12 @@ header value.
 ====
 The relevant security configuration lies in
 ifeval::["{snapshotOrRelease}" == "release"]
-https://github.com/apache/syncope/blob/syncope-{docVersion}/core/spring/src/main/resources/securityContext.xml[securityContext.xml^];
+https://github.com/apache/syncope/blob/syncope-{docVersion}/core/spring/src/main/java/org/apache/syncope/core/spring/security/WebSecurityContext.java[WebSecurityContext^];
 endif::[]
 ifeval::["{snapshotOrRelease}" == "snapshot"]
-https://github.com/apache/syncope/blob/master/core/spring/src/main/resources/securityContext.xml[securityContext.xml^];
+https://github.com/apache/syncope/blob/master/core/spring/src/main/java/org/apache/syncope/core/spring/security/WebSecurityContext.java[WebSecurityContext^];
 endif::[]
-while normally not needed, this configuration can be anyway customized via the <<override-behavior,override behavior>>.
+while normally not needed, this configuration can be anyway <<extending-configuration,customized>>.
 
 https://en.wikipedia.org/wiki/Basic_access_authentication[HTTP Basic Authentication] is set for use by default.
 ====
@@ -561,3 +561,15 @@ for example, `/a/b/c` with prefix `o=isp,` becomes `o=isp,ou=c,ou=b,ou=a`
 connector object, or empty list if not found
 * `syncope:base64Encode(value)` - encodes the given byte array as Base64-encoded string
 * `syncope:base64Decode(value)` - decodes the given string as byte array using Base64 encoding
+
+[TIP]
+====
+More custom functions can be added by providing implementations of the
+ifeval::["{snapshotOrRelease}" == "release"]
+https://github.com/apache/syncope/blob/syncope-{docVersion}/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/JexlFunctions.java[JexlFunctions^]
+endif::[]
+ifeval::["{snapshotOrRelease}" == "snapshot"]
+https://github.com/apache/syncope/blob/master/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/JexlFunctions.java[JexlFunctions^]
+endif::[]
+interface and registering them as beans, taking care of chosing unique namespace identifiers other than `syncope`.
+====