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 2019/12/30 17:09:32 UTC

[syncope] 04/04: [SYNCOPE-1531] Core support

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

commit 4f63cab0c52253d6b6022e39a13b1be8f101a510
Author: Francesco Chicchiriccò <il...@apache.org>
AuthorDate: Mon Dec 30 15:13:20 2019 +0100

    [SYNCOPE-1531] Core support
---
 .../common/rest/api/beans/AbstractCSVSpec.java     | 229 ++++++++++++++++++
 .../syncope/common/rest/api/beans/CSVPullSpec.java | 136 +++++++++++
 .../syncope/common/rest/api/beans/CSVPushSpec.java | 139 +++++++++++
 .../rest/api/service/ReconciliationService.java    |  33 +++
 .../syncope/common/lib/to/AccessTokenTO.java       |   1 -
 .../syncope/common/lib/to}/ProvisioningReport.java |   2 +-
 .../syncope/common/rest/api/RESTHeaders.java       |   7 +-
 core/idm/logic/pom.xml                             |  65 +++++
 .../syncope/core/logic/ReconciliationLogic.java    | 219 ++++++++++++++++-
 .../apache/syncope/core/logic}/AbstractTest.java   |  14 +-
 .../syncope/core/logic/DummyConfParamOps.java}     |  27 ++-
 .../apache/syncope/core/logic/DummyDomainOps.java  |  59 +++++
 .../core/logic/DummyImplementationLookup.java      |  90 +++++++
 .../syncope/core/logic/DummyServiceOps.java}       |  29 ++-
 .../syncope/core/logic/IdMLogicTestContext.java    |  56 +++++
 .../core/logic/ReconciliationLogicTest.java        | 153 ++++++++++++
 .../apache/syncope/core/logic/TestInitializer.java |  70 ++++++
 core/idm/logic/src/test/resources/logicTest.xml    |  65 +++++
 core/idm/logic/src/test/resources/test1.csv        |   3 +
 .../cxf/service/ReconciliationServiceImpl.java     |  43 ++++
 .../core/rest/cxf/service/ResourceServiceImpl.java |   3 +-
 .../apache/syncope/core/logic/AnyObjectLogic.java  |   9 +-
 .../org/apache/syncope/core/logic/GroupLogic.java  |  18 +-
 .../org/apache/syncope/core/logic/TaskLogic.java   |  10 +-
 .../org/apache/syncope/core/logic/UserLogic.java   |  15 +-
 .../jpa/dao/MyJPAJSONPlainSchemaDAO.java           |   2 +-
 .../jpa/dao/PGJPAJSONPlainSchemaDAO.java           |   2 +-
 .../core/persistence/jpa/dao/AbstractAnyDAO.java   |  18 +-
 .../persistence/jpa/dao/JPAPlainSchemaDAO.java     |   8 +-
 .../jpa/entity/resource/JPAExternalResource.java   |   8 +-
 core/provisioning-api/pom.xml                      |  29 ++-
 .../syncope/core/provisioning/api/Connector.java   |  55 ++++-
 .../core/provisioning/api}/IntAttrNameParser.java  |   3 +-
 .../provisioning/api/UserProvisioningManager.java  |   2 +-
 .../provisioning/api}/jexl/ClassFreeUberspect.java |   2 +-
 .../provisioning/api}/jexl/EmptyClassLoader.java   |   2 +-
 .../core/provisioning/api}/jexl/JexlUtils.java     |  20 +-
 .../api}/jexl/SyncopeJexlFunctions.java            |   2 +-
 .../api/propagation/PropagationManager.java        |  19 ++
 .../api/propagation/PropagationTaskExecutor.java   |   4 +-
 .../api/propagation/PropagationTaskInfo.java       |  65 ++++-
 .../api/pushpull/ProvisioningProfile.java          |   1 +
 .../provisioning/api/pushpull/PullActions.java     |   1 +
 .../provisioning/api/pushpull/PushActions.java     |   1 +
 .../api/pushpull/SyncopeSinglePullExecutor.java    |   1 +
 .../api/pushpull/SyncopeSinglePushExecutor.java    |   1 +
 .../api/pushpull/stream/StreamConnector.java       | 208 ++++++++++++++++
 .../SyncopeStreamPullExecutor.java}                |  23 +-
 .../SyncopeStreamPushExecutor.java}                |  26 +-
 .../provisioning/api}/IntAttrNameParserTest.java   | 117 ++++++++-
 core/provisioning-java/pom.xml                     |  10 +-
 .../provisioning/java/ConnectorFacadeProxy.java    |  54 +----
 .../java/DefaultUserProvisioningManager.java       |   2 +-
 .../core/provisioning/java/DerAttrHandlerImpl.java |   2 +-
 .../core/provisioning/java/MappingManagerImpl.java | 130 ++++++++--
 .../provisioning/java/ProvisioningContext.java     |   1 +
 .../java/data/AbstractAnyDataBinder.java           |  17 +-
 .../java/data/JEXLItemTransformerImpl.java         |   9 +-
 .../java/data/NotificationDataBinderImpl.java      |   2 +-
 .../java/data/ResourceDataBinderImpl.java          |   4 +-
 .../java/data/SchemaDataBinderImpl.java            |   2 +-
 .../provisioning/java/data/UserDataBinderImpl.java |   8 +-
 .../notification/DefaultNotificationManager.java   |   4 +-
 .../AbstractPropagationTaskExecutor.java           |  31 ++-
 .../LDAPMembershipPropagationActions.java          |  18 +-
 .../PriorityPropagationTaskExecutor.java           |  49 +---
 .../java/propagation/PropagationManagerImpl.java   |  75 +++---
 .../pushpull/AbstractProvisioningJobDelegate.java  | 114 +++++----
 .../java/pushpull/AbstractPullResultHandler.java   |  18 +-
 .../java/pushpull/AbstractPushResultHandler.java   |  22 +-
 .../java/pushpull/DBPasswordPullActions.java       |   2 +-
 .../DefaultAnyObjectPullResultHandler.java         |   2 +-
 .../pushpull/DefaultGroupPullResultHandler.java    |   2 +-
 .../pushpull/DefaultRealmPullResultHandler.java    |   2 +-
 .../pushpull/DefaultRealmPushResultHandler.java    |   2 +-
 .../pushpull/DefaultUserPullResultHandler.java     |   9 +-
 .../pushpull/DefaultUserPushResultHandler.java     |   2 +-
 .../provisioning/java/pushpull/InboundMatcher.java |  16 +-
 .../java/pushpull/LDAPMembershipPullActions.java   |   2 +-
 .../java/pushpull/LDAPPasswordPullActions.java     |   2 +-
 .../java/pushpull/OutboundMatcher.java             |   2 +-
 .../java/pushpull/PushJobDelegate.java             |   8 +-
 .../java/pushpull/SinglePullJobDelegate.java       |  10 +-
 .../java/pushpull/SinglePushJobDelegate.java       |   2 +-
 .../stream/StreamAnyObjectPushResultHandler.java   |  73 ++++++
 .../stream/StreamGroupPushResultHandler.java       |  73 ++++++
 .../pushpull/stream/StreamPullJobDelegate.java     | 264 +++++++++++++++++++++
 .../pushpull/stream/StreamPushJobDelegate.java     | 200 ++++++++++++++++
 .../stream/StreamUserPushResultHandler.java        |  73 ++++++
 .../core/provisioning/java/utils/MappingUtils.java |  85 -------
 .../provisioning/java/utils/TemplateUtils.java     |   2 +-
 .../core/provisioning/java/AbstractTest.java       |  12 +
 .../java/{ => data}/ResourceDataBinderTest.java    |   3 +-
 .../java/{ => jexl}/MailTemplateTest.java          |   6 +-
 .../provisioning/java/{ => jexl}/MappingTest.java  |  22 +-
 .../pushpull/stream/StreamPullJobDelegateTest.java | 117 +++++++++
 .../pushpull/stream/StreamPushJobDelegateTest.java | 119 ++++++++++
 .../camel/CamelUserProvisioningManager.java        |   2 +-
 .../syncope/core/logic/oidc/OIDCUserManager.java   |   2 +-
 .../java/data/OIDCProviderDataBinderImpl.java      |   4 +-
 .../syncope/core/logic/saml2/SAML2UserManager.java |   2 +-
 .../java/data/SAML2IdPDataBinderImpl.java          |   4 +-
 fit/core-reference/pom.xml                         |  94 ++++++++
 .../apache/syncope/fit/core/PullTaskITCase.java    |  15 +-
 .../syncope/fit/core/ReconciliationITCase.java     | 126 +++++++++-
 .../org/apache/syncope/fit/core/UserITCase.java    |   7 +-
 .../org/apache/syncope/fit/core/VirAttrITCase.java |  13 +-
 pom.xml                                            |  23 +-
 108 files changed, 3535 insertions(+), 556 deletions(-)

diff --git a/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AbstractCSVSpec.java b/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AbstractCSVSpec.java
new file mode 100644
index 0000000..7844335
--- /dev/null
+++ b/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/AbstractCSVSpec.java
@@ -0,0 +1,229 @@
+/*
+ * 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.common.rest.api.beans;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.QueryParam;
+import org.apache.syncope.common.lib.types.MatchingRule;
+import org.apache.syncope.common.lib.types.UnmatchingRule;
+
+public abstract class AbstractCSVSpec {
+
+    protected abstract static class Builder<T extends AbstractCSVSpec, B extends Builder<T, B>> {
+
+        protected T instance;
+
+        protected abstract T newInstance();
+
+        protected T getInstance() {
+            if (instance == null) {
+                instance = newInstance();
+            }
+            return instance;
+        }
+
+        @SuppressWarnings("unchecked")
+        public B columnSeparator(final char columnSeparator) {
+            getInstance().setColumnSeparator(columnSeparator);
+            return (B) this;
+        }
+
+        @SuppressWarnings("unchecked")
+        public B arrayElementSeparator(final String arrayElementSeparator) {
+            getInstance().setArrayElementSeparator(arrayElementSeparator);
+            return (B) this;
+        }
+
+        @SuppressWarnings("unchecked")
+        public B quoteChar(final char quoteChar) {
+            getInstance().setQuoteChar(quoteChar);
+            return (B) this;
+        }
+
+        @SuppressWarnings("unchecked")
+        public B escapeChar(final char escapeChar) {
+            getInstance().setEscapeChar(escapeChar);
+            return (B) this;
+        }
+
+        @SuppressWarnings("unchecked")
+        public B lineSeparator(final String lineSeparatorChar) {
+            getInstance().setLineSeparator(lineSeparatorChar);
+            return (B) this;
+        }
+
+        @SuppressWarnings("unchecked")
+        public B nullValue(final String nullValue) {
+            getInstance().setNullValue(nullValue);
+            return (B) this;
+        }
+
+        @SuppressWarnings("unchecked")
+        public B allowComments(final boolean allowComments) {
+            getInstance().setAllowComments(allowComments);
+            return (B) this;
+        }
+
+        @SuppressWarnings("unchecked")
+        public B unmatchingRule(final UnmatchingRule unmatchingRule) {
+            getInstance().setUnmatchingRule(unmatchingRule);
+            return (B) this;
+        }
+
+        @SuppressWarnings("unchecked")
+        public B matchingRule(final MatchingRule matchingRule) {
+            getInstance().setMatchingRule(matchingRule);
+            return (B) this;
+        }
+
+        @SuppressWarnings("unchecked")
+        public B action(final String action) {
+            getInstance().getActions().add(action);
+            return (B) this;
+        }
+
+        public T build() {
+            return getInstance();
+        }
+    }
+
+    protected String anyTypeKey;
+
+    protected char columnSeparator = ',';
+
+    protected String arrayElementSeparator = ";";
+
+    protected char quoteChar = '"';
+
+    protected Character escapeChar;
+
+    protected String lineSeparator = "\n";
+
+    protected String nullValue = "";
+
+    protected boolean allowComments;
+
+    protected UnmatchingRule unmatchingRule = UnmatchingRule.PROVISION;
+
+    protected MatchingRule matchingRule = MatchingRule.UPDATE;
+
+    protected List<String> actions = new ArrayList<>();
+
+    public String getAnyTypeKey() {
+        return anyTypeKey;
+    }
+
+    @NotNull
+    @QueryParam("anyTypeKey")
+    public void setAnyTypeKey(final String anyTypeKey) {
+        this.anyTypeKey = anyTypeKey;
+    }
+
+    public char getColumnSeparator() {
+        return columnSeparator;
+    }
+
+    @QueryParam("columnSeparator")
+    public void setColumnSeparator(final char columnSeparator) {
+        this.columnSeparator = columnSeparator;
+    }
+
+    public String getArrayElementSeparator() {
+        return arrayElementSeparator;
+    }
+
+    @QueryParam("arrayElementSeparator")
+    public void setArrayElementSeparator(final String arrayElementSeparator) {
+        this.arrayElementSeparator = arrayElementSeparator;
+    }
+
+    public char getQuoteChar() {
+        return quoteChar;
+    }
+
+    @QueryParam("quoteChar")
+    public void setQuoteChar(final char quoteChar) {
+        this.quoteChar = quoteChar;
+    }
+
+    public Character getEscapeChar() {
+        return escapeChar;
+    }
+
+    @QueryParam("escapeChar")
+    public void setEscapeChar(final Character escapeChar) {
+        this.escapeChar = escapeChar;
+    }
+
+    public String getLineSeparator() {
+        return lineSeparator;
+    }
+
+    @QueryParam("lineSeparator")
+    public void setLineSeparator(final String lineSeparator) {
+        this.lineSeparator = lineSeparator;
+    }
+
+    public String getNullValue() {
+        return nullValue;
+    }
+
+    @QueryParam("nullValue")
+    public void setNullValue(final String nullValue) {
+        this.nullValue = nullValue;
+    }
+
+    public boolean isAllowComments() {
+        return allowComments;
+    }
+
+    @QueryParam("allowComments")
+    public void setAllowComments(final boolean allowComments) {
+        this.allowComments = allowComments;
+    }
+
+    public UnmatchingRule getUnmatchingRule() {
+        return unmatchingRule;
+    }
+
+    @QueryParam("unmatchingRule")
+    public void setUnmatchingRule(final UnmatchingRule unmatchingRule) {
+        this.unmatchingRule = unmatchingRule;
+    }
+
+    public MatchingRule getMatchingRule() {
+        return matchingRule;
+    }
+
+    @QueryParam("matchingRule")
+    public void setMatchingRule(final MatchingRule matchingRule) {
+        this.matchingRule = matchingRule;
+    }
+
+    public List<String> getActions() {
+        return actions;
+    }
+
+    @QueryParam("actions")
+    public void setActions(final List<String> actions) {
+        this.actions = actions;
+    }
+}
diff --git a/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/CSVPullSpec.java b/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/CSVPullSpec.java
new file mode 100644
index 0000000..7bb8a2d
--- /dev/null
+++ b/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/CSVPullSpec.java
@@ -0,0 +1,136 @@
+/*
+ * 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.common.rest.api.beans;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.validation.constraints.NotNull;
+import javax.ws.rs.QueryParam;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.types.ConflictResolutionAction;
+
+public class CSVPullSpec extends AbstractCSVSpec {
+
+    public static class Builder extends AbstractCSVSpec.Builder<CSVPullSpec, Builder> {
+
+        @Override
+        protected CSVPullSpec newInstance() {
+            return new CSVPullSpec();
+        }
+
+        public Builder(final String anyTypeKey, final String keyColumn) {
+            getInstance().setAnyTypeKey(anyTypeKey);
+            getInstance().setKeyColumn(keyColumn);
+        }
+
+        public Builder remediation(final boolean remediation) {
+            instance.setRemediation(remediation);
+            return this;
+        }
+
+        public Builder ignoreColumns(final String... ignoreColumns) {
+            instance.getIgnoreColumns().addAll(Stream.of(ignoreColumns).collect(Collectors.toList()));
+            return this;
+        }
+
+        public Builder destinationRealm(final String destinationRealm) {
+            instance.setDestinationRealm(destinationRealm);
+            return this;
+        }
+
+        public Builder conflictResolutionAction(final ConflictResolutionAction conflictResolutionAction) {
+            instance.setConflictResolutionAction(conflictResolutionAction);
+            return this;
+        }
+
+        public Builder pullCorrelationRule(final String pullCorrelationRule) {
+            instance.setPullCorrelationRule(pullCorrelationRule);
+            return this;
+        }
+    }
+
+    private String destinationRealm = SyncopeConstants.ROOT_REALM;
+
+    private String keyColumn;
+
+    private Set<String> ignoreColumns = new HashSet<>();
+
+    private boolean remediation;
+
+    private ConflictResolutionAction conflictResolutionAction = ConflictResolutionAction.IGNORE;
+
+    private String pullCorrelationRule;
+
+    public String getDestinationRealm() {
+        return destinationRealm;
+    }
+
+    @QueryParam("destinationRealm")
+    public void setDestinationRealm(final String destinationRealm) {
+        this.destinationRealm = destinationRealm;
+    }
+
+    public String getKeyColumn() {
+        return keyColumn;
+    }
+
+    @NotNull
+    @QueryParam("keyColumn")
+    public void setKeyColumn(final String keyColumn) {
+        this.keyColumn = keyColumn;
+    }
+
+    public Set<String> getIgnoreColumns() {
+        return ignoreColumns;
+    }
+
+    @QueryParam("ignoreColumns")
+    public void setIgnoreColumns(final Set<String> ignoreColumns) {
+        this.ignoreColumns = ignoreColumns;
+    }
+
+    public boolean isRemediation() {
+        return remediation;
+    }
+
+    @QueryParam("remediation")
+    public void setRemediation(final boolean remediation) {
+        this.remediation = remediation;
+    }
+
+    public ConflictResolutionAction getConflictResolutionAction() {
+        return conflictResolutionAction;
+    }
+
+    @QueryParam("conflictResolutionAction")
+    public void setConflictResolutionAction(final ConflictResolutionAction conflictResolutionAction) {
+        this.conflictResolutionAction = conflictResolutionAction;
+    }
+
+    public String getPullCorrelationRule() {
+        return pullCorrelationRule;
+    }
+
+    @QueryParam("pullCorrelationRule")
+    public void setPullCorrelationRule(final String pullCorrelationRule) {
+        this.pullCorrelationRule = pullCorrelationRule;
+    }
+}
diff --git a/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/CSVPushSpec.java b/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/CSVPushSpec.java
new file mode 100644
index 0000000..92b2196
--- /dev/null
+++ b/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/beans/CSVPushSpec.java
@@ -0,0 +1,139 @@
+/*
+ * 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.common.rest.api.beans;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import javax.ws.rs.QueryParam;
+
+public class CSVPushSpec extends AbstractCSVSpec {
+
+    public static class Builder extends AbstractCSVSpec.Builder<CSVPushSpec, Builder> {
+
+        @Override
+        protected CSVPushSpec newInstance() {
+            return new CSVPushSpec();
+        }
+
+        public Builder(final String anyTypeKey) {
+            getInstance().setAnyTypeKey(anyTypeKey);
+        }
+
+        public Builder field(final String field) {
+            getInstance().getFields().add(field);
+            return this;
+        }
+
+        public Builder fields(final Collection<String> fields) {
+            getInstance().getFields().addAll(fields);
+            return this;
+        }
+
+        public Builder plainAttr(final String plainAttr) {
+            getInstance().getPlainAttrs().add(plainAttr);
+            return this;
+        }
+
+        public Builder plainAttrs(final Collection<String> plainAttrs) {
+            getInstance().getPlainAttrs().addAll(plainAttrs);
+            return this;
+        }
+
+        public Builder derAttr(final String derAttr) {
+            getInstance().getDerAttrs().add(derAttr);
+            return this;
+        }
+
+        public Builder derAttrs(final Collection<String> derAttrs) {
+            getInstance().getDerAttrs().addAll(derAttrs);
+            return this;
+        }
+
+        public Builder virAttr(final String virAttr) {
+            getInstance().getVirAttrs().add(virAttr);
+            return this;
+        }
+
+        public Builder virAttrs(final Collection<String> virAttrs) {
+            getInstance().getVirAttrs().addAll(virAttrs);
+            return this;
+        }
+
+        public Builder ignorePagination(final boolean ignorePagination) {
+            getInstance().setIgnorePaging(ignorePagination);
+            return this;
+        }
+    }
+
+    private List<String> fields = new ArrayList<>();
+
+    private List<String> plainAttrs = new ArrayList<>();
+
+    private List<String> derAttrs = new ArrayList<>();
+
+    private List<String> virAttrs = new ArrayList<>();
+
+    private boolean ignorePaging;
+
+    public List<String> getFields() {
+        return fields;
+    }
+
+    @QueryParam("fields")
+    public void setFields(final List<String> fields) {
+        this.fields = fields;
+    }
+
+    public List<String> getPlainAttrs() {
+        return plainAttrs;
+    }
+
+    @QueryParam("plainAttrs")
+    public void setPlainAttrs(final List<String> plainAttrs) {
+        this.plainAttrs = plainAttrs;
+    }
+
+    public List<String> getDerAttrs() {
+        return derAttrs;
+    }
+
+    @QueryParam("derAttrs")
+    public void setDerAttrs(final List<String> derAttrs) {
+        this.derAttrs = derAttrs;
+    }
+
+    public List<String> getVirAttrs() {
+        return virAttrs;
+    }
+
+    @QueryParam("virAttrs")
+    public void setVirAttrs(final List<String> virAttrs) {
+        this.virAttrs = virAttrs;
+    }
+
+    public boolean isIgnorePaging() {
+        return ignorePaging;
+    }
+
+    @QueryParam("ignorePaging")
+    public void setIgnorePaging(final boolean ignorePaging) {
+        this.ignorePaging = ignorePaging;
+    }
+}
diff --git a/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ReconciliationService.java b/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ReconciliationService.java
index cb44ef9..a2c8193 100644
--- a/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ReconciliationService.java
+++ b/common/idm/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ReconciliationService.java
@@ -23,6 +23,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses;
 import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.security.SecurityRequirements;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import java.io.InputStream;
+import java.util.List;
 import javax.validation.constraints.NotNull;
 import javax.ws.rs.BeanParam;
 import javax.ws.rs.Consumes;
@@ -31,10 +33,15 @@ import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.common.lib.to.PullTaskTO;
 import org.apache.syncope.common.lib.to.PushTaskTO;
 import org.apache.syncope.common.lib.to.ReconStatus;
 import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.beans.AnyQuery;
+import org.apache.syncope.common.rest.api.beans.CSVPullSpec;
+import org.apache.syncope.common.rest.api.beans.CSVPushSpec;
 import org.apache.syncope.common.rest.api.beans.ReconQuery;
 
 /**
@@ -84,4 +91,30 @@ public interface ReconciliationService extends JAXRSService {
     @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
     @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
     void pull(@BeanParam ReconQuery query, @NotNull PullTaskTO pullTask);
+
+    /**
+     * Export a list of any objects matching the given query as CSV according to the provided specification.
+     *
+     * @param anyQuery query conditions
+     * @param spec CSV push specification
+     * @return CSV content matching the provided specification
+     */
+    @GET
+    @Path("csv/push")
+    @Consumes({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
+    @Produces({ RESTHeaders.TEXT_CSV })
+    Response push(@BeanParam AnyQuery anyQuery, @BeanParam CSVPushSpec spec);
+
+    /**
+     * Pulls the CSV input into Syncope according to the provided specification.
+     *
+     * @param spec CSV pull specification
+     * @param csv CSV input
+     * @return pull report
+     */
+    @POST
+    @Path("csv/pull")
+    @Consumes({ RESTHeaders.TEXT_CSV })
+    @Produces({ MediaType.APPLICATION_JSON, RESTHeaders.APPLICATION_YAML, MediaType.APPLICATION_XML })
+    List<ProvisioningReport> pull(@BeanParam CSVPullSpec spec, InputStream csv);
 }
diff --git a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/AccessTokenTO.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/AccessTokenTO.java
index e3e00f6..501ecb9 100644
--- a/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/AccessTokenTO.java
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/AccessTokenTO.java
@@ -72,5 +72,4 @@ public class AccessTokenTO extends BaseBean implements EntityTO {
     public void setOwner(final String owner) {
         this.owner = owner;
     }
-
 }
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/ProvisioningReport.java b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/ProvisioningReport.java
similarity index 98%
rename from core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/ProvisioningReport.java
rename to common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/ProvisioningReport.java
index 3c0a6f0..216ab56 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/ProvisioningReport.java
+++ b/common/idrepo/lib/src/main/java/org/apache/syncope/common/lib/to/ProvisioningReport.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.api.pushpull;
+package org.apache.syncope.common.lib.to;
 
 import java.util.Collection;
 import org.apache.commons.lang3.StringUtils;
diff --git a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/RESTHeaders.java b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/RESTHeaders.java
index 0429e29..fdabce0 100644
--- a/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/RESTHeaders.java
+++ b/common/idrepo/rest-api/src/main/java/org/apache/syncope/common/rest/api/RESTHeaders.java
@@ -68,6 +68,11 @@ public final class RESTHeaders {
     public static final MediaType APPLICATION_YAML_TYPE = new MediaType("application", "yaml");
 
     /**
+     * Mediatype for text/csv, not defined in {@link javax.ws.rs.core.MediaType}.
+     */
+    public static final String TEXT_CSV = "text/csv";
+
+    /**
      * Mediatype for multipart/mixed, not defined in {@link javax.ws.rs.core.MediaType}.
      */
     public static final String MULTIPART_MIXED = "multipart/mixed";
@@ -79,7 +84,7 @@ public final class RESTHeaders {
 
     /**
      * Builds Content-Type string for multipart/mixed and the given boundary.
-     * 
+     *
      * @param boundary multipart boundary value
      * @return multipart/mixed Content-Type string, with given boundary
      */
diff --git a/core/idm/logic/pom.xml b/core/idm/logic/pom.xml
index c34b10c..baf271c 100644
--- a/core/idm/logic/pom.xml
+++ b/core/idm/logic/pom.xml
@@ -39,6 +39,11 @@ under the License.
 
   <dependencies>
     <dependency>
+      <groupId>com.fasterxml.jackson.dataformat</groupId>
+      <artifactId>jackson-dataformat-csv</artifactId>
+    </dependency>
+
+    <dependency>
       <groupId>org.apache.syncope.core.idrepo</groupId>
       <artifactId>syncope-core-idrepo-logic</artifactId>
       <version>${project.version}</version>
@@ -48,6 +53,45 @@ under the License.
       <artifactId>syncope-common-idm-rest-api</artifactId>
       <version>${project.version}</version>
     </dependency>
+    
+    <dependency>
+      <groupId>org.apache.syncope.core</groupId>
+      <artifactId>syncope-core-workflow-java</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>javax.el</groupId>
+      <artifactId>javax.el-api</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.syncope.core</groupId>
+      <artifactId>syncope-core-persistence-jpa</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-simple</artifactId>
+      <version>${slf4j.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>com.h2database</groupId>
+      <artifactId>h2</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-test</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
@@ -57,6 +101,27 @@ under the License.
         <filtering>true</filtering>
       </resource>
     </resources>
+    <testResources>
+      <testResource>
+        <directory>${basedir}/src/test/resources</directory>
+        <filtering>true</filtering>
+      </testResource>
+      <testResource>
+        <directory>${basedir}/../../persistence-jpa/src/main/resources</directory>
+        <includes>
+          <include>persistence.properties</include>
+        </includes>
+        <filtering>true</filtering>
+      </testResource>
+      <testResource>
+        <directory>${basedir}/../../persistence-jpa/src/test/resources</directory>
+        <filtering>true</filtering>
+      </testResource>
+      <testResource>
+        <directory>${basedir}/../../provisioning-java/src/test/resources</directory>
+        <filtering>true</filtering>
+      </testResource>
+    </testResources>
         
     <plugins>
       <plugin>
diff --git a/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ReconciliationLogic.java b/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ReconciliationLogic.java
index 2c9fd4b..388f82b 100644
--- a/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ReconciliationLogic.java
+++ b/core/idm/logic/src/main/java/org/apache/syncope/core/logic/ReconciliationLogic.java
@@ -18,9 +18,16 @@
  */
 package org.apache.syncope.core.logic;
 
+import com.fasterxml.jackson.databind.MappingIterator;
+import com.fasterxml.jackson.databind.SequenceWriter;
+import com.fasterxml.jackson.dataformat.csv.CsvMapper;
+import com.fasterxml.jackson.dataformat.csv.CsvSchema;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import org.apache.commons.lang3.StringUtils;
@@ -35,9 +42,8 @@ import org.apache.syncope.common.lib.to.PushTaskTO;
 import org.apache.syncope.common.lib.to.ReconStatus;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
-import org.apache.syncope.common.lib.types.IdMEntitlement;
-import org.apache.syncope.common.lib.types.IdRepoEntitlement;
 import org.apache.syncope.common.lib.types.MatchType;
+import org.apache.syncope.common.rest.api.beans.CSVPullSpec;
 import org.apache.syncope.common.rest.api.beans.ReconQuery;
 import org.apache.syncope.core.persistence.api.dao.AnyDAO;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
@@ -54,13 +60,30 @@ import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount;
 import org.apache.syncope.core.provisioning.api.ConnectorFactory;
 import org.apache.syncope.core.provisioning.api.MappingManager;
 import org.apache.syncope.core.provisioning.api.VirAttrHandler;
-import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
+import org.apache.syncope.common.lib.types.AnyEntitlement;
+import org.apache.syncope.common.lib.types.IdMEntitlement;
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
+import org.apache.syncope.common.rest.api.beans.AbstractCSVSpec;
+import org.apache.syncope.common.rest.api.beans.CSVPushSpec;
+import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
+import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.VirSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.persistence.api.entity.AnyUtils;
+import org.apache.syncope.core.provisioning.api.pushpull.stream.StreamConnector;
 import org.apache.syncope.core.provisioning.api.pushpull.SyncopeSinglePullExecutor;
 import org.apache.syncope.core.provisioning.api.pushpull.SyncopeSinglePushExecutor;
+import org.apache.syncope.core.provisioning.api.pushpull.stream.SyncopeStreamPullExecutor;
+import org.apache.syncope.core.provisioning.api.pushpull.stream.SyncopeStreamPushExecutor;
+import org.apache.syncope.core.provisioning.api.utils.RealmUtils;
 import org.apache.syncope.core.provisioning.java.pushpull.InboundMatcher;
 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;
+import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.identityconnectors.framework.common.objects.Attribute;
 import org.identityconnectors.framework.common.objects.ConnectorObject;
 import org.identityconnectors.framework.common.objects.Uid;
@@ -85,6 +108,18 @@ public class ReconciliationLogic extends AbstractTransactionalLogic<EntityTO> {
     private RealmDAO realmDAO;
 
     @Autowired
+    private PlainSchemaDAO plainSchemaDAO;
+
+    @Autowired
+    private DerSchemaDAO derSchemaDAO;
+
+    @Autowired
+    private VirSchemaDAO virSchemaDAO;
+
+    @Autowired
+    private AnySearchDAO searchDAO;
+
+    @Autowired
     private VirAttrHandler virAttrHandler;
 
     @Autowired
@@ -105,6 +140,12 @@ public class ReconciliationLogic extends AbstractTransactionalLogic<EntityTO> {
     @Autowired
     private SyncopeSinglePushExecutor singlePushExecutor;
 
+    @Autowired
+    private SyncopeStreamPushExecutor streamPushExecutor;
+
+    @Autowired
+    private SyncopeStreamPullExecutor streamPullExecutor;
+
     private Provision getProvision(final String anyTypeKey, final String resourceKey) {
         AnyType anyType = anyTypeDAO.find(anyTypeKey);
         if (anyType == null) {
@@ -337,6 +378,178 @@ public class ReconciliationLogic extends AbstractTransactionalLogic<EntityTO> {
         }
     }
 
+    private CsvSchema csvSchema(final AbstractCSVSpec spec, final CsvSchema base) {
+        CsvSchema schema = base.
+                withColumnSeparator(spec.getColumnSeparator()).
+                withArrayElementSeparator(spec.getArrayElementSeparator()).
+                withQuoteChar(spec.getQuoteChar()).
+                withLineSeparator(spec.getLineSeparator()).
+                withNullValue(spec.getNullValue()).
+                withAllowComments(spec.isAllowComments());
+        if (spec.getEscapeChar() != null) {
+            schema = schema.withEscapeChar(spec.getEscapeChar());
+        }
+
+        return schema;
+    }
+
+    @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_EXECUTE + "')")
+    public List<ProvisioningReport> push(
+            final SearchCond searchCond,
+            final int page,
+            final int size,
+            final List<OrderByClause> orderBy,
+            final String realm,
+            final CSVPushSpec spec,
+            final OutputStream os) {
+
+        AnyType anyType = anyTypeDAO.find(spec.getAnyTypeKey());
+        if (anyType == null) {
+            throw new NotFoundException("AnyType '" + spec.getAnyTypeKey() + "'");
+        }
+
+        AnyUtils anyUtils = anyUtilsFactory.getInstance(anyType.getKind());
+
+        String entitlement;
+        switch (anyType.getKind()) {
+            case GROUP:
+                entitlement = IdRepoEntitlement.GROUP_SEARCH;
+                break;
+
+            case ANY_OBJECT:
+                entitlement = AnyEntitlement.SEARCH.getFor(anyType.getKey());
+                break;
+
+            case USER:
+            default:
+                entitlement = IdRepoEntitlement.USER_SEARCH;
+        }
+
+        Set<String> adminRealms = RealmUtils.getEffective(AuthContextUtils.getAuthorizations().get(entitlement), realm);
+        SearchCond effectiveCond = searchCond == null ? anyUtils.dao().getAllMatchingCond() : searchCond;
+
+        List<Any<?>> matching;
+        if (spec.isIgnorePaging()) {
+            matching = new ArrayList<>();
+
+            int count = searchDAO.count(adminRealms, searchCond, anyType.getKind());
+            int pages = (count / AnyDAO.DEFAULT_PAGE_SIZE) + 1;
+
+            for (int p = 1; p <= pages; p++) {
+                matching.addAll(searchDAO.search(adminRealms, effectiveCond,
+                        p, AnyDAO.DEFAULT_PAGE_SIZE, orderBy, anyType.getKind()));
+            }
+        } else {
+            matching = searchDAO.search(adminRealms, effectiveCond, page, size, orderBy, anyType.getKind());
+        }
+
+        List<String> columns = new ArrayList<>();
+        spec.getFields().forEach(item -> {
+            if (anyUtils.getField(item) == null) {
+                LOG.warn("Ignoring invalid field {}", item);
+            } else {
+                columns.add(item);
+            }
+        });
+        spec.getPlainAttrs().forEach(item -> {
+            if (plainSchemaDAO.find(item) == null) {
+                LOG.warn("Ignoring invalid plain schema {}", item);
+            } else {
+                columns.add(item);
+            }
+        });
+        spec.getDerAttrs().forEach(item -> {
+            if (derSchemaDAO.find(item) == null) {
+                LOG.warn("Ignoring invalid derived schema {}", item);
+            } else {
+                columns.add(item);
+            }
+        });
+        spec.getVirAttrs().forEach(item -> {
+            if (virSchemaDAO.find(item) == null) {
+                LOG.warn("Ignoring invalid virtual schema {}", item);
+            } else {
+                columns.add(item);
+            }
+        });
+
+        CsvSchema.Builder schemaBuilder = CsvSchema.builder().setUseHeader(true);
+        columns.forEach(schemaBuilder::addColumn);
+        CsvSchema schema = csvSchema(spec, schemaBuilder.build());
+
+        PushTaskTO pushTask = new PushTaskTO();
+        pushTask.setMatchingRule(spec.getMatchingRule());
+        pushTask.setUnmatchingRule(spec.getUnmatchingRule());
+        pushTask.getActions().addAll(spec.getActions());
+
+        try (SequenceWriter writer = new CsvMapper().writer(schema).forType(Map.class).writeValues(os)) {
+            return streamPushExecutor.push(
+                    anyType,
+                    matching,
+                    columns,
+                    new StreamConnector(null, spec.getArrayElementSeparator(), null, writer),
+                    pushTask,
+                    AuthContextUtils.getUsername());
+        } catch (Exception e) {
+            LOG.error("Could not push to stream", e);
+            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Reconciliation);
+            sce.getElements().add(e.getMessage());
+            throw sce;
+        }
+    }
+
+    @PreAuthorize("hasRole('" + IdRepoEntitlement.TASK_EXECUTE + "')")
+    public List<ProvisioningReport> pull(final CSVPullSpec spec, final InputStream csv) {
+        AnyType anyType = anyTypeDAO.find(spec.getAnyTypeKey());
+        if (anyType == null) {
+            throw new NotFoundException("AnyType '" + spec.getAnyTypeKey() + "'");
+        }
+
+        if (realmDAO.findByFullPath(spec.getDestinationRealm()) == null) {
+            throw new NotFoundException("Realm " + spec.getDestinationRealm());
+        }
+
+        PullTaskTO pullTask = new PullTaskTO();
+        pullTask.setDestinationRealm(spec.getDestinationRealm());
+        pullTask.setRemediation(spec.isRemediation());
+        pullTask.setMatchingRule(spec.getMatchingRule());
+        pullTask.setUnmatchingRule(spec.getUnmatchingRule());
+        pullTask.getActions().addAll(spec.getActions());
+
+        CsvSchema schema = csvSchema(spec, CsvSchema.emptySchema().withHeader());
+        try {
+            MappingIterator<Map<String, String>> reader =
+                    new CsvMapper().readerFor(Map.class).with(schema).readValues(csv);
+
+            List<String> columns = new ArrayList<>();
+            ((CsvSchema) reader.getParserSchema()).forEach(column -> {
+                if (!spec.getIgnoreColumns().contains(column.getName())) {
+                    columns.add(column.getName());
+                }
+            });
+
+            if (!columns.contains(spec.getKeyColumn())) {
+                throw new NotFoundException("Key column '" + spec.getKeyColumn() + "'");
+            }
+
+            return streamPullExecutor.pull(
+                    anyType,
+                    spec.getKeyColumn(),
+                    columns,
+                    spec.getConflictResolutionAction(),
+                    spec.getPullCorrelationRule(),
+                    new StreamConnector(spec.getKeyColumn(), spec.getArrayElementSeparator(), reader, null),
+                    pullTask);
+        } catch (NotFoundException e) {
+            throw e;
+        } catch (Exception e) {
+            LOG.error("Could not pull from stream", e);
+            SyncopeClientException sce = SyncopeClientException.build(ClientExceptionType.Reconciliation);
+            sce.getElements().add(e.getMessage());
+            throw sce;
+        }
+    }
+
     @Override
     protected EntityTO resolveReference(final Method method, final Object... os)
             throws UnresolvedReferenceException {
diff --git a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/AbstractTest.java b/core/idm/logic/src/test/java/org/apache/syncope/core/logic/AbstractTest.java
similarity index 75%
copy from core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/AbstractTest.java
copy to core/idm/logic/src/test/java/org/apache/syncope/core/logic/AbstractTest.java
index b120f93..424f348 100644
--- a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/AbstractTest.java
+++ b/core/idm/logic/src/test/java/org/apache/syncope/core/logic/AbstractTest.java
@@ -16,15 +16,19 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.java;
+package org.apache.syncope.core.logic;
 
 import javax.persistence.EntityManager;
+import org.apache.syncope.common.lib.types.EntitlementsHolder;
+import org.apache.syncope.common.lib.types.IdMEntitlement;
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
 import org.apache.syncope.core.spring.ApplicationContextProvider;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.junit.jupiter.api.BeforeAll;
 import org.springframework.orm.jpa.EntityManagerFactoryUtils;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
-@SpringJUnitConfig(classes = { ProvisioningTestContext.class })
+@SpringJUnitConfig(classes = { IdMLogicTestContext.class })
 public abstract class AbstractTest {
 
     protected EntityManager entityManager() {
@@ -37,4 +41,10 @@ public abstract class AbstractTest {
 
         return entityManager;
     }
+
+    @BeforeAll
+    public static void init() {
+        EntitlementsHolder.getInstance().addAll(IdRepoEntitlement.values());
+        EntitlementsHolder.getInstance().addAll(IdMEntitlement.values());
+    }
 }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/jexl/EmptyClassLoader.java b/core/idm/logic/src/test/java/org/apache/syncope/core/logic/DummyConfParamOps.java
similarity index 56%
copy from core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/jexl/EmptyClassLoader.java
copy to core/idm/logic/src/test/java/org/apache/syncope/core/logic/DummyConfParamOps.java
index 037113e..ca3a1ac 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/jexl/EmptyClassLoader.java
+++ b/core/idm/logic/src/test/java/org/apache/syncope/core/logic/DummyConfParamOps.java
@@ -16,21 +16,30 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.java.jexl;
+package org.apache.syncope.core.logic;
 
-/**
- * A class loader that will throw {@link ClassNotFoundException} for every class name.
- */
-class EmptyClassLoader extends ClassLoader {
+import java.util.Map;
+import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
+import org.springframework.stereotype.Component;
+
+@Component
+public class DummyConfParamOps implements ConfParamOps {
 
     @Override
-    public Class<?> loadClass(final String name) throws ClassNotFoundException {
-        throw new ClassNotFoundException("This classloader won't attemp to load " + name);
+    public Map<String, Object> list(final String domain) {
+        return Map.of();
     }
 
     @Override
-    protected Class<?> loadClass(final String name, final boolean resolve) throws ClassNotFoundException {
-        throw new ClassNotFoundException("This classloader won't attemp to load " + name);
+    public <T> T get(final String domain, final String key, final T defaultValue, final Class<T> reference) {
+        return defaultValue;
     }
 
+    @Override
+    public <T> void set(final String domain, final String key, final T value) {
+    }
+
+    @Override
+    public void remove(final String domain, final String key) {
+    }
 }
diff --git a/core/idm/logic/src/test/java/org/apache/syncope/core/logic/DummyDomainOps.java b/core/idm/logic/src/test/java/org/apache/syncope/core/logic/DummyDomainOps.java
new file mode 100644
index 0000000..09757b0
--- /dev/null
+++ b/core/idm/logic/src/test/java/org/apache/syncope/core/logic/DummyDomainOps.java
@@ -0,0 +1,59 @@
+/*
+ * 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.logic;
+
+import java.util.List;
+import org.apache.syncope.common.keymaster.client.api.DomainOps;
+import org.apache.syncope.common.keymaster.client.api.model.Domain;
+import org.apache.syncope.common.lib.types.CipherAlgorithm;
+import org.springframework.stereotype.Component;
+
+@Component
+public class DummyDomainOps implements DomainOps {
+
+    @Override
+    public List<Domain> list() {
+        return List.of();
+    }
+
+    @Override
+    public Domain read(final String key) {
+        return new Domain.Builder(key).build();
+    }
+
+    @Override
+    public void create(final Domain domain) {
+        // nothing to do
+    }
+
+    @Override
+    public void changeAdminPassword(final String key, final String password, final CipherAlgorithm cipherAlgorithm) {
+        // nothing to do
+    }
+
+    @Override
+    public void adjustPoolSize(final String key, final int maxPoolSize, final int minIdle) {
+        // nothing to do
+    }
+
+    @Override
+    public void delete(final String key) {
+        // nothing to do
+    }
+}
diff --git a/core/idm/logic/src/test/java/org/apache/syncope/core/logic/DummyImplementationLookup.java b/core/idm/logic/src/test/java/org/apache/syncope/core/logic/DummyImplementationLookup.java
new file mode 100644
index 0000000..7cf8a15
--- /dev/null
+++ b/core/idm/logic/src/test/java/org/apache/syncope/core/logic/DummyImplementationLookup.java
@@ -0,0 +1,90 @@
+/*
+ * 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.logic;
+
+import java.util.Set;
+import org.apache.syncope.common.lib.policy.AccountRuleConf;
+import org.apache.syncope.common.lib.policy.PasswordRuleConf;
+import org.apache.syncope.common.lib.policy.PullCorrelationRuleConf;
+import org.apache.syncope.common.lib.policy.PushCorrelationRuleConf;
+import org.apache.syncope.common.lib.report.ReportletConf;
+import org.apache.syncope.core.persistence.api.ImplementationLookup;
+import org.apache.syncope.core.persistence.api.dao.AccountRule;
+import org.apache.syncope.core.persistence.api.dao.PasswordRule;
+import org.apache.syncope.core.persistence.api.dao.PullCorrelationRule;
+import org.apache.syncope.core.persistence.api.dao.PushCorrelationRule;
+import org.apache.syncope.core.persistence.api.dao.Reportlet;
+
+public class DummyImplementationLookup implements ImplementationLookup {
+
+    @Override
+    public int getOrder() {
+        return -1;
+    }
+
+    @Override
+    public Set<String> getClassNames(final String type) {
+        return Set.of();
+    }
+
+    @Override
+    public Set<Class<?>> getJWTSSOProviderClasses() {
+        return Set.of();
+    }
+
+    @Override
+    public Class<Reportlet> getReportletClass(
+            final Class<? extends ReportletConf> reportletConfClass) {
+
+        return null;
+    }
+
+    @Override
+    public Class<? extends AccountRule> getAccountRuleClass(
+            final Class<? extends AccountRuleConf> accountRuleConfClass) {
+
+        return null;
+    }
+
+    @Override
+    public Class<? extends PasswordRule> getPasswordRuleClass(
+            final Class<? extends PasswordRuleConf> passwordRuleConfClass) {
+
+        return null;
+    }
+
+    @Override
+    public Class<? extends PullCorrelationRule> getPullCorrelationRuleClass(
+            final Class<? extends PullCorrelationRuleConf> pullCorrelationRuleConfClass) {
+
+        return null;
+    }
+
+    @Override
+    public Class<? extends PushCorrelationRule> getPushCorrelationRuleClass(
+            final Class<? extends PushCorrelationRuleConf> pushCorrelationRuleConfClass) {
+
+        return null;
+    }
+
+    @Override
+    public Set<Class<?>> getAuditAppenderClasses() {
+        return Set.of();
+    }
+}
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/jexl/ClassFreeUberspect.java b/core/idm/logic/src/test/java/org/apache/syncope/core/logic/DummyServiceOps.java
similarity index 55%
copy from core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/jexl/ClassFreeUberspect.java
copy to core/idm/logic/src/test/java/org/apache/syncope/core/logic/DummyServiceOps.java
index aec38b8..ce88ef3 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/jexl/ClassFreeUberspect.java
+++ b/core/idm/logic/src/test/java/org/apache/syncope/core/logic/DummyServiceOps.java
@@ -16,26 +16,33 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.java.jexl;
+package org.apache.syncope.core.logic;
 
-import org.apache.commons.jexl3.internal.introspection.Uberspect;
-import org.apache.commons.jexl3.introspection.JexlMethod;
-import org.apache.commons.jexl3.introspection.JexlPropertyGet;
+import java.util.List;
+import org.apache.syncope.common.keymaster.client.api.ServiceOps;
+import org.apache.syncope.common.keymaster.client.api.model.NetworkService;
+import org.springframework.stereotype.Component;
 
-class ClassFreeUberspect extends Uberspect {
+@Component
+public class DummyServiceOps implements ServiceOps {
 
-    ClassFreeUberspect() {
-        super(null, null);
+    @Override
+    public void register(final NetworkService service) {
+        // do nothing
     }
 
     @Override
-    public JexlPropertyGet getPropertyGet(final Object obj, final Object identifier) {
-        return "class".equals(identifier) ? null : super.getPropertyGet(obj, identifier);
+    public void unregister(final NetworkService service) {
+        // do nothing
     }
 
     @Override
-    public JexlMethod getMethod(final Object obj, final String method, final Object... args) {
-        return "getClass".equals(method) ? null : super.getMethod(obj, method, args);
+    public List<NetworkService> list(final NetworkService.Type serviceType) {
+        return List.of();
     }
 
+    @Override
+    public NetworkService get(final NetworkService.Type serviceType) {
+        return null;
+    }
 }
diff --git a/core/idm/logic/src/test/java/org/apache/syncope/core/logic/IdMLogicTestContext.java b/core/idm/logic/src/test/java/org/apache/syncope/core/logic/IdMLogicTestContext.java
new file mode 100644
index 0000000..2b4d0f9
--- /dev/null
+++ b/core/idm/logic/src/test/java/org/apache/syncope/core/logic/IdMLogicTestContext.java
@@ -0,0 +1,56 @@
+/*
+ * 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.logic;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import org.apache.syncope.core.persistence.api.ImplementationLookup;
+import org.apache.syncope.core.persistence.jpa.PersistenceContext;
+import org.apache.syncope.core.provisioning.java.ProvisioningContext;
+import org.apache.syncope.core.spring.security.SecurityContext;
+import org.apache.syncope.core.workflow.java.WorkflowContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.Primary;
+import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
+
+@Import({ SecurityContext.class, PersistenceContext.class, ProvisioningContext.class, WorkflowContext.class })
+@ComponentScan("org.apache.syncope.core.logic")
+@Configuration
+public class IdMLogicTestContext {
+
+    @Bean
+    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() throws IOException {
+        PropertySourcesPlaceholderConfigurer pspc = new PropertySourcesPlaceholderConfigurer();
+        pspc.setIgnoreResourceNotFound(true);
+        pspc.setIgnoreUnresolvablePlaceholders(true);
+        return pspc;
+    }
+
+    @Primary
+    @Bean
+    public ImplementationLookup classPathScanImplementationLookup()
+            throws ClassNotFoundException, InstantiationException, IllegalAccessException,
+            NoSuchMethodException, IllegalArgumentException, InvocationTargetException {
+
+        return new DummyImplementationLookup();
+    }
+}
diff --git a/core/idm/logic/src/test/java/org/apache/syncope/core/logic/ReconciliationLogicTest.java b/core/idm/logic/src/test/java/org/apache/syncope/core/logic/ReconciliationLogicTest.java
new file mode 100644
index 0000000..72d1a93
--- /dev/null
+++ b/core/idm/logic/src/test/java/org/apache/syncope/core/logic/ReconciliationLogicTest.java
@@ -0,0 +1,153 @@
+/*
+ * 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.logic;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.fasterxml.jackson.databind.MappingIterator;
+import com.fasterxml.jackson.dataformat.csv.CsvMapper;
+import com.fasterxml.jackson.dataformat.csv.CsvSchema;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.to.UserTO;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.common.lib.types.ResourceOperation;
+import org.apache.syncope.common.rest.api.beans.CSVPullSpec;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
+import org.apache.syncope.common.rest.api.beans.CSVPushSpec;
+import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+
+@Transactional("Master")
+public class ReconciliationLogicTest extends AbstractTest {
+
+    @Autowired
+    private ReconciliationLogic reconciliationLogic;
+
+    @Autowired
+    private UserLogic userLogic;
+
+    @Test
+    public void pullFromCSV() {
+        CSVPullSpec spec = new CSVPullSpec.Builder(AnyTypeKind.USER.name(), "username").build();
+        InputStream csv = getClass().getResourceAsStream("/test1.csv");
+
+        List<ProvisioningReport> results = AuthContextUtils.callAsAdmin(SyncopeConstants.MASTER_DOMAIN, () -> {
+            return reconciliationLogic.pull(spec, csv);
+        });
+        assertEquals(2, results.size());
+
+        assertEquals(AnyTypeKind.USER.name(), results.get(0).getAnyType());
+        assertNotNull(results.get(0).getKey());
+        assertEquals("donizetti", results.get(0).getName());
+        assertEquals("donizetti", results.get(0).getUidValue());
+        assertEquals(ResourceOperation.CREATE, results.get(0).getOperation());
+        assertEquals(ProvisioningReport.Status.SUCCESS, results.get(0).getStatus());
+
+        AuthContextUtils.callAsAdmin(SyncopeConstants.MASTER_DOMAIN, () -> {
+            UserTO donizetti = userLogic.read(results.get(0).getKey());
+            assertNotNull(donizetti);
+            assertEquals("Gaetano", donizetti.getPlainAttr("firstname").get().getValues().get(0));
+            assertEquals(1, donizetti.getPlainAttr("loginDate").get().getValues().size());
+
+            UserTO cimarosa = userLogic.read(results.get(1).getKey());
+            assertNotNull(cimarosa);
+            assertEquals("Domenico Cimarosa", cimarosa.getPlainAttr("fullname").get().getValues().get(0));
+            assertEquals(2, cimarosa.getPlainAttr("loginDate").get().getValues().size());
+
+            return null;
+        });
+    }
+
+    @Test
+    public void pushToCSV() throws IOException {
+        Pair<Integer, List<UserTO>> search = AuthContextUtils.callAsAdmin(SyncopeConstants.MASTER_DOMAIN,
+                () -> userLogic.search(null, 1, 100, List.of(), SyncopeConstants.ROOT_REALM, false));
+        assertNotNull(search);
+
+        CSVPushSpec spec = new CSVPushSpec.Builder(AnyTypeKind.USER.name()).
+                ignorePagination(true).
+                field("username").
+                field("status").
+                plainAttr("firstname").
+                plainAttr("surname").
+                plainAttr("email").
+                plainAttr("loginDate").
+                build();
+
+        PipedInputStream in = new PipedInputStream();
+        PipedOutputStream os = new PipedOutputStream(in);
+
+        List<ProvisioningReport> results = AuthContextUtils.callAsAdmin(SyncopeConstants.MASTER_DOMAIN, () -> {
+            return reconciliationLogic.push(null, 1, 1, List.of(), SyncopeConstants.ROOT_REALM, spec, os);
+        });
+        assertEquals(search.getLeft(), results.size());
+
+        CsvSchema.Builder builder = CsvSchema.builder().setUseHeader(true);
+        builder.addColumn("username");
+        builder.addColumn("status");
+        builder.addColumn("firstname");
+        builder.addColumn("surname");
+        builder.addColumn("email");
+        builder.addColumn("loginDate");
+        CsvSchema schema = builder.build();
+
+        MappingIterator<Map<String, String>> reader = new CsvMapper().readerFor(Map.class).with(schema).readValues(in);
+
+        for (int i = 0; i < results.size() && reader.hasNext(); i++) {
+            Map<String, String> row = reader.next();
+
+            assertEquals(results.get(i).getName(), row.get("username"));
+            assertEquals(search.getRight().stream().filter(user -> row.get("username").equals(user.getUsername())).
+                    findFirst().get().getStatus(),
+                    row.get("status"));
+
+            switch (row.get("username")) {
+                case "rossini":
+                    assertEquals(spec.getNullValue(), row.get("email"));
+                    assertTrue(row.get("loginDate").contains(spec.getArrayElementSeparator()));
+                    break;
+
+                case "verdi":
+                    assertEquals("verdi@syncope.org", row.get("email"));
+                    assertEquals(spec.getNullValue(), row.get("loginDate"));
+                    break;
+
+                case "bellini":
+                    assertEquals(spec.getNullValue(), row.get("email"));
+                    assertFalse(row.get("loginDate").contains(spec.getArrayElementSeparator()));
+                    break;
+
+                default:
+                    break;
+            }
+        }
+    }
+}
diff --git a/core/idm/logic/src/test/java/org/apache/syncope/core/logic/TestInitializer.java b/core/idm/logic/src/test/java/org/apache/syncope/core/logic/TestInitializer.java
new file mode 100644
index 0000000..1a4a652
--- /dev/null
+++ b/core/idm/logic/src/test/java/org/apache/syncope/core/logic/TestInitializer.java
@@ -0,0 +1,70 @@
+/*
+ * 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.logic;
+
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.core.persistence.api.DomainHolder;
+import org.apache.syncope.core.persistence.api.content.ContentLoader;
+import org.apache.syncope.core.persistence.jpa.StartupDomainLoader;
+import org.apache.syncope.core.spring.ApplicationContextProvider;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
+
+@Component
+public class TestInitializer implements InitializingBean, ApplicationContextAware {
+
+    private ConfigurableApplicationContext ctx;
+
+    @Autowired
+    private StartupDomainLoader domainLoader;
+
+    @Autowired
+    private DomainHolder domainHolder;
+
+    @Autowired
+    private ContentLoader contentLoader;
+
+    @Override
+    public void setApplicationContext(final ApplicationContext ctx) throws BeansException {
+        this.ctx = (ConfigurableApplicationContext) ctx;
+    }
+
+    @Override
+    public void afterPropertiesSet() throws Exception {
+        ApplicationContextProvider.setApplicationContext(ctx);
+        ApplicationContextProvider.setBeanFactory((DefaultListableBeanFactory) ctx.getBeanFactory());
+
+        if (!TransactionSynchronizationManager.isSynchronizationActive()) {
+            TransactionSynchronizationManager.initSynchronization();
+        }
+
+        domainLoader.load();
+
+        contentLoader.load(
+                SyncopeConstants.MASTER_DOMAIN,
+                domainHolder.getDomains().get(SyncopeConstants.MASTER_DOMAIN));
+    }
+}
diff --git a/core/idm/logic/src/test/resources/logicTest.xml b/core/idm/logic/src/test/resources/logicTest.xml
new file mode 100644
index 0000000..af40284
--- /dev/null
+++ b/core/idm/logic/src/test/resources/logicTest.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans
+                           http://www.springframework.org/schema/beans/spring-beans.xsd">
+    
+  <bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
+    <property name="locations">
+      <list>
+        <value>classpath:persistence.properties</value>
+        <value>classpath:domains/*.properties</value>
+        <value>classpath:security.properties</value>
+        <value>classpath:connid.properties</value>
+        <value>classpath:mail.properties</value>
+        <value>classpath:workflow.properties</value>
+        <value>classpath:provisioning.properties</value>
+        <value>classpath:logic.properties</value>
+      </list>
+    </property>
+    <property name="ignoreResourceNotFound" value="true"/>
+    <property name="ignoreUnresolvablePlaceholders" value="true"/>
+  </bean>
+
+  <bean id="jwtIssuer" class="java.lang.String">
+    <constructor-arg value="${jwtIssuer}"/>
+  </bean>
+  <bean id="jwsKey" class="java.lang.String">
+    <constructor-arg value="ZW7pRixehFuNUtnY5Se47IemgMryTzazPPJ9CGX5LTCmsOJpOgHAQEuPQeV9A28f"/>
+  </bean>
+  <bean id="accessTokenJwsSignatureVerifier"
+        class="org.apache.syncope.core.spring.security.jws.AccessTokenJwsSignatureVerifier">
+    <property name="jwsAlgorithm" value="${jwsAlgorithm}"/>
+    <property name="jwsKey" value="ZW7pRixehFuNUtnY5Se47IemgMryTzazPPJ9CGX5LTCmsOJpOgHAQEuPQeV9A28f"/>
+  </bean>
+  <bean id="accessTokenJwsSignatureProvider"
+        class="org.apache.syncope.core.spring.security.jws.AccessTokenJwsSignatureProvider">
+    <property name="jwsAlgorithm" value="${jwsAlgorithm}"/>
+    <property name="jwsKey" value="ZW7pRixehFuNUtnY5Se47IemgMryTzazPPJ9CGX5LTCmsOJpOgHAQEuPQeV9A28f"/>
+  </bean>
+  <bean id="credentialChecker" class="org.apache.syncope.core.spring.security.DefaultCredentialChecker">
+    <constructor-arg value="ZW7pRixehFuNUtnY5Se47IemgMryTzazPPJ9CGX5LTCmsOJpOgHAQEuPQeV9A28f" index="0"/>
+    <constructor-arg value="DE088591C00CC98B36F5ADAAF7DA2B004CF7F2FE7BBB45B766B6409876E2F3DB13C7905C6AA59464" index="1"/>
+    <constructor-arg value="anonymousKey" index="2"/>
+  </bean>
+  
+  <import resource="logicContext.xml"/>
+</beans>
diff --git a/core/idm/logic/src/test/resources/test1.csv b/core/idm/logic/src/test/resources/test1.csv
new file mode 100644
index 0000000..0ea7355
--- /dev/null
+++ b/core/idm/logic/src/test/resources/test1.csv
@@ -0,0 +1,3 @@
+"username","email","surname","firstname","fullname","userId","loginDate"
+"donizetti","donizetti@apache.org","Donizetti","Gaetano","Gaetano Donizetti","donizetti@apache.org","2019-12-24"
+"cimarosa","cimarosa@apache.org","Cimarosa","Domenico","Domenico Cimarosa","cimarosa@apache.org","2018-11-21;2018-12-24"
diff --git a/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ReconciliationServiceImpl.java b/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ReconciliationServiceImpl.java
index 7b77c2f..6f3b1a1 100644
--- a/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ReconciliationServiceImpl.java
+++ b/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ReconciliationServiceImpl.java
@@ -18,13 +18,27 @@
  */
 package org.apache.syncope.core.rest.cxf.service;
 
+import java.io.InputStream;
+import java.util.List;
 import javax.validation.ValidationException;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.StreamingOutput;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.common.lib.to.PullTaskTO;
 import org.apache.syncope.common.lib.to.PushTaskTO;
 import org.apache.syncope.common.lib.to.ReconStatus;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.beans.AnyQuery;
+import org.apache.syncope.common.rest.api.beans.CSVPullSpec;
+import org.apache.syncope.common.rest.api.beans.CSVPushSpec;
 import org.apache.syncope.common.rest.api.beans.ReconQuery;
 import org.apache.syncope.common.rest.api.service.ReconciliationService;
 import org.apache.syncope.core.logic.ReconciliationLogic;
+import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
+import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
@@ -59,4 +73,33 @@ public class ReconciliationServiceImpl extends AbstractServiceImpl implements Re
         validate(reconQuery);
         logic.pull(reconQuery, pullTask);
     }
+
+    @Override
+    public Response push(final AnyQuery anyQuery, final CSVPushSpec spec) {
+        String realm = StringUtils.prependIfMissing(anyQuery.getRealm(), SyncopeConstants.ROOT_REALM);
+
+        SearchCond searchCond = StringUtils.isBlank(anyQuery.getFiql())
+                ? null
+                : getSearchCond(anyQuery.getFiql(), realm);
+
+        StreamingOutput sout = (os) -> logic.push(
+                searchCond,
+                anyQuery.getPage(),
+                anyQuery.getSize(),
+                getOrderByClauses(anyQuery.getOrderBy()),
+                realm,
+                spec,
+                os);
+
+        return Response.ok(sout).
+                type(RESTHeaders.TEXT_CSV).
+                header(HttpHeaders.CONTENT_DISPOSITION,
+                        "attachment; filename=" + AuthContextUtils.getDomain() + ".csv").
+                build();
+    }
+
+    @Override
+    public List<ProvisioningReport> pull(final CSVPullSpec spec, final InputStream csv) {
+        return logic.pull(spec, csv);
+    }
 }
diff --git a/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java b/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java
index beb377a..c572d98 100644
--- a/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java
+++ b/core/idm/rest-cxf/src/main/java/org/apache/syncope/core/rest/cxf/service/ResourceServiceImpl.java
@@ -19,7 +19,6 @@
 package org.apache.syncope.core.rest.cxf.service;
 
 import java.net.URI;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -104,7 +103,7 @@ public class ResourceServiceImpl extends AbstractServiceImpl implements Resource
             final String key, final String anyTypeKey, final ConnObjectTOQuery query) {
 
         Filter filter = null;
-        Set<String> moreAttrsToGet = Collections.emptySet();
+        Set<String> moreAttrsToGet = Set.of();
         if (StringUtils.isNotBlank(query.getFiql())) {
             try {
                 FilterVisitor visitor = new FilterVisitor();
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 05efdb7..d0ac105 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
@@ -86,16 +86,17 @@ public class AnyObjectLogic extends AbstractAnyLogic<AnyObjectTO, AnyObjectCR, A
             throw new UnsupportedOperationException("Need to specify " + AnyType.class.getSimpleName());
         }
 
-        Set<String> effectiveRealms = RealmUtils.getEffective(
+        Set<String> adminRealms = RealmUtils.getEffective(
                 AuthContextUtils.getAuthorizations().get(AnyEntitlement.SEARCH.getFor(searchCond.hasAnyTypeCond())),
                 realm);
 
-        int count = searchDAO.count(effectiveRealms, searchCond, AnyTypeKind.ANY_OBJECT);
+        int count = searchDAO.count(adminRealms, searchCond, AnyTypeKind.ANY_OBJECT);
 
         List<AnyObject> matching = searchDAO.search(
-                effectiveRealms, searchCond, page, size, orderBy, AnyTypeKind.ANY_OBJECT);
+                adminRealms, searchCond, page, size, orderBy, AnyTypeKind.ANY_OBJECT);
         List<AnyObjectTO> result = matching.stream().
-                map(anyObject -> binder.getAnyObjectTO(anyObject, details)).collect(Collectors.toList());
+                map(anyObject -> binder.getAnyObjectTO(anyObject, details)).
+                collect(Collectors.toList());
 
         return Pair.of(count, result);
     }
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 f563347..daf795b 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
@@ -24,7 +24,6 @@ import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 import javax.annotation.Resource;
@@ -33,7 +32,6 @@ import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.keymaster.client.api.ConfParamOps;
 import org.apache.syncope.common.lib.SyncopeClientException;
-import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.request.GroupCR;
 import org.apache.syncope.common.lib.request.GroupUR;
 import org.apache.syncope.common.lib.request.StringPatchItem;
@@ -159,16 +157,18 @@ public class GroupLogic extends AbstractAnyLogic<GroupTO, GroupCR, GroupUR> {
             final String realm,
             final boolean details) {
 
-        int count = searchDAO.count(
-                RealmUtils.getEffective(SyncopeConstants.FULL_ADMIN_REALMS, realm),
-                Optional.ofNullable(searchCond).orElseGet(() -> groupDAO.getAllMatchingCond()), AnyTypeKind.GROUP);
+        Set<String> adminRealms = RealmUtils.getEffective(
+                AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.GROUP_SEARCH), realm);
+
+        SearchCond effectiveCond = searchCond == null ? groupDAO.getAllMatchingCond() : searchCond;
+
+        int count = searchDAO.count(adminRealms, effectiveCond, AnyTypeKind.GROUP);
 
         List<Group> matching = searchDAO.search(
-                RealmUtils.getEffective(SyncopeConstants.FULL_ADMIN_REALMS, realm),
-                Optional.ofNullable(searchCond).orElseGet(() -> groupDAO.getAllMatchingCond()),
-                page, size, orderBy, AnyTypeKind.GROUP);
+                adminRealms, effectiveCond, page, size, orderBy, AnyTypeKind.GROUP);
         List<GroupTO> result = matching.stream().
-                map(group -> binder.getGroupTO(group, details)).collect(Collectors.toList());
+                map(group -> binder.getGroupTO(group, details)).
+                collect(Collectors.toList());
 
         return Pair.of(count, result);
     }
diff --git a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java
index 16b0122..41acb64 100644
--- a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java
+++ b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java
@@ -43,11 +43,14 @@ import org.apache.syncope.common.lib.types.IdRepoEntitlement;
 import org.apache.syncope.common.lib.types.TaskType;
 import org.apache.syncope.common.rest.api.RESTHeaders;
 import org.apache.syncope.common.rest.api.batch.BatchResponseItem;
+import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
+import org.apache.syncope.core.persistence.api.dao.NotificationDAO;
 import org.apache.syncope.core.persistence.api.dao.NotFoundException;
 import org.apache.syncope.core.persistence.api.dao.TaskDAO;
 import org.apache.syncope.core.persistence.api.dao.TaskExecDAO;
 import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
 import org.apache.syncope.core.persistence.api.entity.task.NotificationTask;
+import org.apache.syncope.core.persistence.api.entity.task.PropagationTask;
 import org.apache.syncope.core.persistence.api.entity.task.SchedTask;
 import org.apache.syncope.core.persistence.api.entity.task.Task;
 import org.apache.syncope.core.persistence.api.entity.task.TaskExec;
@@ -56,8 +59,6 @@ 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.job.JobNamer;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskExecutor;
-import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
-import org.apache.syncope.core.persistence.api.dao.NotificationDAO;
 import org.apache.syncope.core.provisioning.api.notification.NotificationJobDelegate;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
 import org.apache.syncope.core.provisioning.api.utils.ExceptionUtils2;
@@ -232,18 +233,17 @@ public class TaskLogic extends AbstractExecutableLogic<TaskTO> {
 
         TaskUtils taskUtil = taskUtilsFactory.getInstance(task);
         String executor = AuthContextUtils.getUsername();
-        
+
         ExecTO result = null;
         switch (taskUtil.getType()) {
             case PROPAGATION:
                 PropagationTaskTO taskTO = binder.<PropagationTaskTO>getTaskTO(task, taskUtil, false);
-                PropagationTaskInfo taskInfo = new PropagationTaskInfo();
+                PropagationTaskInfo taskInfo = new PropagationTaskInfo(((PropagationTask) task).getResource());
                 taskInfo.setKey(taskTO.getKey());
                 taskInfo.setOperation(taskTO.getOperation());
                 taskInfo.setConnObjectKey(taskTO.getConnObjectKey());
                 taskInfo.setOldConnObjectKey(taskTO.getOldConnObjectKey());
                 taskInfo.setAttributes(taskTO.getAttributes());
-                taskInfo.setResource(taskTO.getResource());
                 taskInfo.setObjectClassName(taskTO.getObjectClassName());
                 taskInfo.setAnyTypeKind(taskTO.getAnyTypeKind());
                 taskInfo.setAnyType(taskTO.getAnyType());
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 0651a98..270c438 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
@@ -23,7 +23,6 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 import org.apache.commons.lang3.ArrayUtils;
@@ -111,14 +110,14 @@ public class UserLogic extends AbstractAnyLogic<UserTO, UserCR, UserUR> {
             final String realm,
             final boolean details) {
 
-        int count = searchDAO.count(RealmUtils.getEffective(
-                AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.USER_SEARCH), realm),
-            Optional.ofNullable(searchCond).orElseGet(() -> userDAO.getAllMatchingCond()), AnyTypeKind.USER);
+        Set<String> adminRealms = RealmUtils.getEffective(
+                AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.USER_SEARCH), realm);
 
-        List<User> matching = searchDAO.search(RealmUtils.getEffective(
-                AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.USER_SEARCH), realm),
-            Optional.ofNullable(searchCond).orElseGet(() -> userDAO.getAllMatchingCond()),
-                page, size, orderBy, AnyTypeKind.USER);
+        SearchCond effectiveCond = searchCond == null ? userDAO.getAllMatchingCond() : searchCond;
+
+        int count = searchDAO.count(adminRealms, effectiveCond, AnyTypeKind.USER);
+
+        List<User> matching = searchDAO.search(adminRealms, effectiveCond, page, size, orderBy, AnyTypeKind.USER);
         List<UserTO> result = matching.stream().
                 map(user -> binder.returnUserTO(binder.getUserTO(user, details))).
                 collect(Collectors.toList());
diff --git a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MyJPAJSONPlainSchemaDAO.java b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MyJPAJSONPlainSchemaDAO.java
index 07c9969..81bce44 100644
--- a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MyJPAJSONPlainSchemaDAO.java
+++ b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/MyJPAJSONPlainSchemaDAO.java
@@ -30,6 +30,6 @@ public class MyJPAJSONPlainSchemaDAO extends AbstractJPAJSONPlainSchemaDAO {
                 "SELECT COUNT(id) FROM " + new SearchSupport(getAnyTypeKind(reference)).field().name
                 + " WHERE JSON_CONTAINS(plainAttrs, '[{\"schema\":\"" + schema.getKey() + "\"}]')");
 
-        return (long) query.getSingleResult() > 0;
+        return ((Number) query.getSingleResult()).intValue() > 0;
     }
 }
diff --git a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAJSONPlainSchemaDAO.java b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAJSONPlainSchemaDAO.java
index 77019a7..c4d27d6 100644
--- a/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAJSONPlainSchemaDAO.java
+++ b/core/persistence-jpa-json/src/main/java/org/apache/syncope/core/persistence/jpa/dao/PGJPAJSONPlainSchemaDAO.java
@@ -30,6 +30,6 @@ public class PGJPAJSONPlainSchemaDAO extends AbstractJPAJSONPlainSchemaDAO {
                 "SELECT COUNT(id) FROM " + new SearchSupport(getAnyTypeKind(reference)).field().name
                 + " WHERE plainAttrs @> '[{\"schema\":\"" + schema.getKey() + "\"}]'::jsonb");
 
-        return (long) query.getSingleResult() > 0;
+        return ((Number) query.getSingleResult()).intValue() > 0;
     }
 }
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractAnyDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractAnyDAO.java
index 05d6a34..0be1f96 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractAnyDAO.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/AbstractAnyDAO.java
@@ -485,22 +485,18 @@ public abstract class AbstractAnyDAO<A extends Any<?>> extends AbstractDAO<A> im
         // schemas given by type extensions
         Map<Group, List<? extends AnyTypeClass>> typeExtensionClasses = new HashMap<>();
         if (any instanceof User) {
-            ((User) any).getMemberships().forEach(
-                    memb -> memb.getRightEnd().getTypeExtensions().forEach(
-                            typeExtension -> typeExtensionClasses.put(
-                                    memb.getRightEnd(), typeExtension.getAuxClasses())));
+            ((User) any).getMemberships().forEach(memb -> memb.getRightEnd().getTypeExtensions().
+                    forEach(typeExt -> typeExtensionClasses.put(memb.getRightEnd(), typeExt.getAuxClasses())));
         } else if (any instanceof AnyObject) {
-            ((AnyObject) any).getMemberships().
-                    forEach(memb -> memb.getRightEnd().getTypeExtensions().stream().
-                    filter(typeExtension -> any.getType().equals(typeExtension.getAnyType())).
-                    forEach(typeExtension -> typeExtensionClasses.put(
-                    memb.getRightEnd(), typeExtension.getAuxClasses())));
+            ((AnyObject) any).getMemberships().forEach(memb -> memb.getRightEnd().getTypeExtensions().stream().
+                    filter(typeExt -> any.getType().equals(typeExt.getAnyType())).
+                    forEach(typeExt -> typeExtensionClasses.put(memb.getRightEnd(), typeExt.getAuxClasses())));
         }
 
         typeExtensionClasses.entrySet().stream().map(entry -> {
             result.getForMemberships().put(entry.getKey(), new HashSet<>());
             return entry;
-        }).forEachOrdered((entry) -> entry.getValue().forEach(typeClass -> {
+        }).forEach(entry -> entry.getValue().forEach(typeClass -> {
             if (reference.equals(PlainSchema.class)) {
                 result.getForMemberships().get(entry.getKey()).
                         addAll((Collection<? extends S>) typeClass.getPlainSchemas());
@@ -543,7 +539,7 @@ public abstract class AbstractAnyDAO<A extends Any<?>> extends AbstractDAO<A> im
         query.getResultList().stream().map(resultKey -> resultKey instanceof Object[]
                 ? (String) ((Object[]) resultKey)[0]
                 : ((String) resultKey)).
-                forEachOrdered((actualKey) -> {
+                forEach((actualKey) -> {
                     DynRealm dynRealm = dynRealmDAO.find(actualKey.toString());
                     if (dynRealm == null) {
                         LOG.error("Could not find dynRealm with id {}, even though returned by the native query",
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAPlainSchemaDAO.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAPlainSchemaDAO.java
index 46f98ad..9309935 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAPlainSchemaDAO.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/dao/JPAPlainSchemaDAO.java
@@ -27,7 +27,6 @@ import org.apache.syncope.core.persistence.api.dao.ExternalResourceDAO;
 import org.apache.syncope.core.persistence.api.dao.PlainAttrDAO;
 import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
 import org.apache.syncope.core.persistence.api.entity.AnyTypeClass;
-import org.apache.syncope.core.persistence.api.entity.AnyUtils;
 import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.Implementation;
 import org.apache.syncope.core.persistence.api.entity.PlainAttr;
@@ -118,7 +117,7 @@ public class JPAPlainSchemaDAO extends AbstractDAO<PlainSchema> implements Plain
                 + ".schema_id WHERE " + JPAPlainSchema.TABLE + ".id = ?1");
         query.setParameter(1, schema.getKey());
 
-        return (long) query.getSingleResult() > 0;
+        return ((Number) query.getSingleResult()).intValue() > 0;
     }
 
     @Override
@@ -128,9 +127,8 @@ public class JPAPlainSchemaDAO extends AbstractDAO<PlainSchema> implements Plain
 
     protected void deleteAttrs(final PlainSchema schema) {
         for (AnyTypeKind anyTypeKind : AnyTypeKind.values()) {
-            AnyUtils anyUtils = anyUtilsFactory.getInstance(anyTypeKind);
-
-            findAttrs(schema, anyUtils.plainAttrClass()).forEach(attr -> plainAttrDAO.delete(attr));
+            findAttrs(schema, anyUtilsFactory.getInstance(anyTypeKind).plainAttrClass()).
+                    forEach(attr -> plainAttrDAO.delete(attr));
         }
     }
 
diff --git a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/resource/JPAExternalResource.java b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/resource/JPAExternalResource.java
index 020d4a5..e61568e 100644
--- a/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/resource/JPAExternalResource.java
+++ b/core/persistence-jpa/src/main/java/org/apache/syncope/core/persistence/jpa/entity/resource/JPAExternalResource.java
@@ -124,16 +124,16 @@ public class JPAExternalResource extends AbstractProvidedKeyEntity implements Ex
     @NotNull
     private TraceLevel provisioningTraceLevel = TraceLevel.FAILURES;
 
-    @ManyToOne(fetch = FetchType.LAZY)
+    @ManyToOne(fetch = FetchType.EAGER)
     private JPAPasswordPolicy passwordPolicy;
 
-    @ManyToOne(fetch = FetchType.LAZY)
+    @ManyToOne(fetch = FetchType.EAGER)
     private JPAAccountPolicy accountPolicy;
 
-    @ManyToOne(fetch = FetchType.LAZY)
+    @ManyToOne(fetch = FetchType.EAGER)
     private JPAPullPolicy pullPolicy;
 
-    @ManyToOne(fetch = FetchType.LAZY)
+    @ManyToOne(fetch = FetchType.EAGER)
     private JPAPushPolicy pushPolicy;
 
     /**
diff --git a/core/provisioning-api/pom.xml b/core/provisioning-api/pom.xml
index 103db94..f803af7 100644
--- a/core/provisioning-api/pom.xml
+++ b/core/provisioning-api/pom.xml
@@ -39,6 +39,11 @@ under the License.
 
   <dependencies>
     <dependency>
+      <groupId>org.apache.commons</groupId>
+      <artifactId>commons-jexl3</artifactId>
+    </dependency>
+
+    <dependency>
       <groupId>com.fasterxml.jackson.core</groupId>
       <artifactId>jackson-core</artifactId>
     </dependency>
@@ -50,12 +55,16 @@ under the License.
       <groupId>com.fasterxml.jackson.module</groupId>
       <artifactId>jackson-module-afterburner</artifactId>
     </dependency>
-    
+
     <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-context</artifactId>
     </dependency>
-    
+    <dependency>
+      <groupId>org.springframework</groupId>
+      <artifactId>spring-tx</artifactId>
+    </dependency>
+
     <dependency>
       <groupId>org.quartz-scheduler</groupId>
       <artifactId>quartz</artifactId>
@@ -66,6 +75,22 @@ under the License.
       <artifactId>syncope-core-persistence-api</artifactId>
       <version>${project.version}</version>
     </dependency>
+
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-junit-jupiter</artifactId>
+      <scope>test</scope>
+    </dependency>    
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/Connector.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/Connector.java
index 09de13c..1c11784 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/Connector.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/Connector.java
@@ -21,6 +21,7 @@ package org.apache.syncope.core.provisioning.api;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
 import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
 import org.apache.syncope.core.persistence.api.entity.ConnInstance;
 import org.apache.syncope.core.provisioning.api.pushpull.ReconFilterBuilder;
@@ -29,11 +30,15 @@ import org.identityconnectors.framework.common.objects.ConnectorObject;
 import org.identityconnectors.framework.common.objects.ObjectClass;
 import org.identityconnectors.framework.common.objects.ObjectClassInfo;
 import org.identityconnectors.framework.common.objects.OperationOptions;
+import org.identityconnectors.framework.common.objects.OperationOptionsBuilder;
 import org.identityconnectors.framework.common.objects.SyncResultsHandler;
 import org.identityconnectors.framework.common.objects.SyncToken;
 import org.identityconnectors.framework.common.objects.Uid;
 import org.identityconnectors.framework.common.objects.filter.Filter;
 import org.identityconnectors.framework.common.objects.SearchResult;
+import org.identityconnectors.framework.common.objects.SortKey;
+import org.identityconnectors.framework.common.objects.SyncDeltaBuilder;
+import org.identityconnectors.framework.common.objects.SyncDeltaType;
 import org.identityconnectors.framework.spi.SearchResultsHandler;
 
 /**
@@ -104,7 +109,9 @@ public interface Connector {
      * @param handler to be used to handle deltas.
      * @param options ConnId's OperationOptions.
      */
-    void fullReconciliation(ObjectClass objectClass, SyncResultsHandler handler, OperationOptions options);
+    default void fullReconciliation(ObjectClass objectClass, SyncResultsHandler handler, OperationOptions options) {
+        filteredReconciliation(objectClass, null, handler, options);
+    }
 
     /**
      * Fetches remote objects (for use during filtered reconciliation).
@@ -114,11 +121,36 @@ public interface Connector {
      * @param handler to be used to handle deltas.
      * @param options ConnId's OperationOptions.
      */
-    void filteredReconciliation(
+    default void filteredReconciliation(
             ObjectClass objectClass,
             ReconFilterBuilder filterBuilder,
             SyncResultsHandler handler,
-            OperationOptions options);
+            OperationOptions options) {
+
+        Filter filter = null;
+        OperationOptions actualOptions = options;
+        if (filterBuilder != null) {
+            filter = filterBuilder.build();
+            actualOptions = filterBuilder.build(actualOptions);
+        }
+
+        search(objectClass, filter, new SearchResultsHandler() {
+
+            @Override
+            public void handleResult(final SearchResult result) {
+                // nothing to do
+            }
+
+            @Override
+            public boolean handle(final ConnectorObject object) {
+                return handler.handle(new SyncDeltaBuilder().
+                        setObject(object).
+                        setDeltaType(SyncDeltaType.CREATE_OR_UPDATE).
+                        setToken(new SyncToken("")).
+                        build());
+            }
+        }, actualOptions);
+    }
 
     /**
      * Sync remote objects from a connector instance.
@@ -182,14 +214,27 @@ public interface Connector {
      * @param options ConnId's OperationOptions
      * @return search result
      */
-    SearchResult search(
+    default SearchResult search(
             ObjectClass objectClass,
             Filter filter,
             SearchResultsHandler handler,
             int pageSize,
             String pagedResultsCookie,
             List<OrderByClause> orderBy,
-            OperationOptions options);
+            OperationOptions options) {
+
+        OperationOptionsBuilder builder = new OperationOptionsBuilder().setPageSize(pageSize).setPagedResultsOffset(-1);
+        if (pagedResultsCookie != null) {
+            builder.setPagedResultsCookie(pagedResultsCookie);
+        }
+        builder.setSortKeys(orderBy.stream().
+                map(clause -> new SortKey(clause.getField(), clause.getDirection() == OrderByClause.Direction.ASC)).
+                collect(Collectors.toList()));
+
+        builder.setAttributesToGet(options.getAttributesToGet());
+
+        return search(objectClass, filter, handler, builder.build());
+    }
 
     /**
      * Builds metadata description of ConnId {@link ObjectClass}.
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/IntAttrNameParser.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/IntAttrNameParser.java
similarity index 98%
rename from core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/IntAttrNameParser.java
rename to core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/IntAttrNameParser.java
index ab1dc4d..fad50f4 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/IntAttrNameParser.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/IntAttrNameParser.java
@@ -16,10 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.java;
+package org.apache.syncope.core.provisioning.api;
 
 import java.text.ParseException;
-import org.apache.syncope.core.provisioning.api.IntAttrName;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import org.apache.commons.lang3.tuple.Pair;
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/UserProvisioningManager.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/UserProvisioningManager.java
index b397149..4ddccca 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/UserProvisioningManager.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/UserProvisioningManager.java
@@ -26,8 +26,8 @@ import org.apache.syncope.common.lib.request.StatusR;
 import org.apache.syncope.common.lib.request.UserCR;
 import org.apache.syncope.common.lib.request.UserUR;
 import org.apache.syncope.common.lib.to.PropagationStatus;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.common.lib.to.UserTO;
-import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
 
 public interface UserProvisioningManager extends ProvisioningManager<UserTO, UserCR, UserUR> {
 
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/jexl/ClassFreeUberspect.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/ClassFreeUberspect.java
similarity index 96%
rename from core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/jexl/ClassFreeUberspect.java
rename to core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/ClassFreeUberspect.java
index aec38b8..e218836 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/jexl/ClassFreeUberspect.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/ClassFreeUberspect.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.java.jexl;
+package org.apache.syncope.core.provisioning.api.jexl;
 
 import org.apache.commons.jexl3.internal.introspection.Uberspect;
 import org.apache.commons.jexl3.introspection.JexlMethod;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/jexl/EmptyClassLoader.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/EmptyClassLoader.java
similarity index 96%
rename from core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/jexl/EmptyClassLoader.java
rename to core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/EmptyClassLoader.java
index 037113e..2457d7f 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/jexl/EmptyClassLoader.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/EmptyClassLoader.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.java.jexl;
+package org.apache.syncope.core.provisioning.api.jexl;
 
 /**
  * A class loader that will throw {@link ClassNotFoundException} for every class name.
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/jexl/JexlUtils.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/JexlUtils.java
similarity index 94%
rename from core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/jexl/JexlUtils.java
rename to core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/JexlUtils.java
index 83ee4cf..ba0e63f 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/jexl/JexlUtils.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/JexlUtils.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.java.jexl;
+package org.apache.syncope.core.provisioning.api.jexl;
 
 import java.beans.IntrospectionException;
 import java.beans.Introspector;
@@ -44,7 +44,6 @@ import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.to.AnyTO;
 import org.apache.syncope.common.lib.Attr;
 import org.apache.syncope.common.lib.to.RealmTO;
-import org.apache.syncope.core.spring.ApplicationContextProvider;
 import org.apache.syncope.core.provisioning.api.utils.FormatUtils;
 import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.persistence.api.entity.DerSchema;
@@ -227,17 +226,24 @@ public final class JexlUtils {
         });
     }
 
-    public static void addDerAttrsToContext(final Any<?> any, final JexlContext jexlContext) {
-        Map<DerSchema, String> derAttrs =
-                ApplicationContextProvider.getBeanFactory().getBean(DerAttrHandler.class).getValues(any);
+    public static void addDerAttrsToContext(
+            final Any<?> any,
+            final DerAttrHandler derAttrHandler,
+            final JexlContext jexlContext) {
+
+        Map<DerSchema, String> derAttrs = derAttrHandler.getValues(any);
 
         derAttrs.forEach((schema, value) -> jexlContext.set(schema.getKey(), value));
     }
 
-    public static boolean evaluateMandatoryCondition(final String mandatoryCondition, final Any<?> any) {
+    public static boolean evaluateMandatoryCondition(
+            final String mandatoryCondition,
+            final Any<?> any,
+            final DerAttrHandler derAttrHandler) {
+
         JexlContext jexlContext = new MapContext();
         addPlainAttrsToContext(any.getPlainAttrs(), jexlContext);
-        addDerAttrsToContext(any, jexlContext);
+        addDerAttrsToContext(any, derAttrHandler, jexlContext);
 
         return Boolean.parseBoolean(evaluate(mandatoryCondition, jexlContext));
     }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/jexl/SyncopeJexlFunctions.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/SyncopeJexlFunctions.java
similarity index 97%
rename from core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/jexl/SyncopeJexlFunctions.java
rename to core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/SyncopeJexlFunctions.java
index 1dd92a8..9cc011f 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/jexl/SyncopeJexlFunctions.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/jexl/SyncopeJexlFunctions.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.java.jexl;
+package org.apache.syncope.core.provisioning.api.jexl;
 
 import java.util.Arrays;
 import java.util.Collections;
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/propagation/PropagationManager.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/propagation/PropagationManager.java
index 523cc22..048f06b 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/propagation/PropagationManager.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/propagation/PropagationManager.java
@@ -20,13 +20,22 @@ package org.apache.syncope.core.provisioning.api.propagation;
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.syncope.common.lib.request.UserUR;
 import org.apache.syncope.common.lib.Attr;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.common.lib.types.ResourceOperation;
+import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.provisioning.api.PropagationByResource;
 import org.apache.syncope.core.persistence.api.entity.Realm;
+import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
+import org.apache.syncope.core.persistence.api.entity.resource.Item;
+import org.apache.syncope.core.persistence.api.entity.resource.Provision;
+import org.apache.syncope.core.provisioning.api.DerAttrHandler;
 import org.apache.syncope.core.provisioning.api.UserWorkflowResult;
+import org.identityconnectors.framework.common.objects.Attribute;
 
 public interface PropagationManager {
 
@@ -147,6 +156,16 @@ public interface PropagationManager {
             PropagationByResource<Pair<String, String>> propByLinkedAccount,
             Collection<String> noPropResourceKeys);
 
+    PropagationTaskInfo newTask(
+            DerAttrHandler derAttrHandler,
+            Any<?> any,
+            ExternalResource resource,
+            ResourceOperation operation,
+            Provision provision,
+            boolean deleteOnResource,
+            Stream<? extends Item> mappingItems,
+            Pair<String, Set<Attribute>> preparedAttrs);
+
     /**
      * Create the needed tasks for the realm for each resource associated, unless in {@code noPropResourceKeys}.
      *
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/propagation/PropagationTaskExecutor.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/propagation/PropagationTaskExecutor.java
index 6447fca..50455b6 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/propagation/PropagationTaskExecutor.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/propagation/PropagationTaskExecutor.java
@@ -70,6 +70,6 @@ public interface PropagationTaskExecutor {
      * @param executor the executor of this task
      * @return reporter to report propagation execution status
      */
-    PropagationReporter execute(Collection<PropagationTaskInfo> taskInfos, boolean nullPriorityAsync,
-                                String executor);
+    PropagationReporter execute(
+            Collection<PropagationTaskInfo> taskInfos, boolean nullPriorityAsync, String executor);
 }
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/propagation/PropagationTaskInfo.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/propagation/PropagationTaskInfo.java
index 322a163..c3b3e44 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/propagation/PropagationTaskInfo.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/propagation/PropagationTaskInfo.java
@@ -19,22 +19,57 @@
 package org.apache.syncope.core.provisioning.api.propagation;
 
 import java.util.Optional;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
 import org.apache.syncope.common.lib.to.PropagationTaskTO;
+import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
+import org.apache.syncope.core.provisioning.api.Connector;
 import org.identityconnectors.framework.common.objects.ConnectorObject;
 
 public class PropagationTaskInfo extends PropagationTaskTO {
 
     private static final long serialVersionUID = -2879861567335503099L;
 
+    private final ExternalResource externalResource;
+
+    private Connector connector;
+
     /**
      * Object on External Resource before propagation takes place.
      *
      * null: beforeObj was not attempted to read
-     * not null, but not present: beforeObj was attempted to read, but not found
+     * not null but not present: beforeObj was attempted to read, but not found
      * not null and present: beforeObj value is available
      */
     private Optional<ConnectorObject> beforeObj;
 
+    public PropagationTaskInfo(final ExternalResource externalResource) {
+        super();
+        this.externalResource = externalResource;
+    }
+
+    public Connector getConnector() {
+        return connector;
+    }
+
+    public void setConnector(final Connector connector) {
+        this.connector = connector;
+    }
+
+    public ExternalResource getExternalResource() {
+        return externalResource;
+    }
+
+    @Override
+    public String getResource() {
+        return externalResource.getKey();
+    }
+
+    @Override
+    public void setResource(final String resource) {
+        throw new IllegalArgumentException("Cannot set ExternalResource on " + getClass().getName());
+    }
+
     public Optional<ConnectorObject> getBeforeObj() {
         return beforeObj;
     }
@@ -42,4 +77,32 @@ public class PropagationTaskInfo extends PropagationTaskTO {
     public void setBeforeObj(final Optional<ConnectorObject> beforeObj) {
         this.beforeObj = beforeObj;
     }
+
+    @Override
+    public int hashCode() {
+        return new HashCodeBuilder().
+                appendSuper(super.hashCode()).
+                append(externalResource.getKey()).
+                append(beforeObj).
+                build();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null) {
+            return false;
+        }
+        if (getClass() != obj.getClass()) {
+            return false;
+        }
+        final PropagationTaskInfo other = (PropagationTaskInfo) obj;
+        return new EqualsBuilder().
+                appendSuper(super.equals(obj)).
+                append(externalResource.getKey(), other.externalResource.getKey()).
+                append(beforeObj, other.beforeObj).
+                build();
+    }
 }
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/ProvisioningProfile.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/ProvisioningProfile.java
index 9aa3050..25f61b6 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/ProvisioningProfile.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/ProvisioningProfile.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.core.provisioning.api.pushpull;
 
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import java.util.ArrayList;
 import java.util.List;
 import org.apache.syncope.common.lib.types.ConflictResolutionAction;
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/PullActions.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/PullActions.java
index ae97ff1..0070836 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/PullActions.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/PullActions.java
@@ -24,6 +24,7 @@ import org.apache.syncope.common.lib.request.AnyUR;
 import org.apache.syncope.common.lib.to.LinkedAccountTO;
 import org.apache.syncope.common.lib.to.RealmTO;
 import org.apache.syncope.common.lib.to.EntityTO;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.core.persistence.api.entity.resource.OrgUnit;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
 import org.identityconnectors.framework.common.objects.SyncDelta;
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/PushActions.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/PushActions.java
index 1642b0e..642d591 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/PushActions.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/PushActions.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.core.provisioning.api.pushpull;
 
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.core.persistence.api.entity.Entity;
 import org.quartz.JobExecutionException;
 
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePullExecutor.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePullExecutor.java
index c0ca069..7614291 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePullExecutor.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePullExecutor.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.core.provisioning.api.pushpull;
 
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import java.util.List;
 import org.apache.syncope.common.lib.to.PullTaskTO;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePushExecutor.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePushExecutor.java
index 8053a65..a9a7ce7 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePushExecutor.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePushExecutor.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.core.provisioning.api.pushpull;
 
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import java.util.List;
 import org.apache.syncope.common.lib.to.PushTaskTO;
 import org.apache.syncope.core.persistence.api.entity.Any;
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/stream/StreamConnector.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/stream/StreamConnector.java
new file mode 100644
index 0000000..5ff51c8
--- /dev/null
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/stream/StreamConnector.java
@@ -0,0 +1,208 @@
+/*
+ * 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.pushpull.stream;
+
+import com.fasterxml.jackson.databind.MappingIterator;
+import com.fasterxml.jackson.databind.SequenceWriter;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.core.persistence.api.entity.ConnInstance;
+import org.apache.syncope.core.provisioning.api.Connector;
+import org.identityconnectors.framework.common.objects.Attribute;
+import org.identityconnectors.framework.common.objects.AttributeBuilder;
+import org.identityconnectors.framework.common.objects.AttributeUtil;
+import org.identityconnectors.framework.common.objects.ConnectorObject;
+import org.identityconnectors.framework.common.objects.ConnectorObjectBuilder;
+import org.identityconnectors.framework.common.objects.ObjectClass;
+import org.identityconnectors.framework.common.objects.ObjectClassInfo;
+import org.identityconnectors.framework.common.objects.OperationOptions;
+import org.identityconnectors.framework.common.objects.SearchResult;
+import org.identityconnectors.framework.common.objects.SyncResultsHandler;
+import org.identityconnectors.framework.common.objects.SyncToken;
+import org.identityconnectors.framework.common.objects.Uid;
+import org.identityconnectors.framework.common.objects.filter.Filter;
+import org.identityconnectors.framework.spi.SearchResultsHandler;
+import org.springframework.util.CollectionUtils;
+
+public class StreamConnector implements Connector {
+
+    private final String keyColumn;
+
+    private final String arrayElementsSeparator;
+
+    private final MappingIterator<Map<String, String>> reader;
+
+    private final SequenceWriter writer;
+
+    public StreamConnector(
+            final String keyColumn,
+            final String arrayElementsSeparator,
+            final MappingIterator<Map<String, String>> reader,
+            final SequenceWriter writer) {
+
+        this.keyColumn = keyColumn;
+        this.arrayElementsSeparator = arrayElementsSeparator;
+        this.reader = reader;
+        this.writer = writer;
+    }
+
+    @Override
+    public Uid authenticate(final String username, final String password, final OperationOptions options) {
+        return null;
+    }
+
+    @Override
+    public ConnInstance getConnInstance() {
+        return null;
+    }
+
+    @Override
+    public Uid create(
+            final ObjectClass objectClass,
+            final Set<Attribute> attrs,
+            final OperationOptions options,
+            final AtomicReference<Boolean> propagationAttempted) {
+
+        if (writer != null) {
+            Map<String, String> row = new HashMap<>();
+            attrs.stream().filter(attr -> !AttributeUtil.isSpecial(attr)).forEach(attr -> {
+                if (CollectionUtils.isEmpty(attr.getValue()) || attr.getValue().get(0) == null) {
+                    row.put(attr.getName(), null);
+                } else if (attr.getValue().size() == 1) {
+                    row.put(attr.getName(), attr.getValue().get(0).toString());
+                } else if (arrayElementsSeparator == null) {
+                    row.put(attr.getName(), attr.getValue().toString());
+                } else {
+                    row.put(
+                            attr.getName(),
+                            attr.getValue().stream().map(Object::toString).
+                                    collect(Collectors.joining(arrayElementsSeparator)));
+                }
+            });
+            try {
+                writer.write(row);
+            } catch (IOException e) {
+                throw new IllegalStateException("Could not object " + row, e);
+            }
+            propagationAttempted.set(Boolean.TRUE);
+        }
+        return null;
+    }
+
+    @Override
+    public Uid update(
+            final ObjectClass objectClass,
+            final Uid uid,
+            final Set<Attribute> attrs,
+            final OperationOptions options,
+            final AtomicReference<Boolean> propagationAttempted) {
+
+        return null;
+    }
+
+    @Override
+    public void delete(
+            final ObjectClass objectClass,
+            final Uid uid,
+            final OperationOptions options,
+            final AtomicReference<Boolean> propagationAttempted) {
+
+        // nothing to do
+    }
+
+    @Override
+    public void sync(
+            final ObjectClass objectClass,
+            final SyncToken token,
+            final SyncResultsHandler handler,
+            final OperationOptions options) {
+
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SyncToken getLatestSyncToken(final ObjectClass objectClass) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ConnectorObject getObject(
+            final ObjectClass objectClass,
+            final Attribute connObjectKey,
+            final boolean ignoreCaseMatch,
+            final OperationOptions options) {
+
+        return null;
+    }
+
+    @Override
+    public SearchResult search(
+            final ObjectClass objectClass,
+            final Filter filter,
+            final SearchResultsHandler handler,
+            final OperationOptions options) {
+
+        SearchResult result = new SearchResult();
+
+        if (reader != null) {
+            while (reader.hasNext()) {
+                Map<String, String> row = reader.next();
+
+                ConnectorObjectBuilder builder = new ConnectorObjectBuilder();
+                builder.setObjectClass(objectClass);
+                builder.setUid(row.get(keyColumn));
+                builder.setName(row.get(keyColumn));
+
+                row.forEach((key, value) -> builder.addAttribute(arrayElementsSeparator == null
+                        ? AttributeBuilder.build(key, value)
+                        : AttributeBuilder.build(key,
+                                (Object[]) StringUtils.splitByWholeSeparator(value, arrayElementsSeparator))));
+
+                handler.handle(builder.build());
+            }
+        }
+
+        return result;
+    }
+
+    @Override
+    public Set<ObjectClassInfo> getObjectClassInfo() {
+        return Set.of();
+    }
+
+    @Override
+    public void validate() {
+        // nothing to do
+    }
+
+    @Override
+    public void test() {
+        // nothing to do
+    }
+
+    @Override
+    public void dispose() {
+        // nothing to do
+    }
+}
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePullExecutor.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/stream/SyncopeStreamPullExecutor.java
similarity index 61%
copy from core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePullExecutor.java
copy to core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/stream/SyncopeStreamPullExecutor.java
index c0ca069..8d5c241 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePullExecutor.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/stream/SyncopeStreamPullExecutor.java
@@ -16,21 +16,24 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.api.pushpull;
+package org.apache.syncope.core.provisioning.api.pushpull.stream;
 
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import java.util.List;
 import org.apache.syncope.common.lib.to.PullTaskTO;
-import org.apache.syncope.core.persistence.api.entity.resource.Provision;
-import org.apache.syncope.core.provisioning.api.Connector;
+import org.apache.syncope.common.lib.types.ConflictResolutionAction;
+import org.apache.syncope.core.persistence.api.entity.AnyType;
 import org.quartz.JobExecutionException;
 
-@FunctionalInterface
-public interface SyncopeSinglePullExecutor {
+public interface SyncopeStreamPullExecutor {
 
     List<ProvisioningReport> pull(
-            Provision provision,
-            Connector connector,
-            String connObjectKey,
-            String connObjectValue,
-            PullTaskTO pullTaskTO) throws JobExecutionException;
+            AnyType anyType,
+            String keyColumn,
+            List<String> columns,
+            ConflictResolutionAction conflictResolutionAction,
+            String pullCorrelationRule,
+            StreamConnector connector,
+            PullTaskTO pullTaskTO)
+            throws JobExecutionException;
 }
diff --git a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePushExecutor.java b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/stream/SyncopeStreamPushExecutor.java
similarity index 60%
copy from core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePushExecutor.java
copy to core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/stream/SyncopeStreamPushExecutor.java
index 8053a65..b70a2c4 100644
--- a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopeSinglePushExecutor.java
+++ b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/stream/SyncopeStreamPushExecutor.java
@@ -16,27 +16,23 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.api.pushpull;
+package org.apache.syncope.core.provisioning.api.pushpull.stream;
 
 import java.util.List;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.common.lib.to.PushTaskTO;
 import org.apache.syncope.core.persistence.api.entity.Any;
-import org.apache.syncope.core.persistence.api.entity.resource.Provision;
-import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount;
-import org.apache.syncope.core.provisioning.api.Connector;
+import org.apache.syncope.core.persistence.api.entity.AnyType;
 import org.quartz.JobExecutionException;
 
-public interface SyncopeSinglePushExecutor {
+public interface SyncopeStreamPushExecutor {
 
     List<ProvisioningReport> push(
-            Provision provision,
-            Connector connector,
-            Any<?> any,
-            PushTaskTO pushTaskTO) throws JobExecutionException;
-
-    ProvisioningReport push(
-            Provision provision,
-            Connector connector,
-            LinkedAccount account,
-            PushTaskTO pushTaskTO) throws JobExecutionException;
+            AnyType anyType,
+            List<? extends Any<?>> anys,
+            List<String> columns,
+            StreamConnector connector,
+            PushTaskTO pushTaskTO,
+            String executor)
+            throws JobExecutionException;
 }
diff --git a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/IntAttrNameParserTest.java b/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/IntAttrNameParserTest.java
similarity index 75%
rename from core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/IntAttrNameParserTest.java
rename to core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/IntAttrNameParserTest.java
index b10a4b9..2a2b178 100644
--- a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/IntAttrNameParserTest.java
+++ b/core/provisioning-api/src/test/java/org/apache/syncope/core/provisioning/api/IntAttrNameParserTest.java
@@ -16,31 +16,130 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.java;
+package org.apache.syncope.core.provisioning.api;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
 import java.text.ParseException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.common.lib.types.AttrSchemaType;
 import org.apache.syncope.common.lib.types.SchemaType;
+import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.VirSchemaDAO;
+import org.apache.syncope.core.persistence.api.entity.AnyUtils;
+import org.apache.syncope.core.persistence.api.entity.AnyUtilsFactory;
 import org.apache.syncope.core.persistence.api.entity.DerSchema;
 import org.apache.syncope.core.persistence.api.entity.PlainSchema;
 import org.apache.syncope.core.persistence.api.entity.VirSchema;
-import org.apache.syncope.core.provisioning.api.IntAttrName;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.transaction.annotation.Transactional;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.springframework.util.ReflectionUtils;
 
-@Transactional("Master")
-public class IntAttrNameParserTest extends AbstractTest {
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.WARN)
+public class IntAttrNameParserTest {
 
-    @Autowired
+    private static final Map<AnyTypeKind, List<String>> FIELDS = new HashMap<>();
+
+    static {
+        FIELDS.put(AnyTypeKind.USER, Arrays.asList("key", "username"));
+        FIELDS.put(AnyTypeKind.GROUP, Arrays.asList("key", "name", "userOwner"));
+        FIELDS.put(AnyTypeKind.ANY_OBJECT, Arrays.asList("key", "name"));
+    }
+
+    @Mock
+    private PlainSchemaDAO plainSchemaDAO;
+
+    @Mock
+    private DerSchemaDAO derSchemaDAO;
+
+    @Mock
+    private VirSchemaDAO virSchemaDAO;
+
+    @Mock
+    private AnyUtilsFactory anyUtilsFactory;
+
+    @Mock
+    private AnyUtils anyUtils;
+
+    @InjectMocks
     private IntAttrNameParser intAttrNameParser;
 
+    @BeforeEach
+    public void initMocks() throws NoSuchFieldException {
+        MockitoAnnotations.initMocks(this);
+
+        when(anyUtilsFactory.getInstance(any(AnyTypeKind.class))).thenAnswer(ic -> {
+            when(anyUtils.anyTypeKind()).thenReturn(ic.getArgument(0));
+            return anyUtils;
+        });
+        when(anyUtils.getField(anyString())).thenAnswer(ic -> {
+            String field = ic.getArgument(0);
+            return FIELDS.get(anyUtils.anyTypeKind()).contains(field)
+                    ? ReflectionUtils.findField(getClass(), "anyUtils")
+                    : null;
+        });
+        when(plainSchemaDAO.find(anyString())).thenAnswer(ic -> {
+            String schemaName = ic.getArgument(0);
+            switch (schemaName) {
+                case "email":
+                case "firstname":
+                case "location":
+                    PlainSchema schema = mock(PlainSchema.class);
+                    when(schema.getKey()).thenReturn(schemaName);
+                    when(schema.getType()).thenReturn(AttrSchemaType.String);
+                    return schema;
+
+                default:
+                    return null;
+            }
+        });
+        when(derSchemaDAO.find(anyString())).thenAnswer(ic -> {
+            String schemaName = ic.getArgument(0);
+            switch (schemaName) {
+                case "cn":
+                    DerSchema schema = mock(DerSchema.class);
+                    when(schema.getKey()).thenReturn(ic.getArgument(0));
+                    return schema;
+
+                default:
+                    return null;
+            }
+        });
+        when(virSchemaDAO.find(anyString())).thenAnswer(ic -> {
+            String schemaName = ic.getArgument(0);
+            switch (schemaName) {
+                case "rvirtualdata":
+                    VirSchema schema = mock(VirSchema.class);
+                    when(schema.getKey()).thenReturn(ic.getArgument(0));
+                    return schema;
+
+                default:
+                    return null;
+            }
+        });
+    }
+
     @Test
     public void ownFields() throws ParseException {
         IntAttrName intAttrName = intAttrNameParser.parse("key", AnyTypeKind.USER);
@@ -231,8 +330,8 @@ public class IntAttrNameParserTest extends AbstractTest {
 
     @Test
     public void relationship() throws ParseException {
-        IntAttrName intAttrName = intAttrNameParser.parse("relationships[inclusion][PRINTER].location",
-                AnyTypeKind.USER);
+        IntAttrName intAttrName = intAttrNameParser.parse(
+                "relationships[inclusion][PRINTER].location", AnyTypeKind.USER);
         assertNotNull(intAttrName);
         assertEquals(AnyTypeKind.ANY_OBJECT, intAttrName.getAnyTypeKind());
         assertNull(intAttrName.getField());
diff --git a/core/provisioning-java/pom.xml b/core/provisioning-java/pom.xml
index a9610df..146e9d8 100644
--- a/core/provisioning-java/pom.xml
+++ b/core/provisioning-java/pom.xml
@@ -45,11 +45,6 @@ under the License.
     </dependency>
 
     <dependency>
-      <groupId>org.apache.commons</groupId>
-      <artifactId>commons-jexl3</artifactId>
-    </dependency>
-    
-    <dependency>
       <groupId>org.springframework</groupId>
       <artifactId>spring-context-support</artifactId>
     </dependency>
@@ -142,6 +137,11 @@ under the License.
       <scope>test</scope>
     </dependency>
     <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter</artifactId>
       <scope>test</scope>
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/ConnectorFacadeProxy.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/ConnectorFacadeProxy.java
index 16faf61..ad89d8f 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/ConnectorFacadeProxy.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/ConnectorFacadeProxy.java
@@ -24,7 +24,6 @@ import java.util.List;
 import java.util.Set;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
 import java.util.concurrent.atomic.AtomicReference;
 import org.apache.syncope.common.lib.types.ConnectorCapability;
 import org.apache.syncope.core.persistence.api.entity.ConnInstance;
@@ -33,7 +32,6 @@ import org.apache.syncope.core.provisioning.api.utils.ConnPoolConfUtils;
 import org.apache.syncope.core.provisioning.api.Connector;
 import org.apache.syncope.core.provisioning.api.TimeoutException;
 import org.apache.syncope.core.provisioning.api.pushpull.ReconFilterBuilder;
-import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
 import org.apache.syncope.core.spring.ApplicationContextProvider;
 import org.identityconnectors.common.security.GuardedByteArray;
 import org.identityconnectors.common.security.GuardedString;
@@ -49,9 +47,6 @@ import org.identityconnectors.framework.common.objects.ObjectClassInfo;
 import org.identityconnectors.framework.common.objects.OperationOptions;
 import org.identityconnectors.framework.common.objects.OperationOptionsBuilder;
 import org.identityconnectors.framework.common.objects.SearchResult;
-import org.identityconnectors.framework.common.objects.SortKey;
-import org.identityconnectors.framework.common.objects.SyncDeltaBuilder;
-import org.identityconnectors.framework.common.objects.SyncDeltaType;
 import org.identityconnectors.framework.common.objects.SyncResultsHandler;
 import org.identityconnectors.framework.common.objects.SyncToken;
 import org.identityconnectors.framework.common.objects.Uid;
@@ -310,7 +305,7 @@ public class ConnectorFacadeProxy implements Connector {
             final SyncResultsHandler handler,
             final OperationOptions options) {
 
-        filteredReconciliation(objectClass, null, handler, options);
+        Connector.super.fullReconciliation(objectClass, handler, options);
     }
 
     @Transactional
@@ -321,29 +316,7 @@ public class ConnectorFacadeProxy implements Connector {
             final SyncResultsHandler handler,
             final OperationOptions options) {
 
-        Filter filter = null;
-        OperationOptions actualOptions = options;
-        if (filterBuilder != null) {
-            filter = filterBuilder.build();
-            actualOptions = filterBuilder.build(actualOptions);
-        }
-
-        search(objectClass, filter, new SearchResultsHandler() {
-
-            @Override
-            public void handleResult(final SearchResult result) {
-                // nothing to do
-            }
-
-            @Override
-            public boolean handle(final ConnectorObject object) {
-                return handler.handle(new SyncDeltaBuilder().
-                        setObject(object).
-                        setDeltaType(SyncDeltaType.CREATE_OR_UPDATE).
-                        setToken(new SyncToken("")).
-                        build());
-            }
-        }, actualOptions);
+        Connector.super.filteredReconciliation(objectClass, filterBuilder, handler, options);
     }
 
     @Override
@@ -477,29 +450,6 @@ public class ConnectorFacadeProxy implements Connector {
     }
 
     @Override
-    public SearchResult search(
-            final ObjectClass objectClass,
-            final Filter filter,
-            final SearchResultsHandler handler,
-            final int pageSize,
-            final String pagedResultsCookie,
-            final List<OrderByClause> orderBy,
-            final OperationOptions options) {
-
-        OperationOptionsBuilder builder = new OperationOptionsBuilder().setPageSize(pageSize).setPagedResultsOffset(-1);
-        if (pagedResultsCookie != null) {
-            builder.setPagedResultsCookie(pagedResultsCookie);
-        }
-        builder.setSortKeys(orderBy.stream().map(clause
-                -> new SortKey(clause.getField(), clause.getDirection() == OrderByClause.Direction.ASC)).
-                collect(Collectors.toList()));
-
-        builder.setAttributesToGet(options.getAttributesToGet());
-
-        return search(objectClass, filter, handler, builder.build());
-    }
-
-    @Override
     public void dispose() {
         connector.dispose();
     }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultUserProvisioningManager.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultUserProvisioningManager.java
index bb86889..49da4df 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultUserProvisioningManager.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DefaultUserProvisioningManager.java
@@ -45,7 +45,7 @@ import org.apache.syncope.core.provisioning.api.propagation.PropagationReporter;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskExecutor;
 import org.apache.syncope.core.provisioning.api.VirAttrHandler;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
-import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.core.workflow.api.UserWorkflowAdapter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DerAttrHandlerImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DerAttrHandlerImpl.java
index 0f4757f..27dbef4 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DerAttrHandlerImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/DerAttrHandlerImpl.java
@@ -23,13 +23,13 @@ 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.provisioning.java.jexl.JexlUtils;
 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.DerSchema;
 import org.apache.syncope.core.persistence.api.entity.GroupableRelatable;
 import org.apache.syncope.core.persistence.api.entity.Membership;
 import org.apache.syncope.core.provisioning.api.DerAttrHandler;
+import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/MappingManagerImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/MappingManagerImpl.java
index 63fd67d..e84d043 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/MappingManagerImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/MappingManagerImpl.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.core.provisioning.java;
 
+import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Base64;
@@ -28,6 +29,8 @@ import java.util.Objects;
 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.reflect.FieldUtils;
@@ -101,7 +104,9 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
 import org.apache.syncope.core.provisioning.api.data.ItemTransformer;
+import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
 import org.identityconnectors.framework.common.objects.Name;
+import org.identityconnectors.framework.common.objects.Uid;
 
 @Component
 public class MappingManagerImpl implements MappingManager {
@@ -182,6 +187,87 @@ public class MappingManagerImpl implements MappingManager {
         return connObjectKey;
     }
 
+    private static Name getName(final String evalConnObjectLink, final String connObjectKey) {
+        // If connObjectLink evaluates to an empty string, just use the provided connObjectKey as Name(),
+        // otherwise evaluated connObjectLink expression is taken as Name().
+        Name name;
+        if (StringUtils.isBlank(evalConnObjectLink)) {
+            // add connObjectKey as __NAME__ attribute ...
+            LOG.debug("Add connObjectKey [{}] as {}", connObjectKey, Name.NAME);
+            name = new Name(connObjectKey);
+        } else {
+            LOG.debug("Add connObjectLink [{}] as {}", evalConnObjectLink, Name.NAME);
+            name = new Name(evalConnObjectLink);
+
+            // connObjectKey not propagated: it will be used to set the value for __UID__ attribute
+            LOG.debug("connObjectKey will be used just as {} attribute", Uid.NAME);
+        }
+
+        return name;
+    }
+
+    /**
+     * Build __NAME__ for propagation.
+     * First look if there is a defined connObjectLink for the given resource (and in
+     * this case evaluate as JEXL); otherwise, take given connObjectKey.
+     *
+     * @param any given any object
+     * @param provision external resource
+     * @param connObjectKey connector object key
+     * @return the value to be propagated as __NAME__
+     */
+    private Name evaluateNAME(final Any<?> any, final Provision provision, final String connObjectKey) {
+        if (StringUtils.isBlank(connObjectKey)) {
+            // LOG error but avoid to throw exception: leave it to the external resource
+            LOG.warn("Missing ConnObjectKey value for {}: ", provision.getResource());
+        }
+
+        // Evaluate connObjectKey expression
+        String connObjectLink = provision == null || provision.getMapping() == null
+                ? null
+                : provision.getMapping().getConnObjectLink();
+        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.evaluate(connObjectLink, jexlContext);
+        }
+
+        return getName(evalConnObjectLink, connObjectKey);
+    }
+
+    /**
+     * Build __NAME__ for propagation.
+     * First look if there is a defined connObjectLink for the given resource (and in
+     * this case evaluate as JEXL); otherwise, take given connObjectKey.
+     *
+     * @param realm given any object
+     * @param orgUnit external resource
+     * @param connObjectKey connector object key
+     * @return the value to be propagated as __NAME__
+     */
+    private Name evaluateNAME(final Realm realm, final OrgUnit orgUnit, final String connObjectKey) {
+        if (StringUtils.isBlank(connObjectKey)) {
+            // LOG error but avoid to throw exception: leave it to the external resource
+            LOG.warn("Missing ConnObjectKey value for {}: ", orgUnit.getResource());
+        }
+
+        // Evaluate connObjectKey expression
+        String connObjectLink = orgUnit == null
+                ? null
+                : orgUnit.getConnObjectLink();
+        String evalConnObjectLink = null;
+        if (StringUtils.isNotBlank(connObjectLink)) {
+            JexlContext jexlContext = new MapContext();
+            JexlUtils.addFieldsToContext(realm, jexlContext);
+            evalConnObjectLink = JexlUtils.evaluate(connObjectLink, jexlContext);
+        }
+
+        return getName(evalConnObjectLink, connObjectKey);
+    }
+
     @Transactional(readOnly = true)
     @Override
     public Pair<String, Set<Attribute>> prepareAttrs(
@@ -225,7 +311,7 @@ public class MappingManagerImpl implements MappingManager {
                 attributes.remove(connObjectKeyAttr);
                 attributes.add(AttributeBuilder.build(connObjectKeyItem.getExtAttrName(), connObjectKeyValue[0]));
             }
-            Name name = MappingUtils.evaluateNAME(any, provision, connObjectKeyValue[0]);
+            Name name = evaluateNAME(any, provision, connObjectKeyValue[0]);
             attributes.add(name);
             if (connObjectKeyAttr == null
                     && connObjectKeyValue[0] != null && !connObjectKeyValue[0].equals(name.getNameValue())) {
@@ -300,7 +386,7 @@ public class MappingManagerImpl implements MappingManager {
                 attributes.remove(connObjectKeyExtAttr);
                 attributes.add(AttributeBuilder.build(connObjectKeyItem.getExtAttrName(), connObjectKey));
             }
-            Name name = MappingUtils.evaluateNAME(user, provision, connObjectKey);
+            Name name = evaluateNAME(user, provision, connObjectKey);
             attributes.add(name);
             if (!connObjectKey.equals(name.getNameValue()) && connObjectKeyExtAttr == null) {
                 attributes.add(AttributeBuilder.build(connObjectKeyItem.getExtAttrName(), connObjectKey));
@@ -378,7 +464,7 @@ public class MappingManagerImpl implements MappingManager {
                 attributes.remove(connObjectKeyAttr);
                 attributes.add(AttributeBuilder.build(connObjectKeyItem.get().getExtAttrName(), connObjectKeyValue[0]));
             }
-            attributes.add(MappingUtils.evaluateNAME(realm, orgUnit, connObjectKeyValue[0]));
+            attributes.add(evaluateNAME(realm, orgUnit, connObjectKeyValue[0]));
         }
 
         return Pair.of(connObjectKeyValue[0], attributes);
@@ -634,24 +720,24 @@ public class MappingManagerImpl implements MappingManager {
 
                     default:
                         try {
-                            Object fieldValue = FieldUtils.readField(ref, intAttrName.getField(), true);
-                            if (fieldValue instanceof Date) {
-                                // needed because ConnId does not natively supports the Date type
-                                attrValue.setStringValue(DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.
-                                        format((Date) fieldValue));
-                            } else if (Boolean.TYPE.isInstance(fieldValue)) {
-                                attrValue.setBooleanValue((Boolean) fieldValue);
-                            } else if (Double.TYPE.isInstance(fieldValue) || Float.TYPE.isInstance(fieldValue)) {
-                                attrValue.setDoubleValue((Double) fieldValue);
-                            } else if (Long.TYPE.isInstance(fieldValue) || Integer.TYPE.isInstance(fieldValue)) {
-                                attrValue.setLongValue((Long) fieldValue);
-                            } else {
-                                attrValue.setStringValue(fieldValue.toString());
-                            }
-                            values.add(attrValue);
-                        } catch (Exception e) {
-                            LOG.error("Could not read value of '{}' from {}", intAttrName.getField(), ref, e);
+                        Object fieldValue = FieldUtils.readField(ref, intAttrName.getField(), true);
+                        if (fieldValue instanceof Date) {
+                            // needed because ConnId does not natively supports the Date type
+                            attrValue.setStringValue(DateFormatUtils.ISO_8601_EXTENDED_DATETIME_TIME_ZONE_FORMAT.
+                                    format((Date) fieldValue));
+                        } else if (Boolean.TYPE.isInstance(fieldValue)) {
+                            attrValue.setBooleanValue((Boolean) fieldValue);
+                        } else if (Double.TYPE.isInstance(fieldValue) || Float.TYPE.isInstance(fieldValue)) {
+                            attrValue.setDoubleValue((Double) fieldValue);
+                        } else if (Long.TYPE.isInstance(fieldValue) || Integer.TYPE.isInstance(fieldValue)) {
+                            attrValue.setLongValue((Long) fieldValue);
+                        } else {
+                            attrValue.setStringValue(fieldValue.toString());
                         }
+                        values.add(attrValue);
+                    } catch (Exception e) {
+                        LOG.error("Could not read value of '{}' from {}", intAttrName.getField(), ref, e);
+                    }
                 }
             } else if (intAttrName.getSchemaType() != null) {
                 switch (intAttrName.getSchemaType()) {
@@ -758,8 +844,8 @@ public class MappingManagerImpl implements MappingManager {
                     PlainAttrGetter.DEFAULT);
         }
 
-        return Optional.ofNullable(preparedAttr)
-                .map(attr -> MappingUtils.evaluateNAME(any, provision, attr.getKey()).getNameValue()).orElse(null);
+        return Optional.ofNullable(preparedAttr).
+                map(attr -> evaluateNAME(any, provision, attr.getKey()).getNameValue()).orElse(null);
     }
 
     @Transactional(readOnly = true)
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 8b14cd9..92507b0 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
@@ -29,6 +29,7 @@ import org.apache.syncope.core.provisioning.api.AnyObjectProvisioningManager;
 import org.apache.syncope.core.provisioning.api.AuditManager;
 import org.apache.syncope.core.provisioning.api.ConnIdBundleManager;
 import org.apache.syncope.core.provisioning.api.GroupProvisioningManager;
+import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.api.UserProvisioningManager;
 import org.apache.syncope.core.provisioning.api.cache.VirAttrCache;
 import org.apache.syncope.core.provisioning.api.job.JobManager;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java
index 4f9649a..baa598f 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/AbstractAnyDataBinder.java
@@ -73,8 +73,8 @@ 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.VirAttrHandler;
-import org.apache.syncope.core.provisioning.java.IntAttrNameParser;
-import org.apache.syncope.core.provisioning.java.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
+import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
 import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -226,7 +226,7 @@ abstract class AbstractAnyDataBinder {
                         AccountGetter.DEFAULT,
                         PlainAttrGetter.DEFAULT);
                 if (intValues.getRight().isEmpty()
-                        && JexlUtils.evaluateMandatoryCondition(mapItem.getMandatoryCondition(), any)) {
+                        && JexlUtils.evaluateMandatoryCondition(mapItem.getMandatoryCondition(), any, derAttrHandler)) {
 
                     missingAttrNames.add(mapItem.getIntAttrName());
                 }
@@ -256,7 +256,7 @@ abstract class AbstractAnyDataBinder {
         return reqValMissing;
     }
 
-    private static void checkMandatory(
+    private void checkMandatory(
             final PlainSchema schema,
             final PlainAttr<?> attr,
             final Any<?> any,
@@ -264,7 +264,7 @@ abstract class AbstractAnyDataBinder {
 
         if (attr == null
                 && !schema.isReadonly()
-                && JexlUtils.evaluateMandatoryCondition(schema.getMandatoryCondition(), any)) {
+                && JexlUtils.evaluateMandatoryCondition(schema.getMandatoryCondition(), any, derAttrHandler)) {
 
             LOG.error("Mandatory schema " + schema.getKey() + " not provided with values");
 
@@ -272,14 +272,13 @@ abstract class AbstractAnyDataBinder {
         }
     }
 
-    private static SyncopeClientException checkMandatory(final Any<?> any, final AnyUtils anyUtils) {
+    private SyncopeClientException checkMandatory(final Any<?> any, final AnyUtils anyUtils) {
         SyncopeClientException reqValMissing = SyncopeClientException.build(ClientExceptionType.RequiredValuesMissing);
 
         // Check if there is some mandatory schema defined for which no value has been provided
         AllowedSchemas<PlainSchema> allowedPlainSchemas = anyUtils.dao().findAllowedSchemas(any, PlainSchema.class);
-        allowedPlainSchemas.getForSelf()
-                .forEach(schema -> checkMandatory(schema, any.getPlainAttr(schema.getKey())
-                .orElse(null), any, reqValMissing));
+        allowedPlainSchemas.getForSelf().forEach(schema -> checkMandatory(
+                schema, any.getPlainAttr(schema.getKey()).orElse(null), any, reqValMissing));
         if (any instanceof GroupableRelatable) {
             allowedPlainSchemas.getForMemberships().forEach((group, schemas) -> {
                 GroupableRelatable<?, ?, ?, ?, ?> groupable = GroupableRelatable.class.cast(any);
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 e3d69e5..997b7d6 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
@@ -32,11 +32,16 @@ import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.persistence.api.entity.Entity;
 import org.apache.syncope.core.persistence.api.entity.PlainAttrValue;
 import org.apache.syncope.core.persistence.api.entity.resource.Item;
-import org.apache.syncope.core.provisioning.java.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.DerAttrHandler;
+import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
 import org.apache.syncope.core.provisioning.api.data.JEXLItemTransformer;
+import org.springframework.beans.factory.annotation.Autowired;
 
 public class JEXLItemTransformerImpl implements JEXLItemTransformer {
 
+    @Autowired
+    private DerAttrHandler derAttrHandler;
+
     private String propagationJEXL;
 
     private String pullJEXL;
@@ -67,7 +72,7 @@ public class JEXLItemTransformerImpl implements JEXLItemTransformer {
                         JexlUtils.addFieldsToContext(entity, jexlContext);
                         if (entity instanceof Any) {
                             JexlUtils.addPlainAttrsToContext(((Any<?>) entity).getPlainAttrs(), jexlContext);
-                            JexlUtils.addDerAttrsToContext(((Any<?>) entity), jexlContext);
+                            JexlUtils.addDerAttrsToContext(((Any<?>) entity), derAttrHandler, jexlContext);
                         }
                     }
                     jexlContext.set("value", originalValue);
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/NotificationDataBinderImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/NotificationDataBinderImpl.java
index c06d18d..0a5a695 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/NotificationDataBinderImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/NotificationDataBinderImpl.java
@@ -36,7 +36,7 @@ import org.apache.syncope.core.persistence.api.entity.AnyAbout;
 import org.apache.syncope.core.persistence.api.entity.AnyType;
 import org.apache.syncope.core.persistence.api.entity.Implementation;
 import org.apache.syncope.core.persistence.api.entity.MailTemplate;
-import org.apache.syncope.core.provisioning.java.IntAttrNameParser;
+import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
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 278f31b..96dad29 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
@@ -50,7 +50,7 @@ import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
 import org.apache.syncope.core.persistence.api.entity.resource.Mapping;
 import org.apache.syncope.core.persistence.api.entity.resource.MappingItem;
 import org.apache.syncope.core.persistence.api.entity.policy.PasswordPolicy;
-import org.apache.syncope.core.provisioning.java.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
 import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
 import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
@@ -67,7 +67,7 @@ import org.apache.syncope.core.persistence.api.entity.resource.Item;
 import org.apache.syncope.core.persistence.api.entity.resource.OrgUnit;
 import org.apache.syncope.core.persistence.api.entity.resource.OrgUnitItem;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
-import org.apache.syncope.core.provisioning.java.IntAttrNameParser;
+import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.api.IntAttrName;
 import org.apache.syncope.core.provisioning.api.data.ResourceDataBinder;
 import org.identityconnectors.framework.common.objects.ObjectClass;
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 38350c4..e5d48b0 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
@@ -33,7 +33,7 @@ import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
 import org.apache.syncope.core.persistence.api.entity.DerSchema;
 import org.apache.syncope.core.persistence.api.entity.PlainSchema;
 import org.apache.syncope.core.persistence.api.entity.VirSchema;
-import org.apache.syncope.core.provisioning.java.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeClassDAO;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
 import org.apache.syncope.core.persistence.api.dao.DerSchemaDAO;
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 efc039e..1d50916 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
@@ -19,11 +19,9 @@
 package org.apache.syncope.core.provisioning.java.data;
 
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
@@ -69,7 +67,6 @@ import org.apache.syncope.core.persistence.api.entity.Privilege;
 import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.persistence.api.entity.RelationshipType;
 import org.apache.syncope.core.persistence.api.entity.Role;
-import org.apache.syncope.core.persistence.api.entity.VirSchema;
 import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
 import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
 import org.apache.syncope.core.persistence.api.entity.user.LAPlainAttr;
@@ -726,14 +723,11 @@ public class UserDataBinderImpl extends AbstractAnyDataBinder implements UserDat
             userTO.setSecurityQuestion(user.getSecurityQuestion().getKey());
         }
 
-        Map<VirSchema, List<String>> virAttrValues = details
-                ? virAttrHandler.getValues(user)
-                : Collections.<VirSchema, List<String>>emptyMap();
         fillTO(userTO, user.getRealm().getFullPath(),
                 user.getAuxClasses(),
                 user.getPlainAttrs(),
                 derAttrHandler.getValues(user),
-                virAttrValues,
+                details ? virAttrHandler.getValues(user) : Map.of(),
                 userDAO.findAllResources(user),
                 details);
 
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 37a13e7..e1f9220 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
@@ -73,8 +73,8 @@ 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.notification.NotificationManager;
 import org.apache.syncope.core.provisioning.api.notification.RecipientsProvider;
-import org.apache.syncope.core.provisioning.java.IntAttrNameParser;
-import org.apache.syncope.core.provisioning.java.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
+import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
 import org.apache.syncope.core.spring.ImplementationManager;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java
index aa7bde0..7895244 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/AbstractPropagationTaskExecutor.java
@@ -338,12 +338,21 @@ public abstract class AbstractPropagationTaskExecutor implements PropagationTask
     }
 
     @Override
-    public TaskExec execute(final PropagationTaskInfo taskInfo, final PropagationReporter reporter,
+    public TaskExec execute(
+            final PropagationTaskInfo taskInfo,
+            final PropagationReporter reporter,
             final String executor) {
+
         PropagationTask task;
         if (taskInfo.getKey() == null) {
+            // double-checks that provided External Resource is valid, for further actions
+            ExternalResource resource = resourceDAO.find(taskInfo.getResource());
+            if (resource == null) {
+                resource = taskInfo.getExternalResource();
+            }
+
             task = entityFactory.newEntity(PropagationTask.class);
-            task.setResource(resourceDAO.find(taskInfo.getResource()));
+            task.setResource(resource);
             task.setObjectClassName(taskInfo.getObjectClassName());
             task.setAnyTypeKind(taskInfo.getAnyTypeKind());
             task.setAnyType(taskInfo.getAnyType());
@@ -360,9 +369,11 @@ public abstract class AbstractPropagationTaskExecutor implements PropagationTask
         }
         task.setAttributes(attributes);
 
-        List<PropagationActions> actions = getPropagationActions(task.getResource());
+        Connector connector = taskInfo.getConnector() == null
+                ? connFactory.getConnector(task.getResource())
+                : taskInfo.getConnector();
 
-        String resource = task.getResource().getKey();
+        List<PropagationActions> actions = getPropagationActions(task.getResource());
 
         Date start = new Date();
 
@@ -382,12 +393,10 @@ public abstract class AbstractPropagationTaskExecutor implements PropagationTask
         Provision provision = null;
         OrgUnit orgUnit = null;
         Uid uid = null;
-        Connector connector = null;
         Result result;
         try {
             provision = task.getResource().getProvision(new ObjectClass(task.getObjectClassName())).orElse(null);
             orgUnit = task.getResource().getOrgUnit();
-            connector = connFactory.getConnector(task.getResource());
 
             if (taskInfo.getBeforeObj() == null) {
                 // Try to read remote object BEFORE any actual operation
@@ -425,7 +434,7 @@ public abstract class AbstractPropagationTaskExecutor implements PropagationTask
             result = Result.SUCCESS;
         } catch (Exception e) {
             result = Result.FAILURE;
-            LOG.error("Exception during provision on resource " + resource, e);
+            LOG.error("Exception during provision on resource " + task.getResource().getKey(), e);
 
             if (e instanceof ConnectorException && e.getCause() != null) {
                 taskExecutionMessage = e.getCause().getMessage();
@@ -509,12 +518,12 @@ public abstract class AbstractPropagationTaskExecutor implements PropagationTask
         String anyTypeKind = task.getAnyTypeKind() == null ? "realm" : task.getAnyTypeKind().name().toLowerCase();
         String operation = task.getOperation().name().toLowerCase();
         boolean notificationsAvailable = notificationManager.notificationsAvailable(
-                AuditElements.EventCategoryType.PROPAGATION, anyTypeKind, resource, operation);
+                AuditElements.EventCategoryType.PROPAGATION, anyTypeKind, task.getResource().getKey(), operation);
         boolean auditRequested = auditManager.auditRequested(
                 AuthContextUtils.getUsername(),
                 AuditElements.EventCategoryType.PROPAGATION,
                 anyTypeKind,
-                resource,
+                task.getResource().getKey(),
                 operation);
 
         if (notificationsAvailable || auditRequested) {
@@ -523,7 +532,7 @@ public abstract class AbstractPropagationTaskExecutor implements PropagationTask
                     AuthContextUtils.getUsername(),
                     AuditElements.EventCategoryType.PROPAGATION,
                     anyTypeKind,
-                    resource,
+                    task.getResource().getKey(),
                     operation,
                     result,
                     beforeObj,
@@ -534,7 +543,7 @@ public abstract class AbstractPropagationTaskExecutor implements PropagationTask
                     AuthContextUtils.getUsername(),
                     AuditElements.EventCategoryType.PROPAGATION,
                     anyTypeKind,
-                    resource,
+                    task.getResource().getKey(),
                     operation,
                     result,
                     beforeObj,
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 45c5f63..854340d 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
@@ -32,11 +32,12 @@ import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.entity.group.Group;
 import org.apache.syncope.core.persistence.api.entity.task.PropagationTask;
 import org.apache.syncope.core.persistence.api.entity.user.User;
-import org.apache.syncope.core.provisioning.java.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
+import org.apache.syncope.core.provisioning.api.DerAttrHandler;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationActions;
 import org.identityconnectors.framework.common.objects.Attribute;
 import org.identityconnectors.framework.common.objects.AttributeBuilder;
@@ -58,6 +59,9 @@ public class LDAPMembershipPropagationActions implements PropagationActions {
     protected static final Logger LOG = LoggerFactory.getLogger(LDAPMembershipPropagationActions.class);
 
     @Autowired
+    protected DerAttrHandler derAttrHandler;
+
+    @Autowired
     protected AnyTypeDAO anyTypeDAO;
 
     @Autowired
@@ -119,11 +123,9 @@ public class LDAPMembershipPropagationActions implements PropagationActions {
 
                         Attribute beforeLdapGroups = beforeObj.getAttributeByName(getGroupMembershipAttrName());
                         LOG.debug("Memberships not managed by Syncope: {}", beforeLdapGroups);
-                        for (Object value : beforeLdapGroups.getValue()) {
-                            if (!connObjectLinks.contains(String.valueOf(value))) {
-                                groups.add(String.valueOf(value));
-                            }
-                        }
+                        beforeLdapGroups.getValue().stream().
+                                filter(value -> !connObjectLinks.contains(String.valueOf(value))).
+                                forEach(value -> groups.add(String.valueOf(value)));
                     }
                 }
                 LOG.debug("Add ldapGroups to attributes: {}" + groups);
@@ -136,13 +138,13 @@ public class LDAPMembershipPropagationActions implements PropagationActions {
         }
     }
 
-    private static String evaluateGroupConnObjectLink(final String connObjectLinkTemplate, final Group group) {
+    private 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, jexlContext);
+        JexlUtils.addDerAttrsToContext(group, derAttrHandler, jexlContext);
 
         return JexlUtils.evaluate(connObjectLinkTemplate, jexlContext);
     }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PriorityPropagationTaskExecutor.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PriorityPropagationTaskExecutor.java
index 0a60ba2..b6db281 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PriorityPropagationTaskExecutor.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PriorityPropagationTaskExecutor.java
@@ -18,7 +18,6 @@
  */
 package org.apache.syncope.core.provisioning.java.propagation;
 
-import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
@@ -36,7 +35,6 @@ import java.util.stream.Collectors;
 import javax.annotation.Resource;
 import org.apache.syncope.common.lib.types.ExecStatus;
 import org.apache.syncope.core.persistence.api.entity.Exec;
-import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
 import org.apache.syncope.core.spring.ApplicationContextProvider;
 import org.apache.syncope.core.persistence.api.entity.task.TaskExec;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationException;
@@ -85,30 +83,25 @@ public class PriorityPropagationTaskExecutor extends AbstractPropagationTaskExec
             final boolean nullPriorityAsync,
             final String executorId) {
 
-        Map<PropagationTaskInfo, ExternalResource> taskToResource = new HashMap<>(taskInfos.size());
         List<PropagationTaskInfo> prioritizedTasks = new ArrayList<>();
 
         int[] connRequestTimeout = { 60 };
 
-        taskInfos.forEach(task -> {
-            ExternalResource resource = resourceDAO.find(task.getResource());
-            taskToResource.put(task, resource);
+        taskInfos.stream().filter(task -> task.getExternalResource().getPropagationPriority() != null).forEach(task -> {
+            prioritizedTasks.add(task);
 
-            if (resource.getPropagationPriority() != null) {
-                prioritizedTasks.add(task);
+            if (task.getExternalResource().getConnector().getConnRequestTimeout() != null
+                    && connRequestTimeout[0] < task.getExternalResource().getConnector().getConnRequestTimeout()) {
 
-                if (resource.getConnector().getConnRequestTimeout() != null
-                        && connRequestTimeout[0] < resource.getConnector().getConnRequestTimeout()) {
-                    connRequestTimeout[0] = resource.getConnector().getConnRequestTimeout();
-                    LOG.debug("Upgrade request connection timeout to {}", connRequestTimeout);
-                }
+                connRequestTimeout[0] = task.getExternalResource().getConnector().getConnRequestTimeout();
+                LOG.debug("Upgrade request connection timeout to {}", connRequestTimeout);
             }
         });
 
-        prioritizedTasks.sort(new PriorityComparator(taskToResource));
+        prioritizedTasks.sort(Comparator.comparing(task -> task.getExternalResource().getPropagationPriority()));
         LOG.debug("Propagation tasks sorted by priority, for serial execution: {}", prioritizedTasks);
 
-        Collection<PropagationTaskInfo> concurrentTasks = taskInfos.stream().
+        Set<PropagationTaskInfo> concurrentTasks = taskInfos.stream().
                 filter(task -> !prioritizedTasks.contains(task)).collect(Collectors.toSet());
         LOG.debug("Propagation tasks for concurrent execution: {}", concurrentTasks);
 
@@ -171,30 +164,4 @@ public class PriorityPropagationTaskExecutor extends AbstractPropagationTaskExec
             }
         }
     }
-
-    /**
-     * Compare propagation tasks according to related ExternalResource's priority.
-     */
-    protected static class PriorityComparator implements Comparator<PropagationTaskInfo>, Serializable {
-
-        private static final long serialVersionUID = -1969355670784448878L;
-
-        private final Map<PropagationTaskInfo, ExternalResource> taskToResource;
-
-        public PriorityComparator(final Map<PropagationTaskInfo, ExternalResource> taskToResource) {
-            this.taskToResource = taskToResource;
-        }
-
-        @Override
-        public int compare(final PropagationTaskInfo task1, final PropagationTaskInfo task2) {
-            int prop1 = taskToResource.get(task1).getPropagationPriority();
-            int prop2 = taskToResource.get(task2).getPropagationPriority();
-
-            return prop1 > prop2
-                    ? 1
-                    : prop1 == prop2
-                            ? 0
-                            : -1;
-        }
-    }
 }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PropagationManagerImpl.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PropagationManagerImpl.java
index 1c38712..9f7b979 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PropagationManagerImpl.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/propagation/PropagationManagerImpl.java
@@ -18,6 +18,15 @@
  */
 package org.apache.syncope.core.provisioning.java.propagation;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
 import java.util.stream.Stream;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Pair;
@@ -43,14 +52,15 @@ import org.apache.syncope.core.persistence.api.entity.resource.OrgUnit;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
 import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount;
 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.MappingManager;
 import org.apache.syncope.core.provisioning.api.PropagationByResource;
 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.UserWorkflowResult;
+import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskExecutor;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
-import org.apache.syncope.core.provisioning.java.jexl.JexlUtils;
 import org.apache.syncope.core.provisioning.java.utils.ConnObjectUtils;
 import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
 import org.identityconnectors.framework.common.objects.Attribute;
@@ -61,15 +71,6 @@ import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
 
 /**
  * Manage the data propagation to external resources.
@@ -85,30 +86,18 @@ public class PropagationManagerImpl implements PropagationManager {
     @Autowired
     protected AnyObjectDAO anyObjectDAO;
 
-    /**
-     * User DAO.
-     */
     @Autowired
     protected UserDAO userDAO;
 
-    /**
-     * Group DAO.
-     */
     @Autowired
     protected GroupDAO groupDAO;
 
-    /**
-     * Resource DAO.
-     */
     @Autowired
     protected ExternalResourceDAO resourceDAO;
 
     @Autowired
     protected EntityFactory entityFactory;
 
-    /**
-     * ConnObjectUtils.
-     */
     @Autowired
     protected ConnObjectUtils connObjectUtils;
 
@@ -116,6 +105,9 @@ public class PropagationManagerImpl implements PropagationManager {
     protected MappingManager mappingManager;
 
     @Autowired
+    protected DerAttrHandler derAttrHandler;
+
+    @Autowired
     protected AnyUtilsFactory anyUtilsFactory;
 
     protected AnyDAO<? extends Any<?>> dao(final AnyTypeKind kind) {
@@ -388,17 +380,18 @@ public class PropagationManagerImpl implements PropagationManager {
         return createTasks(any, null, false, false, true, localPropByRes, propByLinkedAccount, null);
     }
 
-    protected PropagationTaskInfo newTask(
+    @Override
+    public PropagationTaskInfo newTask(
+            final DerAttrHandler derAttrHandler,
             final Any<?> any,
-            final String resource,
+            final ExternalResource resource,
             final ResourceOperation operation,
             final Provision provision,
             final boolean deleteOnResource,
             final Stream<? extends Item> mappingItems,
             final Pair<String, Set<Attribute>> preparedAttrs) {
 
-        PropagationTaskInfo task = new PropagationTaskInfo();
-        task.setResource(resource);
+        PropagationTaskInfo task = new PropagationTaskInfo(resource);
         task.setObjectClassName(provision.getObjectClass().getObjectClassValue());
         task.setAnyTypeKind(any.getType().getKind());
         task.setAnyType(any.getType().getKey());
@@ -413,15 +406,16 @@ public class PropagationManagerImpl implements PropagationManager {
         List<String> mandatoryMissing = new ArrayList<>();
         List<String> mandatoryNullOrEmpty = new ArrayList<>();
         mappingItems.filter(item -> (!item.isConnObjectKey()
-                && JexlUtils.evaluateMandatoryCondition(item.getMandatoryCondition(), any))).forEach(item -> {
-
-            Attribute attr = AttributeUtil.find(item.getExtAttrName(), preparedAttrs.getRight());
-            if (attr == null) {
-                mandatoryMissing.add(item.getExtAttrName());
-            } else if (CollectionUtils.isEmpty(attr.getValue())) {
-                mandatoryNullOrEmpty.add(item.getExtAttrName());
-            }
-        });
+                && JexlUtils.evaluateMandatoryCondition(item.getMandatoryCondition(), any, derAttrHandler))).
+                forEach(item -> {
+
+                    Attribute attr = AttributeUtil.find(item.getExtAttrName(), preparedAttrs.getRight());
+                    if (attr == null) {
+                        mandatoryMissing.add(item.getExtAttrName());
+                    } else if (CollectionUtils.isEmpty(attr.getValue())) {
+                        mandatoryNullOrEmpty.add(item.getExtAttrName());
+                    }
+                });
         if (!mandatoryMissing.isEmpty()) {
             preparedAttrs.getRight().add(AttributeBuilder.build(
                     PropagationTaskExecutor.MANDATORY_MISSING_ATTR_NAME, mandatoryMissing));
@@ -528,8 +522,9 @@ public class PropagationManagerImpl implements PropagationManager {
                 }
 
                 PropagationTaskInfo task = newTask(
+                        derAttrHandler,
                         any,
-                        resourceKey,
+                        resource,
                         operation,
                         provision,
                         deleteOnResource,
@@ -571,8 +566,9 @@ public class PropagationManagerImpl implements PropagationManager {
                             AnyTypeKind.USER.name(), account.getResource());
                 } else {
                     PropagationTaskInfo accountTask = newTask(
+                            derAttrHandler,
                             user,
-                            account.getResource().getKey(),
+                            account.getResource(),
                             operation,
                             provision,
                             deleteOnResource,
@@ -620,8 +616,7 @@ public class PropagationManagerImpl implements PropagationManager {
                 LOG.warn("Requesting propagation for {} but no ConnObjectLink provided for {}",
                         realm.getFullPath(), resource);
             } else {
-                PropagationTaskInfo task = new PropagationTaskInfo();
-                task.setResource(resource.getKey());
+                PropagationTaskInfo task = new PropagationTaskInfo(resource);
                 task.setObjectClassName(orgUnit.getObjectClass().getObjectClassValue());
                 task.setEntityKey(realm.getKey());
                 task.setOperation(operation);
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractProvisioningJobDelegate.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractProvisioningJobDelegate.java
index 5f06bfd..25a85c4 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractProvisioningJobDelegate.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractProvisioningJobDelegate.java
@@ -22,6 +22,7 @@ import java.lang.reflect.ParameterizedType;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Objects;
 import javax.annotation.Resource;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
@@ -36,7 +37,7 @@ import org.apache.syncope.core.persistence.api.entity.task.ProvisioningTask;
 import org.apache.syncope.core.persistence.api.entity.task.TaskExec;
 import org.apache.syncope.core.provisioning.api.Connector;
 import org.apache.syncope.core.provisioning.api.ConnectorFactory;
-import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.core.provisioning.java.job.AbstractSchedTaskJobDelegate;
 import org.apache.syncope.core.provisioning.java.job.TaskJob;
 import org.quartz.JobExecutionException;
@@ -50,6 +51,37 @@ public abstract class AbstractProvisioningJobDelegate<T extends ProvisioningTask
 
     private static final String LINKED_ACCOUNT = "LINKED_ACCOUNT";
 
+    /**
+     * Helper method to invoke logging per provisioning result, for the given trace level.
+     *
+     * @param results provisioning results
+     * @param level trace level
+     * @return report as string
+     */
+    public static String generate(final Collection<ProvisioningReport> results, final TraceLevel level) {
+        StringBuilder sb = new StringBuilder();
+
+        results.stream().map(result -> {
+            if (level == TraceLevel.SUMMARY) {
+                // No per entry log in this case.
+                return null;
+            } else if (level == TraceLevel.FAILURES && result.getStatus() == ProvisioningReport.Status.FAILURE) {
+                // only report failures
+                return String.format("Failed %s (key/name): %s/%s with message: %s",
+                        result.getOperation(), result.getKey(), result.getName(), result.getMessage());
+            } else {
+                // All
+                return String.format("%s %s (key/name): %s/%s %s",
+                        result.getOperation(), result.getStatus(), result.getKey(), result.getName(),
+                        StringUtils.isBlank(result.getMessage())
+                        ? StringUtils.EMPTY
+                        : "with message: " + result.getMessage());
+            }
+        }).filter(Objects::nonNull).forEach(report -> sb.append(report).append('\n'));
+
+        return sb.toString();
+    }
+
     @Resource(name = "adminUser")
     protected String adminUser;
 
@@ -412,71 +444,71 @@ public abstract class AbstractProvisioningJobDelegate<T extends ProvisioningTask
             if (includeUser) {
                 if (!uFailCreate.isEmpty()) {
                     report.append("\n\nUsers failed to create: ");
-                    report.append(ProvisioningReport.generate(uFailCreate, traceLevel));
+                    report.append(generate(uFailCreate, traceLevel));
                 }
                 if (!uFailUpdate.isEmpty()) {
                     report.append("\nUsers failed to update: ");
-                    report.append(ProvisioningReport.generate(uFailUpdate, traceLevel));
+                    report.append(generate(uFailUpdate, traceLevel));
                 }
                 if (!uFailDelete.isEmpty()) {
                     report.append("\nUsers failed to delete: ");
-                    report.append(ProvisioningReport.generate(uFailDelete, traceLevel));
+                    report.append(generate(uFailDelete, traceLevel));
                 }
 
                 if (!laFailCreate.isEmpty()) {
                     report.append("\n\nAccounts failed to create: ");
-                    report.append(ProvisioningReport.generate(laFailCreate, traceLevel));
+                    report.append(generate(laFailCreate, traceLevel));
                 }
                 if (!laFailUpdate.isEmpty()) {
                     report.append("\nAccounts failed to update: ");
-                    report.append(ProvisioningReport.generate(laFailUpdate, traceLevel));
+                    report.append(generate(laFailUpdate, traceLevel));
                 }
                 if (!laFailDelete.isEmpty()) {
                     report.append("\nAccounts failed to delete: ");
-                    report.append(ProvisioningReport.generate(laFailDelete, traceLevel));
+                    report.append(generate(laFailDelete, traceLevel));
                 }
             }
 
             if (includeGroup) {
                 if (!gFailCreate.isEmpty()) {
                     report.append("\n\nGroups failed to create: ");
-                    report.append(ProvisioningReport.generate(gFailCreate, traceLevel));
+                    report.append(generate(gFailCreate, traceLevel));
                 }
                 if (!gFailUpdate.isEmpty()) {
                     report.append("\nGroups failed to update: ");
-                    report.append(ProvisioningReport.generate(gFailUpdate, traceLevel));
+                    report.append(generate(gFailUpdate, traceLevel));
                 }
                 if (!gFailDelete.isEmpty()) {
                     report.append("\nGroups failed to delete: ");
-                    report.append(ProvisioningReport.generate(gFailDelete, traceLevel));
+                    report.append(generate(gFailDelete, traceLevel));
                 }
             }
 
             if (includeAnyObject && !aFailCreate.isEmpty()) {
                 report.append("\nAny objects failed to create: ");
-                report.append(ProvisioningReport.generate(aFailCreate, traceLevel));
+                report.append(generate(aFailCreate, traceLevel));
             }
             if (includeAnyObject && !aFailUpdate.isEmpty()) {
                 report.append("\nAny objects failed to update: ");
-                report.append(ProvisioningReport.generate(aFailUpdate, traceLevel));
+                report.append(generate(aFailUpdate, traceLevel));
             }
             if (includeAnyObject && !aFailDelete.isEmpty()) {
                 report.append("\nAny objects failed to delete: ");
-                report.append(ProvisioningReport.generate(aFailDelete, traceLevel));
+                report.append(generate(aFailDelete, traceLevel));
             }
 
             if (includeRealm) {
                 if (!rFailCreate.isEmpty()) {
                     report.append("\nRealms failed to create: ");
-                    report.append(ProvisioningReport.generate(rFailCreate, traceLevel));
+                    report.append(generate(rFailCreate, traceLevel));
                 }
                 if (!rFailUpdate.isEmpty()) {
                     report.append("\nRealms failed to update: ");
-                    report.append(ProvisioningReport.generate(rFailUpdate, traceLevel));
+                    report.append(generate(rFailUpdate, traceLevel));
                 }
                 if (!rFailDelete.isEmpty()) {
                     report.append("\nRealms failed to delete: ");
-                    report.append(ProvisioningReport.generate(rFailDelete, traceLevel));
+                    report.append(generate(rFailDelete, traceLevel));
                 }
             }
         }
@@ -486,110 +518,110 @@ public abstract class AbstractProvisioningJobDelegate<T extends ProvisioningTask
             if (includeUser) {
                 if (!uSuccCreate.isEmpty()) {
                     report.append("\n\nUsers created:\n").
-                            append(ProvisioningReport.generate(uSuccCreate, traceLevel));
+                            append(generate(uSuccCreate, traceLevel));
                 }
                 if (!uSuccUpdate.isEmpty()) {
                     report.append("\nUsers updated:\n").
-                            append(ProvisioningReport.generate(uSuccUpdate, traceLevel));
+                            append(generate(uSuccUpdate, traceLevel));
                 }
                 if (!uSuccDelete.isEmpty()) {
                     report.append("\nUsers deleted:\n").
-                            append(ProvisioningReport.generate(uSuccDelete, traceLevel));
+                            append(generate(uSuccDelete, traceLevel));
                 }
                 if (!uSuccNone.isEmpty()) {
                     report.append("\nUsers no operation:\n").
-                            append(ProvisioningReport.generate(uSuccNone, traceLevel));
+                            append(generate(uSuccNone, traceLevel));
                 }
                 if (!uIgnore.isEmpty()) {
                     report.append("\nUsers ignored:\n").
-                            append(ProvisioningReport.generate(uIgnore, traceLevel));
+                            append(generate(uIgnore, traceLevel));
                 }
 
                 if (!laSuccCreate.isEmpty()) {
                     report.append("\n\nAccounts created:\n").
-                            append(ProvisioningReport.generate(laSuccCreate, traceLevel));
+                            append(generate(laSuccCreate, traceLevel));
                 }
                 if (!laSuccUpdate.isEmpty()) {
                     report.append("\nAccounts updated:\n").
-                            append(ProvisioningReport.generate(laSuccUpdate, traceLevel));
+                            append(generate(laSuccUpdate, traceLevel));
                 }
                 if (!laSuccDelete.isEmpty()) {
                     report.append("\nAccounts deleted:\n").
-                            append(ProvisioningReport.generate(laSuccDelete, traceLevel));
+                            append(generate(laSuccDelete, traceLevel));
                 }
                 if (!laSuccNone.isEmpty()) {
                     report.append("\nAccounts no operation:\n").
-                            append(ProvisioningReport.generate(laSuccNone, traceLevel));
+                            append(generate(laSuccNone, traceLevel));
                 }
                 if (!laIgnore.isEmpty()) {
                     report.append("\nAccounts ignored:\n").
-                            append(ProvisioningReport.generate(laIgnore, traceLevel));
+                            append(generate(laIgnore, traceLevel));
                 }
             }
             if (includeGroup) {
                 if (!gSuccCreate.isEmpty()) {
                     report.append("\n\nGroups created:\n").
-                            append(ProvisioningReport.generate(gSuccCreate, traceLevel));
+                            append(generate(gSuccCreate, traceLevel));
                 }
                 if (!gSuccUpdate.isEmpty()) {
                     report.append("\nGroups updated:\n").
-                            append(ProvisioningReport.generate(gSuccUpdate, traceLevel));
+                            append(generate(gSuccUpdate, traceLevel));
                 }
                 if (!gSuccDelete.isEmpty()) {
                     report.append("\nGroups deleted:\n").
-                            append(ProvisioningReport.generate(gSuccDelete, traceLevel));
+                            append(generate(gSuccDelete, traceLevel));
                 }
                 if (!gSuccNone.isEmpty()) {
                     report.append("\nGroups no operation:\n").
-                            append(ProvisioningReport.generate(gSuccNone, traceLevel));
+                            append(generate(gSuccNone, traceLevel));
                 }
                 if (!gIgnore.isEmpty()) {
                     report.append("\nGroups ignored:\n").
-                            append(ProvisioningReport.generate(gIgnore, traceLevel));
+                            append(generate(gIgnore, traceLevel));
                 }
             }
             if (includeAnyObject) {
                 if (!aSuccCreate.isEmpty()) {
                     report.append("\n\nAny objects created:\n").
-                            append(ProvisioningReport.generate(aSuccCreate, traceLevel));
+                            append(generate(aSuccCreate, traceLevel));
                 }
                 if (!aSuccUpdate.isEmpty()) {
                     report.append("\nAny objects updated:\n").
-                            append(ProvisioningReport.generate(aSuccUpdate, traceLevel));
+                            append(generate(aSuccUpdate, traceLevel));
                 }
                 if (!aSuccDelete.isEmpty()) {
                     report.append("\nAny objects deleted:\n").
-                            append(ProvisioningReport.generate(aSuccDelete, traceLevel));
+                            append(generate(aSuccDelete, traceLevel));
                 }
                 if (!aSuccNone.isEmpty()) {
                     report.append("\nAny objects no operation:\n").
-                            append(ProvisioningReport.generate(aSuccNone, traceLevel));
+                            append(generate(aSuccNone, traceLevel));
                 }
                 if (!aIgnore.isEmpty()) {
                     report.append("\nAny objects ignored:\n").
-                            append(ProvisioningReport.generate(aIgnore, traceLevel));
+                            append(generate(aIgnore, traceLevel));
                 }
             }
             if (includeRealm) {
                 if (!rSuccCreate.isEmpty()) {
                     report.append("\n\nRealms created:\n").
-                            append(ProvisioningReport.generate(rSuccCreate, traceLevel));
+                            append(generate(rSuccCreate, traceLevel));
                 }
                 if (!rSuccUpdate.isEmpty()) {
                     report.append("\nRealms updated:\n").
-                            append(ProvisioningReport.generate(rSuccUpdate, traceLevel));
+                            append(generate(rSuccUpdate, traceLevel));
                 }
                 if (!rSuccDelete.isEmpty()) {
                     report.append("\nRealms deleted:\n").
-                            append(ProvisioningReport.generate(rSuccDelete, traceLevel));
+                            append(generate(rSuccDelete, traceLevel));
                 }
                 if (!rSuccNone.isEmpty()) {
                     report.append("\nRealms no operation:\n").
-                            append(ProvisioningReport.generate(rSuccNone, traceLevel));
+                            append(generate(rSuccNone, traceLevel));
                 }
                 if (!rIgnore.isEmpty()) {
                     report.append("\nRealms ignored:\n").
-                            append(ProvisioningReport.generate(rIgnore, traceLevel));
+                            append(generate(rIgnore, traceLevel));
                 }
             }
         }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractPullResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractPullResultHandler.java
index 15ec686..9884a63 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractPullResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractPullResultHandler.java
@@ -19,9 +19,9 @@
 package org.apache.syncope.core.provisioning.java.pushpull;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.Date;
 import java.util.List;
+import java.util.Set;
 import java.util.stream.Collectors;
 import org.apache.commons.lang3.exception.ExceptionUtils;
 import org.apache.syncope.common.lib.AnyOperations;
@@ -55,7 +55,7 @@ import org.apache.syncope.core.provisioning.api.cache.VirAttrCache;
 import org.apache.syncope.core.provisioning.api.cache.VirAttrCacheValue;
 import org.apache.syncope.core.provisioning.api.notification.NotificationManager;
 import org.apache.syncope.core.provisioning.api.pushpull.IgnoreProvisionException;
-import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.core.provisioning.api.pushpull.PullActions;
 import org.apache.syncope.core.persistence.api.dao.PullMatch;
 import org.apache.syncope.core.provisioning.api.pushpull.SyncopePullExecutor;
@@ -188,7 +188,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
         if (!profile.getTask().isPerformCreate()) {
             LOG.debug("PullTask not configured for create");
             end(provision.getAnyType().getKind(), UnmatchingRule.toEventName(rule), Result.SUCCESS, null, null, delta);
-            return Collections.<ProvisioningReport>emptyList();
+            return List.of();
         }
 
         AnyCR anyCR = connObjectUtils.getAnyCR(delta.getObject(), profile.getTask(), provision, true);
@@ -263,7 +263,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
             end(provision.getAnyType().getKind(), UnmatchingRule.toEventName(rule), resultStatus, null, output, delta);
         }
 
-        return Collections.singletonList(result);
+        return List.of(result);
     }
 
     protected void throwIgnoreProvisionException(final SyncDelta delta, final Exception exception)
@@ -293,7 +293,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
             LOG.debug("PullTask not configured for update");
             end(provision.getAnyType().getKind(),
                     MatchingRule.toEventName(MatchingRule.UPDATE), Result.SUCCESS, null, null, delta);
-            return Collections.<ProvisioningReport>emptyList();
+            return List.of();
         }
 
         LOG.debug("About to update {}", matches);
@@ -402,7 +402,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
             LOG.debug("PullTask not configured for update");
             end(provision.getAnyType().getKind(),
                     MatchingRule.toEventName(matchingRule), Result.SUCCESS, null, null, delta);
-            return Collections.<ProvisioningReport>emptyList();
+            return List.of();
         }
 
         LOG.debug("About to deprovision {}", matches);
@@ -519,7 +519,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                             ? MatchingRule.toEventName(MatchingRule.UNLINK)
                             : MatchingRule.toEventName(MatchingRule.LINK),
                     Result.SUCCESS, null, null, delta);
-            return Collections.<ProvisioningReport>emptyList();
+            return List.of();
         }
 
         LOG.debug("About to update {}", matches);
@@ -619,7 +619,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
             LOG.debug("PullTask not configured for delete");
             end(provision.getAnyType().getKind(),
                     ResourceOperation.DELETE.name().toLowerCase(), Result.SUCCESS, null, null, delta);
-            return Collections.<ProvisioningReport>emptyList();
+            return List.of();
         }
 
         LOG.debug("About to delete {}", matches);
@@ -649,7 +649,7 @@ public abstract class AbstractPullResultHandler extends AbstractSyncopeResultHan
                     try {
                         getProvisioningManager().delete(
                                 match.getAny().getKey(),
-                                Collections.singleton(profile.getTask().getResource().getKey()),
+                                Set.of(profile.getTask().getResource().getKey()),
                                 true);
                         output = null;
                         resultStatus = Result.SUCCESS;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractPushResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractPushResultHandler.java
index f8a4e1b..50310f2 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractPushResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/AbstractPushResultHandler.java
@@ -39,7 +39,7 @@ import org.apache.syncope.common.lib.types.ResourceOperation;
 import org.apache.syncope.common.lib.types.UnmatchingRule;
 import org.apache.syncope.core.persistence.api.entity.task.PushTask;
 import org.apache.syncope.core.persistence.api.entity.user.User;
-import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.core.provisioning.api.pushpull.PushActions;
 import org.apache.syncope.core.persistence.api.entity.Any;
 import org.apache.syncope.core.persistence.api.entity.Entity;
@@ -88,13 +88,6 @@ public abstract class AbstractPushResultHandler extends AbstractSyncopeResultHan
 
     protected abstract String getName(Any<?> any);
 
-    protected void reportPropagation(final ProvisioningReport result, final PropagationReporter reporter) {
-        if (!reporter.getStatuses().isEmpty()) {
-            result.setStatus(toProvisioningReportStatus(reporter.getStatuses().get(0).getStatus()));
-            result.setMessage(reporter.getStatuses().get(0).getFailureReason());
-        }
-    }
-
     protected void update(
             final Any<?> any,
             final Boolean enable,
@@ -494,7 +487,14 @@ public abstract class AbstractPushResultHandler extends AbstractSyncopeResultHan
         }
     }
 
-    protected ResourceOperation toResourceOperation(final UnmatchingRule rule) {
+    protected static void reportPropagation(final ProvisioningReport result, final PropagationReporter reporter) {
+        if (!reporter.getStatuses().isEmpty()) {
+            result.setStatus(toProvisioningReportStatus(reporter.getStatuses().get(0).getStatus()));
+            result.setMessage(reporter.getStatuses().get(0).getFailureReason());
+        }
+    }
+
+    protected static ResourceOperation toResourceOperation(final UnmatchingRule rule) {
         switch (rule) {
             case ASSIGN:
             case PROVISION:
@@ -504,7 +504,7 @@ public abstract class AbstractPushResultHandler extends AbstractSyncopeResultHan
         }
     }
 
-    protected ResourceOperation toResourceOperation(final MatchingRule rule) {
+    protected static ResourceOperation toResourceOperation(final MatchingRule rule) {
         switch (rule) {
             case UPDATE:
                 return ResourceOperation.UPDATE;
@@ -516,7 +516,7 @@ public abstract class AbstractPushResultHandler extends AbstractSyncopeResultHan
         }
     }
 
-    protected ProvisioningReport.Status toProvisioningReportStatus(final ExecStatus status) {
+    protected static ProvisioningReport.Status toProvisioningReportStatus(final ExecStatus status) {
         switch (status) {
             case FAILURE:
                 return ProvisioningReport.Status.FAILURE;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DBPasswordPullActions.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DBPasswordPullActions.java
index fd7add0..9afbbb2 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DBPasswordPullActions.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DBPasswordPullActions.java
@@ -35,7 +35,7 @@ import org.apache.syncope.core.persistence.api.entity.ConnInstance;
 import org.apache.syncope.core.persistence.api.entity.user.User;
 import org.apache.syncope.core.provisioning.api.Connector;
 import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningProfile;
-import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.core.provisioning.api.pushpull.PullActions;
 import org.identityconnectors.framework.common.objects.SyncDelta;
 import org.quartz.JobExecutionException;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultAnyObjectPullResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultAnyObjectPullResultHandler.java
index a87fff4..14943a0 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultAnyObjectPullResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultAnyObjectPullResultHandler.java
@@ -37,7 +37,7 @@ import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
 import org.apache.syncope.core.provisioning.api.AnyObjectProvisioningManager;
 import org.apache.syncope.core.provisioning.api.ProvisioningManager;
 import org.apache.syncope.core.provisioning.api.WorkflowResult;
-import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.identityconnectors.framework.common.objects.SyncDelta;
 import org.apache.syncope.core.provisioning.api.pushpull.AnyObjectPullResultHandler;
 import org.springframework.beans.factory.annotation.Autowired;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultGroupPullResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultGroupPullResultHandler.java
index 96c765a..63ba2c0 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultGroupPullResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultGroupPullResultHandler.java
@@ -40,7 +40,7 @@ import org.apache.syncope.core.persistence.api.entity.group.Group;
 import org.apache.syncope.core.provisioning.api.GroupProvisioningManager;
 import org.apache.syncope.core.provisioning.api.ProvisioningManager;
 import org.apache.syncope.core.provisioning.api.WorkflowResult;
-import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.identityconnectors.framework.common.objects.SyncDelta;
 import org.apache.syncope.core.provisioning.api.pushpull.GroupPullResultHandler;
 import org.springframework.beans.factory.annotation.Autowired;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultRealmPullResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultRealmPullResultHandler.java
index f94a7d8..cd60747 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultRealmPullResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultRealmPullResultHandler.java
@@ -44,7 +44,7 @@ import org.apache.syncope.core.provisioning.api.PropagationByResource;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationException;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
 import org.apache.syncope.core.provisioning.api.pushpull.IgnoreProvisionException;
-import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.core.provisioning.api.pushpull.PullActions;
 import org.apache.syncope.core.provisioning.api.pushpull.RealmPullResultHandler;
 import org.apache.syncope.core.provisioning.api.pushpull.SyncopePullExecutor;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultRealmPushResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultRealmPushResultHandler.java
index a79d7e2..4d3bf03 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultRealmPushResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultRealmPushResultHandler.java
@@ -45,7 +45,7 @@ import org.apache.syncope.core.provisioning.api.event.AfterHandlingEvent;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationReporter;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
 import org.apache.syncope.core.provisioning.api.pushpull.IgnoreProvisionException;
-import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.core.provisioning.api.pushpull.PushActions;
 import org.apache.syncope.core.provisioning.api.pushpull.RealmPushResultHandler;
 import org.apache.syncope.core.provisioning.java.job.AfterHandlingJob;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultUserPullResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultUserPullResultHandler.java
index 5e79274..facde19 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultUserPullResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultUserPullResultHandler.java
@@ -18,7 +18,6 @@
  */
 package org.apache.syncope.core.provisioning.java.pushpull;
 
-import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
@@ -58,7 +57,7 @@ import org.apache.syncope.core.provisioning.api.ProvisioningManager;
 import org.apache.syncope.core.provisioning.api.UserProvisioningManager;
 import org.apache.syncope.core.provisioning.api.WorkflowResult;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationException;
-import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.core.provisioning.api.pushpull.PullActions;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.identityconnectors.framework.common.objects.SyncDelta;
@@ -368,7 +367,7 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
                         req,
                         report,
                         null,
-                        Collections.singleton(profile.getTask().getResource().getKey()),
+                        Set.of(profile.getTask().getResource().getKey()),
                         true);
                 resultStatus = Result.SUCCESS;
 
@@ -493,7 +492,7 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
                         patch,
                         report,
                         null,
-                        Collections.singleton(profile.getTask().getResource().getKey()),
+                        Set.of(profile.getTask().getResource().getKey()),
                         true);
                 resultStatus = Result.SUCCESS;
 
@@ -587,7 +586,7 @@ public class DefaultUserPullResultHandler extends AbstractPullResultHandler impl
                             req,
                             report,
                             null,
-                            Collections.singleton(profile.getTask().getResource().getKey()),
+                            Set.of(profile.getTask().getResource().getKey()),
                             true);
                     resultStatus = Result.SUCCESS;
 
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultUserPushResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultUserPushResultHandler.java
index a34bc6b..8f04fef 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultUserPushResultHandler.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/DefaultUserPushResultHandler.java
@@ -45,7 +45,7 @@ import org.apache.syncope.core.provisioning.api.WorkflowResult;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationReporter;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
 import org.apache.syncope.core.provisioning.api.pushpull.IgnoreProvisionException;
-import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.core.provisioning.api.pushpull.PushActions;
 import org.apache.syncope.core.provisioning.api.pushpull.UserPushResultHandler;
 import org.apache.syncope.core.provisioning.java.propagation.DefaultPropagationReporter;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/InboundMatcher.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/InboundMatcher.java
index a7ec5f3..3646a76 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/InboundMatcher.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/InboundMatcher.java
@@ -20,7 +20,6 @@ package org.apache.syncope.core.provisioning.java.pushpull;
 
 import java.text.ParseException;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Optional;
 import java.util.stream.Collectors;
@@ -58,7 +57,7 @@ import org.apache.syncope.core.persistence.api.entity.PlainSchema;
 import org.apache.syncope.core.persistence.api.entity.VirSchema;
 import org.apache.syncope.core.persistence.api.entity.resource.Item;
 import org.apache.syncope.core.provisioning.api.VirAttrHandler;
-import org.apache.syncope.core.provisioning.java.IntAttrNameParser;
+import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
 import org.apache.syncope.core.spring.ImplementationManager;
 import org.identityconnectors.framework.common.objects.Attribute;
@@ -212,13 +211,13 @@ public class InboundMatcher {
             List<Object> output = transformer.beforePull(
                     connObjectKeyItem,
                     null,
-                    Collections.<Object>singletonList(finalConnObjectKeyValue));
+                    List.of(finalConnObjectKeyValue));
             if (!CollectionUtils.isEmpty(output)) {
                 finalConnObjectKeyValue = output.get(0).toString();
             }
         }
 
-        List<PullMatch> noMatchResult = Collections.singletonList(PullCorrelationRule.NO_MATCH);
+        List<PullMatch> noMatchResult = List.of(PullCorrelationRule.NO_MATCH);
 
         IntAttrName intAttrName;
         try {
@@ -356,7 +355,7 @@ public class InboundMatcher {
             }
         }
 
-        List<PullMatch> result = Collections.emptyList();
+        List<PullMatch> result = List.of();
         try {
             if (rule.isPresent()) {
                 result = matchByCorrelationRule(syncDelta, provision, rule.get(), provision.getAnyType().getKind());
@@ -376,7 +375,7 @@ public class InboundMatcher {
                     }
                 }
                 if (connObjectKeyValue == null) {
-                    result = Collections.singletonList(PullCorrelationRule.NO_MATCH);
+                    result = List.of(PullCorrelationRule.NO_MATCH);
                 } else {
                     result = matchByConnObjectKeyValue(connObjectKeyItem.get(), connObjectKeyValue, provision);
                 }
@@ -412,14 +411,14 @@ public class InboundMatcher {
             }
         }
         if (connObjectKey == null) {
-            return Collections.emptyList();
+            return List.of();
         }
 
         for (ItemTransformer transformer : MappingUtils.getItemTransformers(connObjectKeyItem.get())) {
             List<Object> output = transformer.beforePull(
                     connObjectKeyItem.get(),
                     null,
-                    Collections.<Object>singletonList(connObjectKey));
+                    List.of(connObjectKey));
             if (!CollectionUtils.isEmpty(output)) {
                 connObjectKey = output.get(0).toString();
             }
@@ -459,4 +458,3 @@ public class InboundMatcher {
         return result;
     }
 }
-
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/LDAPMembershipPullActions.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/LDAPMembershipPullActions.java
index 7b09175..22550fc 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/LDAPMembershipPullActions.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/LDAPMembershipPullActions.java
@@ -30,7 +30,7 @@ import org.apache.syncope.common.lib.to.GroupTO;
 import org.apache.syncope.core.persistence.api.dao.GroupDAO;
 import org.apache.syncope.core.provisioning.api.Connector;
 import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningProfile;
-import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
 import org.apache.syncope.core.persistence.api.dao.PullMatch;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/LDAPPasswordPullActions.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/LDAPPasswordPullActions.java
index 0c7df3f..cb4703c 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/LDAPPasswordPullActions.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/LDAPPasswordPullActions.java
@@ -35,7 +35,7 @@ import org.apache.syncope.common.lib.types.CipherAlgorithm;
 import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.entity.user.User;
 import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningProfile;
-import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.core.provisioning.api.pushpull.PullActions;
 import org.identityconnectors.framework.common.objects.SyncDelta;
 import org.quartz.JobExecutionException;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/OutboundMatcher.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/OutboundMatcher.java
index 6e98595..41637c6 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/OutboundMatcher.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/OutboundMatcher.java
@@ -182,7 +182,7 @@ public class OutboundMatcher {
             LOG.error("Could not match {} with any existing {}", any, provision.getObjectClass(), e);
         }
 
-        if (result.size() == 1) {
+        if (any != null && result.size() == 1) {
             virAttrHandler.setValues(any, result.get(0));
         }
 
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PushJobDelegate.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PushJobDelegate.java
index 2638b96..726323c 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PushJobDelegate.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/PushJobDelegate.java
@@ -128,22 +128,22 @@ public class PushJobDelegate extends AbstractProvisioningJobDelegate<PushTask> {
         }
     }
 
-    protected static RealmPushResultHandler buildRealmHandler() {
+    protected RealmPushResultHandler buildRealmHandler() {
         return (RealmPushResultHandler) ApplicationContextProvider.getBeanFactory().
                 createBean(DefaultRealmPushResultHandler.class, AbstractBeanDefinition.AUTOWIRE_BY_NAME, false);
     }
 
-    protected static AnyObjectPushResultHandler buildAnyObjectHandler() {
+    protected AnyObjectPushResultHandler buildAnyObjectHandler() {
         return (AnyObjectPushResultHandler) ApplicationContextProvider.getBeanFactory().
                 createBean(DefaultAnyObjectPushResultHandler.class, AbstractBeanDefinition.AUTOWIRE_BY_NAME, false);
     }
 
-    protected static UserPushResultHandler buildUserHandler() {
+    protected UserPushResultHandler buildUserHandler() {
         return (UserPushResultHandler) ApplicationContextProvider.getBeanFactory().
                 createBean(DefaultUserPushResultHandler.class, AbstractBeanDefinition.AUTOWIRE_BY_NAME, false);
     }
 
-    protected static GroupPushResultHandler buildGroupHandler() {
+    protected GroupPushResultHandler buildGroupHandler() {
         return (GroupPushResultHandler) ApplicationContextProvider.getBeanFactory().
                 createBean(DefaultGroupPushResultHandler.class, AbstractBeanDefinition.AUTOWIRE_BY_NAME, false);
     }
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 02abde5..a31d08e 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
@@ -23,6 +23,7 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.stream.Stream;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.common.lib.to.PullTaskTO;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.lib.types.ConflictResolutionAction;
@@ -42,14 +43,13 @@ 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.pushpull.GroupPullResultHandler;
 import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningProfile;
-import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
 import org.apache.syncope.core.provisioning.api.pushpull.PullActions;
 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.spring.ImplementationManager;
 import org.apache.syncope.core.provisioning.java.utils.TemplateUtils;
+import org.apache.syncope.core.spring.ImplementationManager;
 import org.identityconnectors.framework.common.objects.AttributeBuilder;
 import org.identityconnectors.framework.common.objects.filter.Filter;
 import org.identityconnectors.framework.common.objects.filter.FilterBuilder;
@@ -160,7 +160,7 @@ public class SinglePullJobDelegate extends PullJobDelegate implements SyncopeSin
 
             connector.filteredReconciliation(
                     provision.getObjectClass(),
-                    new AccountReconciliationFilterBuilder(connObjectKey, connObjectValue),
+                    new SingleReconciliationFilterBuilder(connObjectKey, connObjectValue),
                     handler,
                     MappingUtils.buildOperationOptions(mapItems, moreAttrsToGet.toArray(new String[0])));
 
@@ -182,13 +182,13 @@ public class SinglePullJobDelegate extends PullJobDelegate implements SyncopeSin
         }
     }
 
-    static class AccountReconciliationFilterBuilder implements ReconFilterBuilder {
+    static class SingleReconciliationFilterBuilder implements ReconFilterBuilder {
 
         private final String key;
 
         private final String value;
 
-        AccountReconciliationFilterBuilder(final String key, final String value) {
+        SingleReconciliationFilterBuilder(final String key, final String value) {
             this.key = key;
             this.value = value;
         }
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/SinglePushJobDelegate.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/SinglePushJobDelegate.java
index 4889311..f0c4258 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/SinglePushJobDelegate.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/SinglePushJobDelegate.java
@@ -33,7 +33,7 @@ import org.apache.syncope.core.persistence.api.entity.task.PushTask;
 import org.apache.syncope.core.persistence.api.entity.user.LinkedAccount;
 import org.apache.syncope.core.provisioning.api.Connector;
 import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningProfile;
-import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.core.provisioning.api.pushpull.PushActions;
 import org.apache.syncope.core.provisioning.api.pushpull.SyncopePushResultHandler;
 import org.apache.syncope.core.provisioning.api.pushpull.SyncopeSinglePushExecutor;
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamAnyObjectPushResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamAnyObjectPushResultHandler.java
new file mode 100644
index 0000000..17576cb
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamAnyObjectPushResultHandler.java
@@ -0,0 +1,73 @@
+/*
+ * 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.java.pushpull.stream;
+
+import java.util.Set;
+import java.util.stream.Stream;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
+import org.apache.syncope.common.lib.types.ResourceOperation;
+import org.apache.syncope.core.persistence.api.entity.Any;
+import org.apache.syncope.core.persistence.api.entity.resource.Item;
+import org.apache.syncope.core.persistence.api.entity.resource.Provision;
+import org.apache.syncope.core.provisioning.api.DerAttrHandler;
+import org.apache.syncope.core.provisioning.api.propagation.PropagationReporter;
+import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
+import org.apache.syncope.core.provisioning.java.propagation.DefaultPropagationReporter;
+import org.apache.syncope.core.provisioning.java.pushpull.DefaultAnyObjectPushResultHandler;
+import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
+import org.identityconnectors.framework.common.objects.Attribute;
+import org.springframework.beans.factory.annotation.Autowired;
+
+public class StreamAnyObjectPushResultHandler extends DefaultAnyObjectPushResultHandler {
+
+    @Autowired
+    private DerAttrHandler derAttrHandler;
+
+    private String executor;
+
+    public void setExecutor(final String executor) {
+        this.executor = executor;
+    }
+
+    @Override
+    protected void provision(final Any<?> any, final Boolean enabled, final ProvisioningReport result) {
+        Provision provision = profile.getTask().getResource().getProvisions().get(0);
+
+        Stream<? extends Item> items = MappingUtils.getPropagationItems(provision.getMapping().getItems().stream());
+
+        Pair<String, Set<Attribute>> preparedAttrs = mappingManager.prepareAttrs(any, null, false, enabled, provision);
+
+        PropagationTaskInfo propagationTask = propagationManager.newTask(
+                derAttrHandler,
+                any,
+                profile.getTask().getResource(),
+                ResourceOperation.CREATE,
+                provision,
+                false,
+                items,
+                preparedAttrs);
+        propagationTask.setConnector(profile.getConnector());
+        LOG.debug("PropagationTask created: {}", propagationTask);
+
+        PropagationReporter reporter = new DefaultPropagationReporter();
+        taskExecutor.execute(propagationTask, reporter, executor);
+        reportPropagation(result, reporter);
+    }
+}
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamGroupPushResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamGroupPushResultHandler.java
new file mode 100644
index 0000000..32e6710
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamGroupPushResultHandler.java
@@ -0,0 +1,73 @@
+/*
+ * 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.java.pushpull.stream;
+
+import java.util.Set;
+import java.util.stream.Stream;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
+import org.apache.syncope.common.lib.types.ResourceOperation;
+import org.apache.syncope.core.persistence.api.entity.Any;
+import org.apache.syncope.core.persistence.api.entity.resource.Item;
+import org.apache.syncope.core.persistence.api.entity.resource.Provision;
+import org.apache.syncope.core.provisioning.api.DerAttrHandler;
+import org.apache.syncope.core.provisioning.api.propagation.PropagationReporter;
+import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
+import org.apache.syncope.core.provisioning.java.propagation.DefaultPropagationReporter;
+import org.apache.syncope.core.provisioning.java.pushpull.DefaultGroupPushResultHandler;
+import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
+import org.identityconnectors.framework.common.objects.Attribute;
+import org.springframework.beans.factory.annotation.Autowired;
+
+public class StreamGroupPushResultHandler extends DefaultGroupPushResultHandler {
+
+    @Autowired
+    private DerAttrHandler derAttrHandler;
+
+    private String executor;
+
+    public void setExecutor(final String executor) {
+        this.executor = executor;
+    }
+
+    @Override
+    protected void provision(final Any<?> any, final Boolean enabled, final ProvisioningReport result) {
+        Provision provision = profile.getTask().getResource().getProvisions().get(0);
+
+        Stream<? extends Item> items = MappingUtils.getPropagationItems(provision.getMapping().getItems().stream());
+
+        Pair<String, Set<Attribute>> preparedAttrs = mappingManager.prepareAttrs(any, null, false, enabled, provision);
+
+        PropagationTaskInfo propagationTask = propagationManager.newTask(
+                derAttrHandler,
+                any,
+                profile.getTask().getResource(),
+                ResourceOperation.CREATE,
+                provision,
+                false,
+                items,
+                preparedAttrs);
+        propagationTask.setConnector(profile.getConnector());
+        LOG.debug("PropagationTask created: {}", propagationTask);
+
+        PropagationReporter reporter = new DefaultPropagationReporter();
+        taskExecutor.execute(propagationTask, reporter, executor);
+        reportPropagation(result, reporter);
+    }
+}
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamPullJobDelegate.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamPullJobDelegate.java
new file mode 100644
index 0000000..ff6553e
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamPullJobDelegate.java
@@ -0,0 +1,264 @@
+/*
+ * 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.java.pushpull.stream;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+import org.apache.syncope.common.lib.to.PullTaskTO;
+import org.apache.syncope.common.lib.types.ConflictResolutionAction;
+import org.apache.syncope.common.lib.types.MappingPurpose;
+import org.apache.syncope.common.lib.types.PullMode;
+import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
+import org.apache.syncope.core.persistence.api.dao.PlainSchemaDAO;
+import org.apache.syncope.core.persistence.api.dao.RealmDAO;
+import org.apache.syncope.core.persistence.api.entity.AnyType;
+import org.apache.syncope.core.persistence.api.entity.AnyUtils;
+import org.apache.syncope.core.persistence.api.entity.Implementation;
+import org.apache.syncope.core.persistence.api.entity.PlainSchema;
+import org.apache.syncope.core.persistence.api.entity.VirSchema;
+import org.apache.syncope.core.persistence.api.entity.policy.PullCorrelationRuleEntity;
+import org.apache.syncope.core.persistence.api.entity.policy.PullPolicy;
+import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
+import org.apache.syncope.core.persistence.api.entity.resource.Item;
+import org.apache.syncope.core.persistence.api.entity.resource.Mapping;
+import org.apache.syncope.core.persistence.api.entity.resource.MappingItem;
+import org.apache.syncope.core.persistence.api.entity.resource.Provision;
+import org.apache.syncope.core.persistence.api.entity.task.PullTask;
+import org.apache.syncope.core.provisioning.api.pushpull.GroupPullResultHandler;
+import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningProfile;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
+import org.apache.syncope.common.lib.types.IdMImplementationType;
+import org.apache.syncope.core.provisioning.api.pushpull.PullActions;
+import org.apache.syncope.core.provisioning.api.pushpull.stream.StreamConnector;
+import org.apache.syncope.core.provisioning.api.pushpull.SyncopePullResultHandler;
+import org.apache.syncope.core.spring.ImplementationManager;
+import org.quartz.JobExecutionException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.apache.syncope.core.provisioning.api.pushpull.stream.SyncopeStreamPullExecutor;
+import org.apache.syncope.core.provisioning.java.pushpull.PullJobDelegate;
+import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
+import org.apache.syncope.core.spring.security.SecureRandomUtils;
+import org.identityconnectors.framework.common.objects.ObjectClass;
+
+@Component
+public class StreamPullJobDelegate extends PullJobDelegate implements SyncopeStreamPullExecutor {
+
+    @Autowired
+    private ImplementationDAO implementationDAO;
+
+    @Autowired
+    private RealmDAO realmDAO;
+
+    @Autowired
+    private PlainSchemaDAO plainSchemaDAO;
+
+    private PullPolicy pullPolicy(
+            final AnyType anyType,
+            final ConflictResolutionAction conflictResolutionAction,
+            final String pullCorrelationRule) {
+
+        PullCorrelationRuleEntity pullCorrelationRuleEntity = null;
+        if (pullCorrelationRule != null) {
+            Implementation impl = implementationDAO.find(pullCorrelationRule);
+            if (impl == null || !IdMImplementationType.PULL_ACTIONS.equals(impl.getType())) {
+                LOG.debug("Invalid " + Implementation.class.getSimpleName() + " {}, ignoring...", pullCorrelationRule);
+            } else {
+                pullCorrelationRuleEntity = entityFactory.newEntity(PullCorrelationRuleEntity.class);
+                pullCorrelationRuleEntity.setAnyType(anyType);
+                pullCorrelationRuleEntity.setImplementation(impl);
+            }
+        }
+
+        PullPolicy pullPolicy = entityFactory.newEntity(PullPolicy.class);
+        pullPolicy.setConflictResolutionAction(conflictResolutionAction);
+
+        if (pullCorrelationRuleEntity != null) {
+            pullPolicy.add(pullCorrelationRuleEntity);
+            pullCorrelationRuleEntity.setPullPolicy(pullPolicy);
+        }
+
+        return pullPolicy;
+    }
+
+    private Provision provision(
+            final AnyType anyType,
+            final String keyColumn,
+            final List<String> columns) throws JobExecutionException {
+
+        Provision provision = entityFactory.newEntity(Provision.class);
+        provision.setAnyType(anyType);
+        provision.setObjectClass(new ObjectClass(anyType.getKey()));
+
+        Mapping mapping = entityFactory.newEntity(Mapping.class);
+        provision.setMapping(mapping);
+        mapping.setProvision(provision);
+
+        AnyUtils anyUtils = anyUtilsFactory.getInstance(anyType.getKind());
+        if (anyUtils.getField(keyColumn) == null) {
+            PlainSchema keyColumnSchema = plainSchemaDAO.find(keyColumn);
+            if (keyColumnSchema == null) {
+                throw new JobExecutionException("Plain Schema for key column not found: " + keyColumn);
+            }
+        }
+
+        MappingItem connObjectKeyItem = entityFactory.newEntity(MappingItem.class);
+        connObjectKeyItem.setExtAttrName(keyColumn);
+        connObjectKeyItem.setIntAttrName(keyColumn);
+        connObjectKeyItem.setPurpose(MappingPurpose.PULL);
+        mapping.setConnObjectKeyItem(connObjectKeyItem);
+
+        columns.stream().
+                filter(column -> anyUtils.getField(column) != null
+                || plainSchemaDAO.find(column) != null || virSchemaDAO.find(column) != null).
+                map(column -> {
+                    MappingItem item = entityFactory.newEntity(MappingItem.class);
+                    item.setExtAttrName(column);
+                    item.setIntAttrName(column);
+                    item.setPurpose(MappingPurpose.PULL);
+                    mapping.add(item);
+                    return item;
+                }).forEach(mapping::add);
+
+        return provision;
+    }
+
+    private ExternalResource externalResource(
+            final AnyType anyType,
+            final String keyColumn,
+            final List<String> columns,
+            final ConflictResolutionAction conflictResolutionAction,
+            final String pullCorrelationRule) throws JobExecutionException {
+
+        Provision provision = provision(anyType, keyColumn, columns);
+
+        ExternalResource resource = entityFactory.newEntity(ExternalResource.class);
+        resource.setKey("StreamPull_" + SecureRandomUtils.generateRandomUUID().toString());
+        resource.add(provision);
+        provision.setResource(resource);
+
+        resource.setPullPolicy(pullPolicy(anyType, conflictResolutionAction, pullCorrelationRule));
+
+        return resource;
+    }
+
+    @Override
+    public List<ProvisioningReport> pull(
+            final AnyType anyType,
+            final String keyColumn,
+            final List<String> columns,
+            final ConflictResolutionAction conflictResolutionAction,
+            final String pullCorrelationRule,
+            final StreamConnector connector,
+            final PullTaskTO pullTaskTO) throws JobExecutionException {
+
+        LOG.debug("Executing stream pull");
+
+        List<PullActions> actions = new ArrayList<>();
+        pullTaskTO.getActions().forEach(key -> {
+            Implementation impl = implementationDAO.find(key);
+            if (impl == null || !IdMImplementationType.PULL_ACTIONS.equals(impl.getType())) {
+                LOG.debug("Invalid " + Implementation.class.getSimpleName() + " {}, ignoring...", key);
+            } else {
+                try {
+                    actions.add(ImplementationManager.build(impl));
+                } catch (Exception e) {
+                    LOG.warn("While building {}", impl, e);
+                }
+            }
+        });
+
+        try {
+            ExternalResource resource =
+                    externalResource(anyType, keyColumn, columns, conflictResolutionAction, pullCorrelationRule);
+            Provision provision = resource.getProvisions().get(0);
+
+            PullTask pullTask = entityFactory.newEntity(PullTask.class);
+            pullTask.setResource(resource);
+            pullTask.setMatchingRule(pullTaskTO.getMatchingRule());
+            pullTask.setUnmatchingRule(pullTaskTO.getUnmatchingRule());
+            pullTask.setPullMode(PullMode.FULL_RECONCILIATION);
+            pullTask.setPerformCreate(true);
+            pullTask.setPerformUpdate(true);
+            pullTask.setPerformDelete(false);
+            pullTask.setSyncStatus(false);
+            pullTask.setDestinationRealm(realmDAO.findByFullPath(pullTaskTO.getDestinationRealm()));
+            pullTask.setRemediation(pullTaskTO.isRemediation());
+
+            profile = new ProvisioningProfile<>(connector, pullTask);
+            profile.setDryRun(false);
+            profile.setConflictResolutionAction(ConflictResolutionAction.FIRSTMATCH);
+            profile.getActions().addAll(actions);
+
+            for (PullActions action : actions) {
+                action.beforeAll(profile);
+            }
+
+            SyncopePullResultHandler handler;
+            GroupPullResultHandler ghandler = buildGroupHandler();
+            switch (anyType.getKind()) {
+                case USER:
+                    handler = buildUserHandler();
+                    break;
+
+                case GROUP:
+                    handler = ghandler;
+                    break;
+
+                case ANY_OBJECT:
+                default:
+                    handler = buildAnyObjectHandler();
+            }
+            handler.setProfile(profile);
+            handler.setPullExecutor(this);
+
+            // execute filtered pull
+            Set<String> moreAttrsToGet = new HashSet<>();
+            actions.forEach(action -> moreAttrsToGet.addAll(action.moreAttrsToGet(profile, provision)));
+
+            Stream<? extends Item> mapItems = Stream.concat(
+                    MappingUtils.getPullItems(provision.getMapping().getItems().stream()),
+                    virSchemaDAO.findByProvision(provision).stream().map(VirSchema::asLinkingMappingItem));
+
+            connector.fullReconciliation(
+                    provision.getObjectClass(),
+                    handler,
+                    MappingUtils.buildOperationOptions(mapItems, moreAttrsToGet.toArray(new String[0])));
+
+            try {
+                setGroupOwners(ghandler);
+            } catch (Exception e) {
+                LOG.error("While setting group owners", e);
+            }
+
+            for (PullActions action : actions) {
+                action.afterAll(profile);
+            }
+
+            return profile.getResults();
+        } catch (Exception e) {
+            throw e instanceof JobExecutionException
+                    ? (JobExecutionException) e
+                    : new JobExecutionException("While stream pulling", e);
+        }
+    }
+}
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamPushJobDelegate.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamPushJobDelegate.java
new file mode 100644
index 0000000..1b775d4
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamPushJobDelegate.java
@@ -0,0 +1,200 @@
+/*
+ * 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.java.pushpull.stream;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
+import org.apache.syncope.common.lib.to.PushTaskTO;
+import org.apache.syncope.common.lib.types.ConflictResolutionAction;
+import org.apache.syncope.common.lib.types.IdMImplementationType;
+import org.apache.syncope.common.lib.types.MappingPurpose;
+import org.apache.syncope.core.persistence.api.dao.ImplementationDAO;
+import org.apache.syncope.core.persistence.api.entity.Any;
+import org.apache.syncope.core.persistence.api.entity.AnyType;
+import org.apache.syncope.core.persistence.api.entity.Implementation;
+import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
+import org.apache.syncope.core.persistence.api.entity.resource.Mapping;
+import org.apache.syncope.core.persistence.api.entity.resource.MappingItem;
+import org.apache.syncope.core.persistence.api.entity.resource.Provision;
+import org.apache.syncope.core.persistence.api.entity.task.PushTask;
+import org.apache.syncope.core.provisioning.api.pushpull.AnyObjectPushResultHandler;
+import org.apache.syncope.core.provisioning.api.pushpull.GroupPushResultHandler;
+import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningProfile;
+import org.apache.syncope.core.provisioning.api.pushpull.PushActions;
+import org.apache.syncope.core.provisioning.api.pushpull.stream.StreamConnector;
+import org.apache.syncope.core.provisioning.api.pushpull.SyncopePushResultHandler;
+import org.apache.syncope.core.provisioning.api.pushpull.stream.SyncopeStreamPushExecutor;
+import org.apache.syncope.core.provisioning.api.pushpull.UserPushResultHandler;
+import org.apache.syncope.core.provisioning.java.pushpull.PushJobDelegate;
+import org.apache.syncope.core.spring.ApplicationContextProvider;
+import org.apache.syncope.core.spring.ImplementationManager;
+import org.apache.syncope.core.spring.security.SecureRandomUtils;
+import org.identityconnectors.framework.common.objects.ObjectClass;
+import org.quartz.JobExecutionException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.support.AbstractBeanDefinition;
+import org.springframework.stereotype.Component;
+
+@Component
+public class StreamPushJobDelegate extends PushJobDelegate implements SyncopeStreamPushExecutor {
+
+    @Autowired
+    private ImplementationDAO implementationDAO;
+
+    private String executor;
+
+    @Override
+    protected AnyObjectPushResultHandler buildAnyObjectHandler() {
+        StreamAnyObjectPushResultHandler handler =
+                (StreamAnyObjectPushResultHandler) ApplicationContextProvider.getBeanFactory().createBean(
+                        StreamAnyObjectPushResultHandler.class, AbstractBeanDefinition.AUTOWIRE_BY_NAME, false);
+        handler.setExecutor(executor);
+        return handler;
+    }
+
+    @Override
+    protected UserPushResultHandler buildUserHandler() {
+        StreamUserPushResultHandler handler =
+                (StreamUserPushResultHandler) ApplicationContextProvider.getBeanFactory().
+                        createBean(StreamUserPushResultHandler.class, AbstractBeanDefinition.AUTOWIRE_BY_NAME, false);
+        handler.setExecutor(executor);
+        return handler;
+    }
+
+    @Override
+    protected GroupPushResultHandler buildGroupHandler() {
+        StreamGroupPushResultHandler handler = (StreamGroupPushResultHandler) ApplicationContextProvider.
+                getBeanFactory().
+                createBean(StreamGroupPushResultHandler.class, AbstractBeanDefinition.AUTOWIRE_BY_NAME, false);
+        handler.setExecutor(executor);
+        return handler;
+    }
+
+    private ExternalResource externalResource(
+            final AnyType anyType, final List<String> columns) throws JobExecutionException {
+
+        Provision provision = entityFactory.newEntity(Provision.class);
+        provision.setAnyType(anyType);
+        provision.setObjectClass(new ObjectClass(anyType.getKey()));
+
+        Mapping mapping = entityFactory.newEntity(Mapping.class);
+        provision.setMapping(mapping);
+        mapping.setProvision(provision);
+
+        MappingItem connObjectKeyItem = entityFactory.newEntity(MappingItem.class);
+        connObjectKeyItem.setExtAttrName("key");
+        connObjectKeyItem.setIntAttrName("key");
+        connObjectKeyItem.setPurpose(MappingPurpose.NONE);
+        mapping.setConnObjectKeyItem(connObjectKeyItem);
+
+        columns.stream().map(column -> {
+            MappingItem item = entityFactory.newEntity(MappingItem.class);
+            item.setExtAttrName(column);
+            item.setIntAttrName(column);
+            item.setPurpose(MappingPurpose.PROPAGATION);
+            mapping.add(item);
+            return item;
+        }).forEach(mapping::add);
+
+        ExternalResource resource = entityFactory.newEntity(ExternalResource.class);
+        resource.setKey("StreamPush_" + SecureRandomUtils.generateRandomUUID().toString());
+        resource.add(provision);
+        provision.setResource(resource);
+
+        return resource;
+    }
+
+    @Override
+    public List<ProvisioningReport> push(
+            final AnyType anyType,
+            final List<? extends Any<?>> anys,
+            final List<String> columns,
+            final StreamConnector connector,
+            final PushTaskTO pushTaskTO,
+            final String executor) throws JobExecutionException {
+
+        LOG.debug("Executing stream push as {}", executor);
+        this.executor = executor;
+
+        List<PushActions> actions = new ArrayList<>();
+        pushTaskTO.getActions().forEach(key -> {
+            Implementation impl = implementationDAO.find(key);
+            if (impl == null || !IdMImplementationType.PUSH_ACTIONS.equals(impl.getType())) {
+                LOG.debug("Invalid " + Implementation.class.getSimpleName() + " {}, ignoring...", key);
+            } else {
+                try {
+                    actions.add(ImplementationManager.build(impl));
+                } catch (Exception e) {
+                    LOG.warn("While building {}", impl, e);
+                }
+            }
+        });
+
+        try {
+            ExternalResource resource = externalResource(anyType, columns);
+            Provision provision = resource.getProvisions().get(0);
+
+            PushTask pushTask = entityFactory.newEntity(PushTask.class);
+            pushTask.setResource(resource);
+            pushTask.setMatchingRule(pushTaskTO.getMatchingRule());
+            pushTask.setUnmatchingRule(pushTaskTO.getUnmatchingRule());
+            pushTask.setPerformCreate(true);
+            pushTask.setPerformUpdate(true);
+            pushTask.setPerformDelete(true);
+            pushTask.setSyncStatus(false);
+
+            profile = new ProvisioningProfile<>(connector, pushTask);
+            profile.getActions().addAll(actions);
+            profile.setConflictResolutionAction(ConflictResolutionAction.FIRSTMATCH);
+
+            for (PushActions action : actions) {
+                action.beforeAll(profile);
+            }
+
+            SyncopePushResultHandler handler;
+            switch (provision.getAnyType().getKind()) {
+                case USER:
+                    handler = buildUserHandler();
+                    break;
+
+                case GROUP:
+                    handler = buildGroupHandler();
+                    break;
+
+                case ANY_OBJECT:
+                default:
+                    handler = buildAnyObjectHandler();
+            }
+            handler.setProfile(profile);
+
+            doHandle(anys, handler, provision.getResource());
+
+            for (PushActions action : actions) {
+                action.afterAll(profile);
+            }
+
+            return profile.getResults();
+        } catch (Exception e) {
+            throw e instanceof JobExecutionException
+                    ? (JobExecutionException) e
+                    : new JobExecutionException("While stream pushing", e);
+        }
+    }
+}
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamUserPushResultHandler.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamUserPushResultHandler.java
new file mode 100644
index 0000000..9b788a0
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamUserPushResultHandler.java
@@ -0,0 +1,73 @@
+/*
+ * 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.java.pushpull.stream;
+
+import java.util.Set;
+import java.util.stream.Stream;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
+import org.apache.syncope.common.lib.types.ResourceOperation;
+import org.apache.syncope.core.persistence.api.entity.Any;
+import org.apache.syncope.core.persistence.api.entity.resource.Item;
+import org.apache.syncope.core.persistence.api.entity.resource.Provision;
+import org.apache.syncope.core.provisioning.api.DerAttrHandler;
+import org.apache.syncope.core.provisioning.api.propagation.PropagationReporter;
+import org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
+import org.apache.syncope.core.provisioning.java.propagation.DefaultPropagationReporter;
+import org.apache.syncope.core.provisioning.java.pushpull.DefaultUserPushResultHandler;
+import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
+import org.identityconnectors.framework.common.objects.Attribute;
+import org.springframework.beans.factory.annotation.Autowired;
+
+public class StreamUserPushResultHandler extends DefaultUserPushResultHandler {
+
+    @Autowired
+    private DerAttrHandler derAttrHandler;
+
+    private String executor;
+
+    public void setExecutor(final String executor) {
+        this.executor = executor;
+    }
+
+    @Override
+    protected void provision(final Any<?> any, final Boolean enabled, final ProvisioningReport result) {
+        Provision provision = profile.getTask().getResource().getProvisions().get(0);
+
+        Stream<? extends Item> items = MappingUtils.getPropagationItems(provision.getMapping().getItems().stream());
+
+        Pair<String, Set<Attribute>> preparedAttrs = mappingManager.prepareAttrs(any, null, false, enabled, provision);
+
+        PropagationTaskInfo propagationTask = propagationManager.newTask(
+                derAttrHandler,
+                any,
+                profile.getTask().getResource(),
+                ResourceOperation.CREATE,
+                provision,
+                false,
+                items,
+                preparedAttrs);
+        propagationTask.setConnector(profile.getConnector());
+        LOG.debug("PropagationTask created: {}", propagationTask);
+
+        PropagationReporter reporter = new DefaultPropagationReporter();
+        taskExecutor.execute(propagationTask, reporter, executor);
+        reportPropagation(result, reporter);
+    }
+}
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/MappingUtils.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/MappingUtils.java
index cdb8280..1f52585 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/MappingUtils.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/MappingUtils.java
@@ -25,20 +25,14 @@ import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Stream;
-import org.apache.commons.jexl3.JexlContext;
-import org.apache.commons.jexl3.MapContext;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.common.lib.types.MappingPurpose;
-import org.apache.syncope.core.persistence.api.entity.Any;
-import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.persistence.api.entity.resource.Item;
 import org.apache.syncope.core.persistence.api.entity.resource.Mapping;
 import org.apache.syncope.core.persistence.api.entity.resource.MappingItem;
-import org.apache.syncope.core.persistence.api.entity.resource.OrgUnit;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
 import org.apache.syncope.core.provisioning.java.data.JEXLItemTransformerImpl;
-import org.apache.syncope.core.provisioning.java.jexl.JexlUtils;
 import org.apache.syncope.core.spring.ApplicationContextProvider;
 import org.identityconnectors.framework.common.objects.Name;
 import org.identityconnectors.framework.common.objects.OperationOptions;
@@ -77,85 +71,6 @@ public final class MappingUtils {
                 item -> item.getPurpose() == MappingPurpose.PULL || item.getPurpose() == MappingPurpose.BOTH);
     }
 
-    private static Name getName(final String evalConnObjectLink, final String connObjectKey) {
-        // If connObjectLink evaluates to an empty string, just use the provided connObjectKey as Name(),
-        // otherwise evaluated connObjectLink expression is taken as Name().
-        Name name;
-        if (StringUtils.isBlank(evalConnObjectLink)) {
-            // add connObjectKey as __NAME__ attribute ...
-            LOG.debug("Add connObjectKey [{}] as {}", connObjectKey, Name.NAME);
-            name = new Name(connObjectKey);
-        } else {
-            LOG.debug("Add connObjectLink [{}] as {}", evalConnObjectLink, Name.NAME);
-            name = new Name(evalConnObjectLink);
-
-            // connObjectKey not propagated: it will be used to set the value for __UID__ attribute
-            LOG.debug("connObjectKey will be used just as {} attribute", Uid.NAME);
-        }
-
-        return name;
-    }
-
-    /**
-     * Build __NAME__ for propagation.
-     * First look if there is a defined connObjectLink for the given resource (and in
-     * this case evaluate as JEXL); otherwise, take given connObjectKey.
-     *
-     * @param any given any object
-     * @param provision external resource
-     * @param connObjectKey connector object key
-     * @return the value to be propagated as __NAME__
-     */
-    public static Name evaluateNAME(final Any<?> any, final Provision provision, final String connObjectKey) {
-        if (StringUtils.isBlank(connObjectKey)) {
-            // LOG error but avoid to throw exception: leave it to the external resource
-            LOG.warn("Missing ConnObjectKey value for {}: ", provision.getResource());
-        }
-
-        // Evaluate connObjectKey expression
-        String connObjectLink = provision == null || provision.getMapping() == null
-                ? null
-                : provision.getMapping().getConnObjectLink();
-        String evalConnObjectLink = null;
-        if (StringUtils.isNotBlank(connObjectLink)) {
-            JexlContext jexlContext = new MapContext();
-            JexlUtils.addFieldsToContext(any, jexlContext);
-            JexlUtils.addPlainAttrsToContext(any.getPlainAttrs(), jexlContext);
-            JexlUtils.addDerAttrsToContext(any, jexlContext);
-            evalConnObjectLink = JexlUtils.evaluate(connObjectLink, jexlContext);
-        }
-
-        return getName(evalConnObjectLink, connObjectKey);
-    }
-
-    /**
-     * Build __NAME__ for propagation.
-     * First look if there is a defined connObjectLink for the given resource (and in
-     * this case evaluate as JEXL); otherwise, take given connObjectKey.
-     *
-     * @param realm given any object
-     * @param orgUnit external resource
-     * @param connObjectKey connector object key
-     * @return the value to be propagated as __NAME__
-     */
-    public static Name evaluateNAME(final Realm realm, final OrgUnit orgUnit, final String connObjectKey) {
-        if (StringUtils.isBlank(connObjectKey)) {
-            // LOG error but avoid to throw exception: leave it to the external resource
-            LOG.warn("Missing ConnObjectKey value for {}: ", orgUnit.getResource());
-        }
-
-        // Evaluate connObjectKey expression
-        String connObjectLink = Optional.ofNullable(orgUnit).map(OrgUnit::getConnObjectLink).orElse(null);
-        String evalConnObjectLink = null;
-        if (StringUtils.isNotBlank(connObjectLink)) {
-            JexlContext jexlContext = new MapContext();
-            JexlUtils.addFieldsToContext(realm, jexlContext);
-            evalConnObjectLink = JexlUtils.evaluate(connObjectLink, jexlContext);
-        }
-
-        return getName(evalConnObjectLink, connObjectKey);
-    }
-
     public static List<ItemTransformer> getItemTransformers(final Item item) {
         List<ItemTransformer> result = new ArrayList<>();
 
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/TemplateUtils.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/TemplateUtils.java
index 8dcdd81..79a0491 100644
--- a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/TemplateUtils.java
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/utils/TemplateUtils.java
@@ -34,12 +34,12 @@ import org.apache.syncope.common.lib.to.GroupTO;
 import org.apache.syncope.common.lib.to.GroupableRelatableTO;
 import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
-import org.apache.syncope.core.provisioning.java.jexl.JexlUtils;
 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.entity.AnyTemplate;
 import org.apache.syncope.core.persistence.api.entity.group.Group;
 import org.apache.syncope.core.persistence.api.entity.user.User;
+import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
diff --git a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/AbstractTest.java b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/AbstractTest.java
index b120f93..3d9f9ba 100644
--- a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/AbstractTest.java
+++ b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/AbstractTest.java
@@ -19,8 +19,13 @@
 package org.apache.syncope.core.provisioning.java;
 
 import javax.persistence.EntityManager;
+import org.apache.syncope.common.lib.types.AMEntitlement;
+import org.apache.syncope.common.lib.types.EntitlementsHolder;
+import org.apache.syncope.common.lib.types.IdMEntitlement;
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
 import org.apache.syncope.core.spring.ApplicationContextProvider;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.junit.jupiter.api.BeforeAll;
 import org.springframework.orm.jpa.EntityManagerFactoryUtils;
 import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
 
@@ -37,4 +42,11 @@ public abstract class AbstractTest {
 
         return entityManager;
     }
+
+    @BeforeAll
+    public static void init() {
+        EntitlementsHolder.getInstance().addAll(IdRepoEntitlement.values());
+        EntitlementsHolder.getInstance().addAll(IdMEntitlement.values());
+        EntitlementsHolder.getInstance().addAll(AMEntitlement.values());
+    }
 }
diff --git a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/ResourceDataBinderTest.java b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderTest.java
similarity index 98%
rename from core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/ResourceDataBinderTest.java
rename to core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderTest.java
index e2ab0da..bf71aa8 100644
--- a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/ResourceDataBinderTest.java
+++ b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/data/ResourceDataBinderTest.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.java;
+package org.apache.syncope.core.provisioning.java.data;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -40,6 +40,7 @@ import org.apache.syncope.core.persistence.api.entity.PlainSchema;
 import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
 import org.apache.syncope.core.persistence.api.entity.resource.MappingItem;
 import org.apache.syncope.core.provisioning.api.data.ResourceDataBinder;
+import org.apache.syncope.core.provisioning.java.AbstractTest;
 import org.apache.syncope.core.spring.security.SyncopeAuthenticationDetails;
 import org.apache.syncope.core.spring.security.SyncopeGrantedAuthority;
 import org.identityconnectors.framework.common.objects.ObjectClass;
diff --git a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/MailTemplateTest.java b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/jexl/MailTemplateTest.java
similarity index 96%
rename from core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/MailTemplateTest.java
rename to core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/jexl/MailTemplateTest.java
index 1efd0ea..60fb718 100644
--- a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/MailTemplateTest.java
+++ b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/jexl/MailTemplateTest.java
@@ -16,7 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.java;
+package org.apache.syncope.core.provisioning.java.jexl;
+
+import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
 
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -34,8 +36,8 @@ import org.apache.commons.lang3.SerializationUtils;
 import org.apache.syncope.common.lib.Attr;
 import org.apache.syncope.common.lib.to.MembershipTO;
 import org.apache.syncope.common.lib.to.UserTO;
-import org.apache.syncope.core.provisioning.java.jexl.JexlUtils;
 import org.apache.syncope.core.persistence.api.dao.MailTemplateDAO;
+import org.apache.syncope.core.provisioning.java.AbstractTest;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Transactional;
diff --git a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/MappingTest.java b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/jexl/MappingTest.java
similarity index 80%
rename from core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/MappingTest.java
rename to core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/jexl/MappingTest.java
index ea47539..862a866 100644
--- a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/MappingTest.java
+++ b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/jexl/MappingTest.java
@@ -16,7 +16,9 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.syncope.core.provisioning.java;
+package org.apache.syncope.core.provisioning.java.jexl;
+
+import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -31,9 +33,7 @@ import org.apache.syncope.core.persistence.api.entity.Realm;
 import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
 import org.apache.syncope.core.persistence.api.entity.resource.Provision;
 import org.apache.syncope.core.persistence.api.entity.user.User;
-import org.apache.syncope.core.provisioning.java.jexl.JexlUtils;
-import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
-import org.identityconnectors.framework.common.objects.Name;
+import org.apache.syncope.core.provisioning.java.AbstractTest;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Transactional;
@@ -66,14 +66,16 @@ public class MappingTest extends AbstractTest {
         User user = userDAO.findByUsername("rossini");
         assertNotNull(user);
 
-        Name name = MappingUtils.evaluateNAME(user, provision, user.getUsername());
-        assertEquals("uid=rossini,ou=people,o=isp", name.getNameValue());
+        JexlContext jexlContext = new MapContext();
+        JexlUtils.addFieldsToContext(user, jexlContext);
+        JexlUtils.addPlainAttrsToContext(user.getPlainAttrs(), jexlContext);
 
-        provision.getMapping().setConnObjectLink(
-                "'uid=' + username + realm.replaceAll('/', ',o=') + ',ou=people,o=isp'");
+        assertEquals(
+                "uid=rossini,ou=people,o=isp",
+                JexlUtils.evaluate(provision.getMapping().getConnObjectLink(), jexlContext));
 
-        name = MappingUtils.evaluateNAME(user, provision, user.getUsername());
-        assertEquals("uid=rossini,o=even,ou=people,o=isp", name.getNameValue());
+        String connObjectLink = "'uid=' + username + realm.replaceAll('/', ',o=') + ',ou=people,o=isp'";
+        assertEquals("uid=rossini,o=even,ou=people,o=isp", JexlUtils.evaluate(connObjectLink, jexlContext));
     }
 
     @Test
diff --git a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamPullJobDelegateTest.java b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamPullJobDelegateTest.java
new file mode 100644
index 0000000..dfa523c
--- /dev/null
+++ b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamPullJobDelegateTest.java
@@ -0,0 +1,117 @@
+/*
+ * 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.java.pushpull.stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.fasterxml.jackson.databind.MappingIterator;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.to.PullTaskTO;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.common.lib.types.ConflictResolutionAction;
+import org.apache.syncope.common.lib.types.MatchingRule;
+import org.apache.syncope.common.lib.types.ResourceOperation;
+import org.apache.syncope.common.lib.types.UnmatchingRule;
+import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.entity.user.User;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
+import org.apache.syncope.core.provisioning.api.pushpull.stream.StreamConnector;
+import org.apache.syncope.core.provisioning.api.pushpull.stream.SyncopeStreamPullExecutor;
+import org.apache.syncope.core.provisioning.java.AbstractTest;
+import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.junit.jupiter.api.Test;
+import org.quartz.JobExecutionException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+
+@Transactional("Master")
+public class StreamPullJobDelegateTest extends AbstractTest {
+
+    @Autowired
+    private SyncopeStreamPullExecutor streamPullExecutor;
+
+    @Autowired
+    private AnyTypeDAO anyTypeDAO;
+
+    @Autowired
+    private UserDAO userDAO;
+
+    @Test
+    public void pull() throws JobExecutionException, IOException {
+        PullTaskTO pullTask = new PullTaskTO();
+        pullTask.setDestinationRealm(SyncopeConstants.ROOT_REALM);
+        pullTask.setRemediation(false);
+        pullTask.setMatchingRule(MatchingRule.UPDATE);
+        pullTask.setUnmatchingRule(UnmatchingRule.PROVISION);
+
+        Map<String, String> user = new HashMap<>();
+        user.put("username", "donizetti");
+        user.put("email", "donizetti@apache.org");
+        user.put("surname", "Donizetti");
+        user.put("firstname", "Gaetano");
+        user.put("fullname", "Gaetano Donizetti");
+        user.put("userId", "donizetti@apache.org");
+        Iterator<Map<String, String>> backing = List.of(user).iterator();
+
+        @SuppressWarnings("unchecked")
+        MappingIterator<Map<String, String>> itor = mock(MappingIterator.class);
+        when(itor.hasNext()).thenAnswer(invocation -> backing.hasNext());
+        when(itor.next()).thenAnswer(invocation -> backing.next());
+
+        List<String> columns = user.keySet().stream().collect(Collectors.toList());
+
+        List<ProvisioningReport> results = AuthContextUtils.callAsAdmin(SyncopeConstants.MASTER_DOMAIN, () -> {
+            try {
+                return streamPullExecutor.pull(
+                        anyTypeDAO.findUser(),
+                        "username",
+                        columns,
+                        ConflictResolutionAction.IGNORE,
+                        null,
+                        new StreamConnector("username", null, itor, null),
+                        pullTask);
+            } catch (JobExecutionException e) {
+                throw new RuntimeException(e);
+            }
+        });
+        assertEquals(1, results.size());
+
+        assertEquals(AnyTypeKind.USER.name(), results.get(0).getAnyType());
+        assertNotNull(results.get(0).getKey());
+        assertEquals("donizetti", results.get(0).getName());
+        assertEquals("donizetti", results.get(0).getUidValue());
+        assertEquals(ResourceOperation.CREATE, results.get(0).getOperation());
+        assertEquals(ProvisioningReport.Status.SUCCESS, results.get(0).getStatus());
+
+        User donizetti = userDAO.find(results.get(0).getKey());
+        assertNotNull(donizetti);
+        assertEquals("donizetti", donizetti.getUsername());
+        assertEquals("Gaetano", donizetti.getPlainAttr("firstname").get().getValuesAsStrings().get(0));
+    }
+}
diff --git a/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamPushJobDelegateTest.java b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamPushJobDelegateTest.java
new file mode 100644
index 0000000..2fcab3c
--- /dev/null
+++ b/core/provisioning-java/src/test/java/org/apache/syncope/core/provisioning/java/pushpull/stream/StreamPushJobDelegateTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.java.pushpull.stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.fasterxml.jackson.databind.MappingIterator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SequenceWriter;
+import java.io.IOException;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
+import org.apache.syncope.common.lib.to.PushTaskTO;
+import org.apache.syncope.common.lib.types.MatchingRule;
+import org.apache.syncope.common.lib.types.UnmatchingRule;
+import org.apache.syncope.core.persistence.api.dao.AnyTypeDAO;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.provisioning.api.pushpull.stream.StreamConnector;
+import org.apache.syncope.core.provisioning.api.pushpull.stream.SyncopeStreamPushExecutor;
+import org.apache.syncope.core.provisioning.java.AbstractTest;
+import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+
+@Transactional("Master")
+public class StreamPushJobDelegateTest extends AbstractTest {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    @Autowired
+    private SyncopeStreamPushExecutor streamPushExecutor;
+
+    @Autowired
+    private AnyTypeDAO anyTypeDAO;
+
+    @Autowired
+    private UserDAO userDAO;
+
+    @Test
+    public void push() throws IOException {
+        PipedInputStream in = new PipedInputStream();
+        PipedOutputStream os = new PipedOutputStream(in);
+
+        PushTaskTO pushTask = new PushTaskTO();
+        pushTask.setMatchingRule(MatchingRule.UPDATE);
+        pushTask.setUnmatchingRule(UnmatchingRule.PROVISION);
+
+        List<ProvisioningReport> results = AuthContextUtils.callAsAdmin(SyncopeConstants.MASTER_DOMAIN, () -> {
+            try (SequenceWriter writer = MAPPER.writer().forType(Map.class).writeValues(os)) {
+                writer.init(true);
+
+                return streamPushExecutor.push(
+                        anyTypeDAO.findUser(),
+                        userDAO.findAll(1, 100),
+                        Arrays.asList("username", "firstname", "surname", "email", "status", "loginDate"),
+                        new StreamConnector(null, null, null, writer),
+                        pushTask,
+                        "user");
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            }
+        });
+        assertEquals(userDAO.count(), results.size());
+
+        MappingIterator<Map<String, String>> reader = MAPPER.readerFor(Map.class).readValues(in);
+
+        for (int i = 0; i < results.size() && reader.hasNext(); i++) {
+            Map<String, String> row = reader.next();
+
+            assertEquals(results.get(i).getName(), row.get("username"));
+            assertEquals(userDAO.findByUsername(row.get("username")).getStatus(), row.get("status"));
+
+            switch (row.get("username")) {
+                case "rossini":
+                    assertNull(row.get("email"));
+                    assertTrue(row.get("loginDate").contains(","));
+                    break;
+
+                case "verdi":
+                    assertEquals("verdi@syncope.org", row.get("email"));
+                    assertNull(row.get("loginDate"));
+                    break;
+
+                case "bellini":
+                    assertNull(row.get("email"));
+                    assertFalse(row.get("loginDate").contains(","));
+                    break;
+
+                default:
+                    break;
+            }
+        }
+    }
+}
diff --git a/ext/camel/provisioning-camel/src/main/java/org/apache/syncope/core/provisioning/camel/CamelUserProvisioningManager.java b/ext/camel/provisioning-camel/src/main/java/org/apache/syncope/core/provisioning/camel/CamelUserProvisioningManager.java
index ef0b317..4b7f46e 100644
--- a/ext/camel/provisioning-camel/src/main/java/org/apache/syncope/core/provisioning/camel/CamelUserProvisioningManager.java
+++ b/ext/camel/provisioning-camel/src/main/java/org/apache/syncope/core/provisioning/camel/CamelUserProvisioningManager.java
@@ -31,10 +31,10 @@ import org.apache.syncope.common.lib.request.StatusR;
 import org.apache.syncope.common.lib.request.UserCR;
 import org.apache.syncope.common.lib.request.UserUR;
 import org.apache.syncope.common.lib.to.PropagationStatus;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.core.provisioning.api.PropagationByResource;
 import org.apache.syncope.core.provisioning.api.UserProvisioningManager;
 import org.apache.syncope.core.provisioning.api.UserWorkflowResult;
-import org.apache.syncope.core.provisioning.api.pushpull.ProvisioningReport;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.transaction.annotation.Propagation;
diff --git a/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCUserManager.java b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCUserManager.java
index 32d3395..587dfa5 100644
--- a/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCUserManager.java
+++ b/ext/oidcclient/logic/src/main/java/org/apache/syncope/core/logic/oidc/OIDCUserManager.java
@@ -40,11 +40,11 @@ import org.apache.syncope.core.persistence.api.entity.OIDCProvider;
 import org.apache.syncope.core.persistence.api.entity.OIDCProviderItem;
 import org.apache.syncope.core.persistence.api.entity.user.User;
 import org.apache.syncope.core.provisioning.api.IntAttrName;
+import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.api.OIDCProviderActions;
 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.java.IntAttrNameParser;
 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;
diff --git a/ext/oidcclient/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/OIDCProviderDataBinderImpl.java b/ext/oidcclient/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/OIDCProviderDataBinderImpl.java
index 7fd536e..a206cf9 100644
--- a/ext/oidcclient/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/OIDCProviderDataBinderImpl.java
+++ b/ext/oidcclient/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/OIDCProviderDataBinderImpl.java
@@ -41,8 +41,8 @@ import org.apache.syncope.core.persistence.api.entity.OIDCProviderItem;
 import org.apache.syncope.core.persistence.api.entity.OIDCUserTemplate;
 import org.apache.syncope.core.provisioning.api.IntAttrName;
 import org.apache.syncope.core.provisioning.api.data.OIDCProviderDataBinder;
-import org.apache.syncope.core.provisioning.java.IntAttrNameParser;
-import org.apache.syncope.core.provisioning.java.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
diff --git a/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2UserManager.java b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2UserManager.java
index 1d33f34..1e7c335 100644
--- a/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2UserManager.java
+++ b/ext/saml2sp/logic/src/main/java/org/apache/syncope/core/logic/saml2/SAML2UserManager.java
@@ -42,11 +42,11 @@ import org.apache.syncope.core.persistence.api.dao.UserDAO;
 import org.apache.syncope.core.persistence.api.entity.SAML2IdP;
 import org.apache.syncope.core.persistence.api.entity.user.User;
 import org.apache.syncope.core.provisioning.api.IntAttrName;
+import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
 import org.apache.syncope.core.provisioning.api.SAML2IdPActions;
 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.java.IntAttrNameParser;
 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;
diff --git a/ext/saml2sp/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2IdPDataBinderImpl.java b/ext/saml2sp/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2IdPDataBinderImpl.java
index 87b765c..992a8ce 100644
--- a/ext/saml2sp/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2IdPDataBinderImpl.java
+++ b/ext/saml2sp/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/data/SAML2IdPDataBinderImpl.java
@@ -42,8 +42,8 @@ import org.apache.syncope.core.persistence.api.entity.SAML2IdPItem;
 import org.apache.syncope.core.persistence.api.entity.SAML2UserTemplate;
 import org.apache.syncope.core.provisioning.api.IntAttrName;
 import org.apache.syncope.core.provisioning.api.data.SAML2IdPDataBinder;
-import org.apache.syncope.core.provisioning.java.IntAttrNameParser;
-import org.apache.syncope.core.provisioning.java.jexl.JexlUtils;
+import org.apache.syncope.core.provisioning.api.IntAttrNameParser;
+import org.apache.syncope.core.provisioning.api.jexl.JexlUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
diff --git a/fit/core-reference/pom.xml b/fit/core-reference/pom.xml
index c327f43..a7b532e 100644
--- a/fit/core-reference/pom.xml
+++ b/fit/core-reference/pom.xml
@@ -427,6 +427,12 @@ under the License.
         </includes>
       </testResource>
       <testResource>
+        <directory>${basedir}/../../core/idm/logic/src/test/resources</directory>
+        <includes>
+          <include>test1.csv</include>
+        </includes>
+      </testResource>
+      <testResource>
         <directory>${basedir}/../../core/rest-cxf/src/main/resources</directory>
         <includes>
           <include>errorMessages.properties</include>
@@ -532,6 +538,17 @@ under the License.
       <build>
         <plugins>
           <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <inherited>true</inherited>
+            <configuration>
+              <includes>
+                <include>**/org/apache/syncope/fit/core/*ITCase.java</include>
+              </includes>
+            </configuration>
+          </plugin>
+
+          <plugin>
             <groupId>io.fabric8</groupId>
             <artifactId>docker-maven-plugin</artifactId>
             <configuration>
@@ -644,6 +661,17 @@ under the License.
 
           <plugin>
             <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <inherited>true</inherited>
+            <configuration>
+              <includes>
+                <include>**/org/apache/syncope/fit/core/*ITCase.java</include>
+              </includes>
+            </configuration>
+          </plugin>
+
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
             <artifactId>maven-antrun-plugin</artifactId>
             <inherited>true</inherited>
             <executions>
@@ -769,6 +797,17 @@ under the License.
 
         <plugins>
           <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <inherited>true</inherited>
+            <configuration>
+              <includes>
+                <include>**/org/apache/syncope/fit/core/*ITCase.java</include>
+              </includes>
+            </configuration>
+          </plugin>
+
+          <plugin>
             <groupId>io.fabric8</groupId>
             <artifactId>docker-maven-plugin</artifactId>
             <configuration>
@@ -882,6 +921,17 @@ under the License.
 
           <plugin>
             <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <inherited>true</inherited>
+            <configuration>
+              <includes>
+                <include>**/org/apache/syncope/fit/core/*ITCase.java</include>
+              </includes>
+            </configuration>
+          </plugin>
+
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
             <artifactId>maven-antrun-plugin</artifactId>
             <inherited>true</inherited>
             <executions>
@@ -1025,6 +1075,17 @@ under the License.
 
         <plugins>
           <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <inherited>true</inherited>
+            <configuration>
+              <includes>
+                <include>**/org/apache/syncope/fit/core/*ITCase.java</include>
+              </includes>
+            </configuration>
+          </plugin>
+
+          <plugin>
             <groupId>io.fabric8</groupId>
             <artifactId>docker-maven-plugin</artifactId>
             <configuration>
@@ -1114,6 +1175,17 @@ under the License.
 
         <plugins>
           <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <inherited>true</inherited>
+            <configuration>
+              <includes>
+                <include>**/org/apache/syncope/fit/core/*ITCase.java</include>
+              </includes>
+            </configuration>
+          </plugin>
+
+          <plugin>
             <groupId>io.fabric8</groupId>
             <artifactId>docker-maven-plugin</artifactId>
             <configuration>
@@ -1203,6 +1275,17 @@ under the License.
 
         <plugins>
           <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <inherited>true</inherited>
+            <configuration>
+              <includes>
+                <include>**/org/apache/syncope/fit/core/*ITCase.java</include>
+              </includes>
+            </configuration>
+          </plugin>
+
+          <plugin>
             <groupId>io.fabric8</groupId>
             <artifactId>docker-maven-plugin</artifactId>
             <configuration>
@@ -1299,6 +1382,17 @@ under the License.
 
         <plugins>
           <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <inherited>true</inherited>
+            <configuration>
+              <includes>
+                <include>**/org/apache/syncope/fit/core/*ITCase.java</include>
+              </includes>
+            </configuration>
+          </plugin>
+
+          <plugin>
             <groupId>io.fabric8</groupId>
             <artifactId>docker-maven-plugin</artifactId>
             <configuration>
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PullTaskITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PullTaskITCase.java
index 4405fcc..3a3e537 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PullTaskITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/PullTaskITCase.java
@@ -69,6 +69,7 @@ import org.apache.syncope.common.lib.to.ItemTO;
 import org.apache.syncope.common.lib.to.PullTaskTO;
 import org.apache.syncope.common.lib.to.ExecTO;
 import org.apache.syncope.common.lib.to.ImplementationTO;
+import org.apache.syncope.common.lib.to.PropagationTaskTO;
 import org.apache.syncope.common.lib.to.ProvisioningResult;
 import org.apache.syncope.common.lib.to.RemediationTO;
 import org.apache.syncope.common.lib.to.UserTO;
@@ -1036,15 +1037,21 @@ public class PullTaskITCase extends AbstractTaskITCase {
         // exec task: one user from CSV will match the user created above and template will be applied
         execProvisioningTask(taskService, TaskType.PULL, task.getKey(), MAX_WAIT_SECONDS, false);
 
-        // check that template was successfully applied...
-        userTO = userService.read(userTO.getKey());
-        assertEquals("virtualvalue", userTO.getVirAttr("virtualdata").get().getValues().get(0));
+        // check that template was successfully applied
+        // 1. propagation to db
+        PagedResult<PropagationTaskTO> tasks = taskService.search(new TaskQuery.Builder(TaskType.PROPAGATION).
+                anyTypeKind(AnyTypeKind.USER).entityKey(userTO.getKey()).resource(RESOURCE_NAME_DBVIRATTR).build());
+        assertFalse(tasks.getResult().isEmpty());
+        assertEquals(ExecStatus.SUCCESS.name(), tasks.getResult().get(0).getLatestExecStatus());
 
-        // ...and that propagation to db succeeded
         JdbcTemplate jdbcTemplate = new JdbcTemplate(testDataSource);
         String value = queryForObject(jdbcTemplate,
                 MAX_WAIT_SECONDS, "SELECT USERNAME FROM testpull WHERE ID=?", String.class, userTO.getKey());
         assertEquals("virtualvalue", value);
+
+        // 2. virtual attribute
+        userTO = userService.read(userTO.getKey());
+        assertEquals("virtualvalue", userTO.getVirAttr("virtualdata").get().getValues().get(0));
     }
 
     @Test
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ReconciliationITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ReconciliationITCase.java
index 1635f02..822bb5f 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ReconciliationITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/ReconciliationITCase.java
@@ -19,24 +19,46 @@
 package org.apache.syncope.fit.core;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
+import com.fasterxml.jackson.databind.MappingIterator;
+import com.fasterxml.jackson.dataformat.csv.CsvMapper;
+import com.fasterxml.jackson.dataformat.csv.CsvSchema;
+import java.io.IOException;
+import java.io.InputStream;
 import java.util.Date;
 import org.apache.syncope.common.lib.request.AnyObjectCR;
+import org.apache.syncope.common.lib.Attr;
+import java.util.List;
+import java.util.Map;
 import java.util.UUID;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import org.apache.cxf.jaxrs.client.Client;
+import org.apache.cxf.jaxrs.client.WebClient;
+import org.apache.syncope.client.lib.SyncopeClient;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.to.AnyObjectTO;
-import org.apache.syncope.common.lib.Attr;
+import org.apache.syncope.common.lib.to.PagedResult;
+import org.apache.syncope.common.lib.to.ProvisioningReport;
 import org.apache.syncope.common.lib.to.PullTaskTO;
 import org.apache.syncope.common.lib.to.PushTaskTO;
 import org.apache.syncope.common.lib.to.ReconStatus;
+import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.types.AnyTypeKind;
 import org.apache.syncope.common.lib.types.MatchType;
+import org.apache.syncope.common.lib.types.ResourceOperation;
 import org.apache.syncope.common.lib.types.UnmatchingRule;
+import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.beans.AnyQuery;
+import org.apache.syncope.common.rest.api.beans.CSVPullSpec;
+import org.apache.syncope.common.rest.api.beans.CSVPushSpec;
 import org.apache.syncope.common.rest.api.beans.ReconQuery;
+import org.apache.syncope.common.rest.api.service.ReconciliationService;
 import org.apache.syncope.fit.AbstractITCase;
 import org.identityconnectors.framework.common.objects.OperationalAttributes;
 import org.identityconnectors.framework.common.objects.Uid;
@@ -168,4 +190,106 @@ public class ReconciliationITCase extends AbstractITCase {
         AnyObjectTO printer = anyObjectService.read(externalName);
         assertNotNull(printer);
     }
+
+    @Test
+    public void importCSV() {
+        ReconciliationService service = adminClient.getService(ReconciliationService.class);
+        Client client = WebClient.client(service);
+        client.type(RESTHeaders.TEXT_CSV);
+
+        CSVPullSpec spec = new CSVPullSpec.Builder(AnyTypeKind.USER.name(), "username").build();
+        InputStream csv = getClass().getResourceAsStream("/test1.csv");
+
+        List<ProvisioningReport> results = service.pull(spec, csv);
+        assertEquals(AnyTypeKind.USER.name(), results.get(0).getAnyType());
+        assertNotNull(results.get(0).getKey());
+        assertEquals("donizetti", results.get(0).getName());
+        assertEquals("donizetti", results.get(0).getUidValue());
+        assertEquals(ResourceOperation.CREATE, results.get(0).getOperation());
+        assertEquals(ProvisioningReport.Status.SUCCESS, results.get(0).getStatus());
+
+        UserTO donizetti = userService.read(results.get(0).getKey());
+        assertNotNull(donizetti);
+        assertEquals("Gaetano", donizetti.getPlainAttr("firstname").get().getValues().get(0));
+        assertEquals(1, donizetti.getPlainAttr("loginDate").get().getValues().size());
+
+        UserTO cimarosa = userService.read(results.get(1).getKey());
+        assertNotNull(cimarosa);
+        assertEquals("Domenico Cimarosa", cimarosa.getPlainAttr("fullname").get().getValues().get(0));
+        assertEquals(2, cimarosa.getPlainAttr("loginDate").get().getValues().size());
+    }
+
+    @Test
+    public void exportCSV() throws IOException {
+        ReconciliationService service = adminClient.getService(ReconciliationService.class);
+        Client client = WebClient.client(service);
+        client.accept(RESTHeaders.TEXT_CSV);
+
+        AnyQuery anyQuery = new AnyQuery.Builder().realm(SyncopeConstants.ROOT_REALM).
+                fiql(SyncopeClient.getUserSearchConditionBuilder().is("username").equalTo("*ini").query()).
+                page(1).
+                size(1000).
+                orderBy("username ASC").
+                build();
+
+        CSVPushSpec spec = new CSVPushSpec.Builder(AnyTypeKind.USER.name()).
+                ignorePagination(true).
+                field("username").
+                field("status").
+                plainAttr("firstname").
+                plainAttr("surname").
+                plainAttr("email").
+                plainAttr("loginDate").
+                build();
+
+        Response response = service.push(anyQuery, spec);
+        assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
+        assertEquals(
+                "attachment; filename=" + SyncopeConstants.MASTER_DOMAIN + ".csv",
+                response.getHeaderString(HttpHeaders.CONTENT_DISPOSITION));
+
+        PagedResult<UserTO> users = userService.search(anyQuery);
+        assertNotNull(users);
+
+        CsvSchema.Builder builder = CsvSchema.builder().setUseHeader(true);
+        builder.addColumn("username");
+        builder.addColumn("status");
+        builder.addColumn("firstname");
+        builder.addColumn("surname");
+        builder.addColumn("email");
+        builder.addColumn("loginDate");
+        CsvSchema schema = builder.build();
+
+        MappingIterator<Map<String, String>> reader = new CsvMapper().readerFor(Map.class).with(schema).
+                readValues((InputStream) response.getEntity());
+
+        int rows = 0;
+        for (; reader.hasNext(); rows++) {
+            Map<String, String> row = reader.next();
+
+            assertEquals(users.getResult().get(rows).getUsername(), row.get("username"));
+            assertEquals(users.getResult().get(rows).getStatus(), row.get("status"));
+
+            switch (row.get("username")) {
+                case "rossini":
+                    assertEquals(spec.getNullValue(), row.get("email"));
+                    assertTrue(row.get("loginDate").contains(spec.getArrayElementSeparator()));
+                    break;
+
+                case "verdi":
+                    assertEquals("verdi@syncope.org", row.get("email"));
+                    assertEquals(spec.getNullValue(), row.get("loginDate"));
+                    break;
+
+                case "bellini":
+                    assertEquals(spec.getNullValue(), row.get("email"));
+                    assertFalse(row.get("loginDate").contains(spec.getArrayElementSeparator()));
+                    break;
+
+                default:
+                    break;
+            }
+        }
+        assertEquals(rows, users.getTotalCount());
+    }
 }
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java
index 95a44dd..f0a2e61 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/UserITCase.java
@@ -31,11 +31,12 @@ import java.io.IOException;
 import java.security.AccessControlException;
 import java.util.ArrayList;
 import java.util.Date;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.UUID;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 import javax.ws.rs.core.GenericType;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
@@ -858,8 +859,8 @@ public class UserITCase extends AbstractITCase {
     private static void verifyAsyncResult(final List<PropagationStatus> statuses) {
         assertEquals(3, statuses.size());
 
-        Map<String, PropagationStatus> byResource = new HashMap<>(3);
-        statuses.forEach(status -> byResource.put(status.getResource(), status));
+        Map<String, PropagationStatus> byResource = statuses.stream().collect(
+                Collectors.toMap(PropagationStatus::getResource, Function.identity()));
         assertEquals(ExecStatus.SUCCESS, byResource.get(RESOURCE_NAME_LDAP).getStatus());
         assertTrue(byResource.get(RESOURCE_NAME_TESTDB).getStatus() == ExecStatus.CREATED
                 || byResource.get(RESOURCE_NAME_TESTDB).getStatus() == ExecStatus.SUCCESS);
diff --git a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/VirAttrITCase.java b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/VirAttrITCase.java
index 05b65c8..cf3dd58 100644
--- a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/VirAttrITCase.java
+++ b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/VirAttrITCase.java
@@ -223,13 +223,13 @@ public class VirAttrITCase extends AbstractITCase {
 
         // 3. update virtual attribute directly
         JdbcTemplate jdbcTemplate = new JdbcTemplate(testDataSource);
-        String value = queryForObject(jdbcTemplate, 
+        String value = queryForObject(jdbcTemplate,
                 MAX_WAIT_SECONDS, "SELECT USERNAME FROM testpull WHERE ID=?", String.class, actual.getKey());
         assertEquals("virattrcache", value);
 
         jdbcTemplate.update("UPDATE testpull set USERNAME='virattrcache2' WHERE ID=?", actual.getKey());
 
-        value = queryForObject(jdbcTemplate, 
+        value = queryForObject(jdbcTemplate,
                 MAX_WAIT_SECONDS, "SELECT USERNAME FROM testpull WHERE ID=?", String.class, actual.getKey());
         assertEquals("virattrcache2", value);
 
@@ -380,14 +380,15 @@ public class VirAttrITCase extends AbstractITCase {
         // 4. update value on external resource
         // ----------------------------------------
         JdbcTemplate jdbcTemplate = new JdbcTemplate(testDataSource);
-        String value = queryForObject(
-                jdbcTemplate, MAX_WAIT_SECONDS, "SELECT USERNAME FROM testpull WHERE ID=?", String.class, userTO.getKey());
+        String value = queryForObject(jdbcTemplate,
+                MAX_WAIT_SECONDS, "SELECT USERNAME FROM testpull WHERE ID=?", String.class, userTO.
+                        getKey());
         assertEquals("virattrcache", value);
 
         jdbcTemplate.update("UPDATE testpull set USERNAME='virattrcache2' WHERE ID=?", userTO.getKey());
 
-        value = queryForObject(
-                jdbcTemplate, MAX_WAIT_SECONDS, "SELECT USERNAME FROM testpull WHERE ID=?", String.class, userTO.getKey());
+        value = queryForObject(jdbcTemplate,
+                MAX_WAIT_SECONDS, "SELECT USERNAME FROM testpull WHERE ID=?", String.class, userTO.getKey());
         assertEquals("virattrcache2", value);
         // ----------------------------------------
 
diff --git a/pom.xml b/pom.xml
index 7903d41..ed2b67a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -452,6 +452,7 @@ under the License.
     <h2.version>1.4.200</h2.version>
 
     <junit.version>5.5.2</junit.version>
+    <mockito.version>3.2.4</mockito.version>
 
     <conf.directory>${project.build.directory}/test-classes</conf.directory>
     <bundles.directory>${project.build.directory}/bundles</bundles.directory>
@@ -973,6 +974,11 @@ under the License.
         </exclusions>
       </dependency>
       <dependency>
+        <groupId>com.fasterxml.jackson.dataformat</groupId>
+        <artifactId>jackson-dataformat-csv</artifactId>
+        <version>${jackson.version}</version>
+      </dependency>
+      <dependency>
         <groupId>com.fasterxml.jackson.datatype</groupId>
         <artifactId>jackson-datatype-joda</artifactId>
         <version>${jackson.version}</version>
@@ -1785,10 +1791,22 @@ under the License.
       <dependency>
         <groupId>org.mockito</groupId>
         <artifactId>mockito-core</artifactId>
-        <version>3.2.0</version>
+        <version>${mockito.version}</version>
         <scope>test</scope>
       </dependency>
       <dependency>
+        <groupId>org.mockito</groupId>
+        <artifactId>mockito-junit-jupiter</artifactId>
+        <version>${mockito.version}</version>
+        <scope>test</scope>
+        <exclusions>
+          <exclusion>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+          </exclusion>
+        </exclusions>
+      </dependency>
+      <dependency>
         <groupId>org.junit.jupiter</groupId>
         <artifactId>junit-jupiter</artifactId>
         <version>${junit.version}</version>
@@ -2446,7 +2464,8 @@ under the License.
             <link>http://fasterxml.github.io/jackson-databind/javadoc/2.10/</link>
             <link>http://fasterxml.github.io/jackson-annotations/javadoc/2.10/</link>
             <link>http://fasterxml.github.io/jackson-dataformat-xml/javadoc/2.10/</link>
-            <link>http://fasterxml.github.io/jackson-dataformat-yaml/javadoc/2.9.pr1/</link>
+            <link>http://fasterxml.github.io/jackson-dataformats-text/javadoc/yaml/2.10/</link>
+            <link>http://fasterxml.github.io/jackson-dataformats-text/javadoc/csv/2.10/</link>
             <link>http://fasterxml.github.io/jackson-datatype-joda/javadoc/2.10/</link>
             <link>http://www.javadoc.io/doc/org.apache.camel/camel-core/3.0.0/</link>
             <link>http://www.javadoc.io/doc/org.apache.camel/camel-spring/3.0.0/</link>