[SYNCOPE-1279] Now providing runtime status updates from running Tasks and 
Reports


Project: http://git-wip-us.apache.org/repos/asf/syncope/repo
Commit: http://git-wip-us.apache.org/repos/asf/syncope/commit/799f079f
Tree: http://git-wip-us.apache.org/repos/asf/syncope/tree/799f079f
Diff: http://git-wip-us.apache.org/repos/asf/syncope/diff/799f079f

Branch: refs/heads/2_0_X
Commit: 799f079f132d6a2f9dc36a0bfd20475eb988febc
Parents: 988dfee
Author: Francesco Chicchiriccò <ilgro...@apache.org>
Authored: Fri Mar 2 11:45:34 2018 +0100
Committer: Francesco Chicchiriccò <ilgro...@apache.org>
Committed: Fri Mar 2 11:45:34 2018 +0100

----------------------------------------------------------------------
 .../console/reports/ReportDirectoryPanel.java   |  56 ++++
 .../console/rest/AnyObjectRestClient.java       |   2 -
 .../client/console/rest/ReportRestClient.java   |   4 +
 .../client/console/rest/TaskRestClient.java     |   4 +
 .../tasks/ProvisioningTaskDirectoryPanel.java   |  61 ++++
 .../client/console/widgets/JobActionPanel.java  |  23 +-
 .../client/console/widgets/JobWidget.java       |   4 +-
 .../META-INF/resources/css/syncopeConsole.css   |   6 +-
 .../client/console/widgets/JobActionPanel.html  |   2 +
 .../org/apache/syncope/common/lib/to/JobTO.java |  11 +
 .../rest/api/service/ExecutableService.java     |  11 +
 .../core/logic/AbstractExecutableLogic.java     |   2 +
 .../syncope/core/logic/AbstractJobLogic.java    |  63 ++--
 .../apache/syncope/core/logic/ReportLogic.java  |  25 ++
 .../apache/syncope/core/logic/TaskLogic.java    |  27 +-
 .../core/persistence/api/dao/Reportlet.java     |   4 +-
 .../core/provisioning/api/Connector.java        |   7 +-
 .../core/provisioning/api/job/JobDelegate.java  |  27 ++
 .../api/job/SchedTaskJobDelegate.java           |   2 +-
 .../api/job/report/ReportJobDelegate.java       |  27 ++
 .../notification/NotificationJobDelegate.java   |  31 ++
 .../api/pushpull/SyncopePullExecutor.java       |   3 +
 .../provisioning/java/ConnectorFacadeProxy.java |  13 +-
 .../java/job/AbstractInterruptableJob.java      |  21 +-
 .../java/job/AbstractSchedTaskJobDelegate.java  |  12 +
 .../GroupMemberProvisionTaskJobDelegate.java    |  28 +-
 .../java/job/IdentityRecertification.java       |   9 +-
 .../core/provisioning/java/job/TaskJob.java     |  19 +-
 .../DefaultNotificationJobDelegate.java         | 296 +++++++++++++++++++
 .../java/job/notification/NotificationJob.java  |   7 +
 .../notification/NotificationJobDelegate.java   | 278 -----------------
 .../java/job/report/AbstractReportlet.java      |  13 +-
 .../java/job/report/AuditReportlet.java         |  16 +-
 .../job/report/DefaultReportJobDelegate.java    | 216 ++++++++++++++
 .../java/job/report/GroupReportlet.java         |  19 +-
 .../job/report/ReconciliationReportlet.java     | 106 +++++--
 .../provisioning/java/job/report/ReportJob.java |   7 +
 .../java/job/report/ReportJobDelegate.java      | 197 ------------
 .../java/job/report/StaticReportlet.java        |   8 +-
 .../java/job/report/UserReportlet.java          |  17 +-
 .../AbstractPropagationTaskExecutor.java        |  11 +-
 .../pushpull/AbstractPullResultHandler.java     |   2 +
 .../pushpull/DefaultRealmPullResultHandler.java |   2 +
 .../java/pushpull/PullJobDelegate.java          |  40 +++
 .../java/pushpull/PushJobDelegate.java          |  52 ++++
 .../cxf/service/AbstractExecutableService.java  |   5 +
 46 files changed, 1215 insertions(+), 581 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/client/console/src/main/java/org/apache/syncope/client/console/reports/ReportDirectoryPanel.java
----------------------------------------------------------------------
diff --git 
a/client/console/src/main/java/org/apache/syncope/client/console/reports/ReportDirectoryPanel.java
 
b/client/console/src/main/java/org/apache/syncope/client/console/reports/ReportDirectoryPanel.java
index 01f77db..e0c5d6f 100644
--- 
a/client/console/src/main/java/org/apache/syncope/client/console/reports/ReportDirectoryPanel.java
+++ 
b/client/console/src/main/java/org/apache/syncope/client/console/reports/ReportDirectoryPanel.java
@@ -34,27 +34,36 @@ import org.apache.syncope.client.console.pages.BasePage;
 import org.apache.syncope.client.console.panels.DirectoryPanel;
 import org.apache.syncope.client.console.panels.MultilevelPanel;
 import org.apache.syncope.client.console.rest.ReportRestClient;
+import 
org.apache.syncope.client.console.wicket.ajax.IndicatorAjaxTimerBehavior;
 import 
org.apache.syncope.client.console.wicket.extensions.markup.html.repeater.data.table.BooleanPropertyColumn;
 import 
org.apache.syncope.client.console.wicket.extensions.markup.html.repeater.data.table.DatePropertyColumn;
 import 
org.apache.syncope.client.console.wicket.extensions.markup.html.repeater.data.table.KeyPropertyColumn;
 import org.apache.syncope.client.console.wicket.markup.html.form.ActionLink;
 import 
org.apache.syncope.client.console.wicket.markup.html.form.ActionLink.ActionType;
 import org.apache.syncope.client.console.wicket.markup.html.form.ActionsPanel;
+import org.apache.syncope.client.console.widgets.JobActionPanel;
 import org.apache.syncope.client.console.wizards.AjaxWizard;
 import org.apache.syncope.common.lib.types.StandardEntitlement;
 import org.apache.syncope.common.lib.SyncopeClientException;
+import org.apache.syncope.common.lib.to.JobTO;
 import org.apache.syncope.common.lib.to.ReportTO;
 import org.apache.wicket.PageReference;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import 
org.apache.wicket.authroles.authorization.strategies.role.metadata.MetaDataRoleAuthorizationStrategy;
 import org.apache.wicket.event.Broadcast;
+import org.apache.wicket.event.IEvent;
+import 
org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
 import org.apache.wicket.extensions.markup.html.repeater.data.sort.SortOrder;
+import 
org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn;
 import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
 import 
org.apache.wicket.extensions.markup.html.repeater.data.table.PropertyColumn;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.markup.repeater.Item;
 import org.apache.wicket.model.CompoundPropertyModel;
 import org.apache.wicket.model.IModel;
 import org.apache.wicket.model.Model;
 import org.apache.wicket.model.StringResourceModel;
+import org.apache.wicket.util.time.Duration;
 
 /**
  * Reports page.
@@ -76,6 +85,17 @@ public abstract class ReportDirectoryPanel
         modal.size(Modal.Size.Large);
         initResultTable();
 
+        container.add(new IndicatorAjaxTimerBehavior(Duration.seconds(10)) {
+
+            private static final long serialVersionUID = -4661303265651934868L;
+
+            @Override
+            protected void onTimer(final AjaxRequestTarget target) {
+                container.modelChanged();
+                target.add(container);
+            }
+        });
+
         startAt = new ReportStartAtTogglePanel(container, pageRef);
         addInnerObject(startAt);
     }
@@ -107,10 +127,46 @@ public abstract class ReportDirectoryPanel
         columns.add(new BooleanPropertyColumn<ReportTO>(
                 new StringResourceModel("active", this), "active", "active"));
 
+        columns.add(new AbstractColumn<ReportTO, String>(new Model<>(""), 
"running") {
+
+            private static final long serialVersionUID = 4209532514416998046L;
+
+            @Override
+            public void populateItem(
+                    final Item<ICellPopulator<ReportTO>> cellItem,
+                    final String componentId,
+                    final IModel<ReportTO> rowModel) {
+
+                JobTO jobTO = restClient.getJob(rowModel.getObject().getKey());
+                JobActionPanel panel = new JobActionPanel(
+                        componentId, jobTO, false, ReportDirectoryPanel.this, 
pageRef);
+                MetaDataRoleAuthorizationStrategy.authorize(panel, 
WebPage.ENABLE,
+                        String.format("%s,%s",
+                                StandardEntitlement.TASK_EXECUTE,
+                                StandardEntitlement.TASK_UPDATE));
+                cellItem.add(panel);
+            }
+
+            @Override
+            public String getCssClass() {
+                return "col-xs-1";
+            }
+        });
+
         return columns;
     }
 
     @Override
+    public void onEvent(final IEvent<?> event) {
+        if (event.getPayload() instanceof JobActionPanel.JobActionPayload) {
+            container.modelChanged();
+            
JobActionPanel.JobActionPayload.class.cast(event.getPayload()).getTarget().add(container);
+        } else {
+            super.onEvent(event);
+        }
+    }
+
+    @Override
     public ActionsPanel<ReportTO> getActions(final IModel<ReportTO> model) {
         final ActionsPanel<ReportTO> panel = super.getActions(model);
 

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/client/console/src/main/java/org/apache/syncope/client/console/rest/AnyObjectRestClient.java
----------------------------------------------------------------------
diff --git 
a/client/console/src/main/java/org/apache/syncope/client/console/rest/AnyObjectRestClient.java
 
b/client/console/src/main/java/org/apache/syncope/client/console/rest/AnyObjectRestClient.java
index 31cd3a8..a885764 100644
--- 
a/client/console/src/main/java/org/apache/syncope/client/console/rest/AnyObjectRestClient.java
+++ 
b/client/console/src/main/java/org/apache/syncope/client/console/rest/AnyObjectRestClient.java
@@ -18,8 +18,6 @@
  */
 package org.apache.syncope.client.console.rest;
 
-import static org.apache.syncope.client.console.rest.BaseRestClient.getService;
-
 import java.util.List;
 import javax.ws.rs.core.GenericType;
 import javax.ws.rs.core.Response;

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/client/console/src/main/java/org/apache/syncope/client/console/rest/ReportRestClient.java
----------------------------------------------------------------------
diff --git 
a/client/console/src/main/java/org/apache/syncope/client/console/rest/ReportRestClient.java
 
b/client/console/src/main/java/org/apache/syncope/client/console/rest/ReportRestClient.java
index 0c96627..e65ce7e 100644
--- 
a/client/console/src/main/java/org/apache/syncope/client/console/rest/ReportRestClient.java
+++ 
b/client/console/src/main/java/org/apache/syncope/client/console/rest/ReportRestClient.java
@@ -54,6 +54,10 @@ public class ReportRestClient extends BaseRestClient
         return getService(ReportService.class).list();
     }
 
+    public JobTO getJob(final String key) {
+        return getService(ReportService.class).getJob(key);
+    }
+
     public List<JobTO> listJobs() {
         return getService(ReportService.class).listJobs();
     }

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/client/console/src/main/java/org/apache/syncope/client/console/rest/TaskRestClient.java
----------------------------------------------------------------------
diff --git 
a/client/console/src/main/java/org/apache/syncope/client/console/rest/TaskRestClient.java
 
b/client/console/src/main/java/org/apache/syncope/client/console/rest/TaskRestClient.java
index d184085..723ae2e 100644
--- 
a/client/console/src/main/java/org/apache/syncope/client/console/rest/TaskRestClient.java
+++ 
b/client/console/src/main/java/org/apache/syncope/client/console/rest/TaskRestClient.java
@@ -47,6 +47,10 @@ public class TaskRestClient extends BaseRestClient 
implements ExecutionRestClien
 
     private static final long serialVersionUID = 6284485820911028843L;
 
+    public JobTO getJob(final String key) {
+        return getService(TaskService.class).getJob(key);
+    }
+
     public List<JobTO> listJobs() {
         return getService(TaskService.class).listJobs();
     }

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/client/console/src/main/java/org/apache/syncope/client/console/tasks/ProvisioningTaskDirectoryPanel.java
----------------------------------------------------------------------
diff --git 
a/client/console/src/main/java/org/apache/syncope/client/console/tasks/ProvisioningTaskDirectoryPanel.java
 
b/client/console/src/main/java/org/apache/syncope/client/console/tasks/ProvisioningTaskDirectoryPanel.java
index 91f17b8..cacd6f7 100644
--- 
a/client/console/src/main/java/org/apache/syncope/client/console/tasks/ProvisioningTaskDirectoryPanel.java
+++ 
b/client/console/src/main/java/org/apache/syncope/client/console/tasks/ProvisioningTaskDirectoryPanel.java
@@ -23,18 +23,32 @@ import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 import org.apache.syncope.client.console.panels.MultilevelPanel;
+import 
org.apache.syncope.client.console.wicket.ajax.IndicatorAjaxTimerBehavior;
 import 
org.apache.syncope.client.console.wicket.extensions.markup.html.repeater.data.table.BooleanPropertyColumn;
 import 
org.apache.syncope.client.console.wicket.extensions.markup.html.repeater.data.table.DatePropertyColumn;
 import 
org.apache.syncope.client.console.wicket.extensions.markup.html.repeater.data.table.KeyPropertyColumn;
 import 
org.apache.syncope.client.console.wicket.markup.html.bootstrap.dialog.BaseModal;
+import org.apache.syncope.client.console.widgets.JobActionPanel;
+import org.apache.syncope.common.lib.to.JobTO;
 import org.apache.syncope.common.lib.to.ProvisioningTaskTO;
 import org.apache.syncope.common.lib.to.PullTaskTO;
 import org.apache.syncope.common.lib.to.PushTaskTO;
+import org.apache.syncope.common.lib.types.StandardEntitlement;
 import org.apache.syncope.common.lib.types.TaskType;
 import org.apache.wicket.PageReference;
+import org.apache.wicket.ajax.AjaxRequestTarget;
+import 
org.apache.wicket.authroles.authorization.strategies.role.metadata.MetaDataRoleAuthorizationStrategy;
+import org.apache.wicket.event.IEvent;
+import 
org.apache.wicket.extensions.markup.html.repeater.data.grid.ICellPopulator;
+import 
org.apache.wicket.extensions.markup.html.repeater.data.table.AbstractColumn;
 import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
 import 
org.apache.wicket.extensions.markup.html.repeater.data.table.PropertyColumn;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.markup.repeater.Item;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
 import org.apache.wicket.model.StringResourceModel;
+import org.apache.wicket.util.time.Duration;
 
 /**
  * Tasks page.
@@ -63,6 +77,17 @@ public abstract class ProvisioningTaskDirectoryPanel<T 
extends ProvisioningTaskT
 
         // super in order to call the parent implementation
         super.initResultTable();
+
+        container.add(new IndicatorAjaxTimerBehavior(Duration.seconds(10)) {
+
+            private static final long serialVersionUID = -4661303265651934868L;
+
+            @Override
+            protected void onTimer(final AjaxRequestTarget target) {
+                container.modelChanged();
+                target.add(container);
+            }
+        });
     }
 
     @Override
@@ -103,9 +128,45 @@ public abstract class ProvisioningTaskDirectoryPanel<T 
extends ProvisioningTaskT
         columns.add(new BooleanPropertyColumn<T>(
                 new StringResourceModel("active", this), "active", "active"));
 
+        columns.add(new AbstractColumn<T, String>(new Model<>(""), "running") {
+
+            private static final long serialVersionUID = -4008579357070833846L;
+
+            @Override
+            public void populateItem(
+                    final Item<ICellPopulator<T>> cellItem,
+                    final String componentId,
+                    final IModel<T> rowModel) {
+
+                JobTO jobTO = restClient.getJob(rowModel.getObject().getKey());
+                JobActionPanel panel = new JobActionPanel(
+                        componentId, jobTO, false, 
ProvisioningTaskDirectoryPanel.this, pageRef);
+                MetaDataRoleAuthorizationStrategy.authorize(panel, 
WebPage.ENABLE,
+                        String.format("%s,%s",
+                                StandardEntitlement.TASK_EXECUTE,
+                                StandardEntitlement.TASK_UPDATE));
+                cellItem.add(panel);
+            }
+
+            @Override
+            public String getCssClass() {
+                return "col-xs-1";
+            }
+        });
+
         return columns;
     }
 
+    @Override
+    public void onEvent(final IEvent<?> event) {
+        if (event.getPayload() instanceof JobActionPanel.JobActionPayload) {
+            container.modelChanged();
+            
JobActionPanel.JobActionPayload.class.cast(event.getPayload()).getTarget().add(container);
+        } else {
+            super.onEvent(event);
+        }
+    }
+
     protected class ProvisioningTasksProvider<T extends ProvisioningTaskTO> 
extends SchedTasksProvider<T> {
 
         private static final long serialVersionUID = 4725679400450513556L;

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/client/console/src/main/java/org/apache/syncope/client/console/widgets/JobActionPanel.java
----------------------------------------------------------------------
diff --git 
a/client/console/src/main/java/org/apache/syncope/client/console/widgets/JobActionPanel.java
 
b/client/console/src/main/java/org/apache/syncope/client/console/widgets/JobActionPanel.java
index 82665f8..5e03e2d 100644
--- 
a/client/console/src/main/java/org/apache/syncope/client/console/widgets/JobActionPanel.java
+++ 
b/client/console/src/main/java/org/apache/syncope/client/console/widgets/JobActionPanel.java
@@ -18,6 +18,9 @@
  */
 package org.apache.syncope.client.console.widgets;
 
+import 
de.agilecoders.wicket.core.markup.html.bootstrap.components.PopoverBehavior;
+import 
de.agilecoders.wicket.core.markup.html.bootstrap.components.PopoverConfig;
+import 
de.agilecoders.wicket.core.markup.html.bootstrap.components.TooltipConfig;
 import java.io.Serializable;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.syncope.client.console.SyncopeConsoleSession;
@@ -30,10 +33,13 @@ import 
org.apache.syncope.client.console.wicket.ajax.markup.html.IndicatorAjaxLi
 import org.apache.syncope.client.console.wizards.WizardMgtPanel;
 import org.apache.syncope.common.lib.to.JobTO;
 import org.apache.syncope.common.lib.types.JobAction;
+import org.apache.wicket.Component;
 import org.apache.wicket.PageReference;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.event.Broadcast;
+import org.apache.wicket.markup.html.basic.Label;
 import org.apache.wicket.markup.html.panel.Fragment;
+import org.apache.wicket.model.Model;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -52,14 +58,21 @@ public class JobActionPanel extends 
WizardMgtPanel<Serializable> {
     public JobActionPanel(
             final String id,
             final JobTO jobTO,
-            final JobWidget widget,
+            final boolean showNotRunning,
+            final Component container,
             final PageReference pageRef) {
+
         super(id, true);
         setOutputMarkupId(true);
 
         Fragment controls;
         if (jobTO.isRunning()) {
             controls = new Fragment("controls", "runningFragment", this);
+            controls.add(new Label("status", Model.of()).add(new 
PopoverBehavior(
+                    Model.<String>of(),
+                    Model.of("<pre>" + (jobTO.getStatus() == null ? 
StringUtils.EMPTY : jobTO.getStatus()) + "</pre>"),
+                    new 
PopoverConfig().withAnimation(true).withHoverTrigger().withHtml(true).
+                            withPlacement(TooltipConfig.Placement.left))));
             controls.add(new IndicatorAjaxLink<Void>("stop") {
 
                 private static final long serialVersionUID = 
-7978723352517770644L;
@@ -83,7 +96,7 @@ public class JobActionPanel extends 
WizardMgtPanel<Serializable> {
                             default:
                         }
                         
SyncopeConsoleSession.get().info(getString(Constants.OPERATION_SUCCEEDED));
-                        send(widget, Broadcast.EXACT, new 
JobActionPayload(target));
+                        send(container, Broadcast.EXACT, new 
JobActionPayload(target));
                     } catch (Exception e) {
                         LOG.error("While stopping {}", jobTO.getRefDesc(), e);
                         
SyncopeConsoleSession.get().error(StringUtils.isBlank(e.getMessage()) ? 
e.getClass().getName()
@@ -117,7 +130,7 @@ public class JobActionPanel extends 
WizardMgtPanel<Serializable> {
                             default:
                         }
                         
SyncopeConsoleSession.get().info(getString(Constants.OPERATION_SUCCEEDED));
-                        send(widget, Broadcast.EXACT, new 
JobActionPayload(target));
+                        send(container, Broadcast.EXACT, new 
JobActionPayload(target));
                     } catch (Exception e) {
                         LOG.error("While starting {}", jobTO.getRefDesc(), e);
                         
SyncopeConsoleSession.get().error(StringUtils.isBlank(e.getMessage()) ? 
e.getClass().getName()
@@ -126,6 +139,10 @@ public class JobActionPanel extends 
WizardMgtPanel<Serializable> {
                     ((BasePage) 
getPage()).getNotificationPanel().refresh(target);
                 }
             });
+            if (!showNotRunning) {
+                controls.setOutputMarkupPlaceholderTag(true);
+                controls.setVisible(false);
+            }
         }
         addInnerObject(controls);
     }

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/client/console/src/main/java/org/apache/syncope/client/console/widgets/JobWidget.java
----------------------------------------------------------------------
diff --git 
a/client/console/src/main/java/org/apache/syncope/client/console/widgets/JobWidget.java
 
b/client/console/src/main/java/org/apache/syncope/client/console/widgets/JobWidget.java
index cd10c70..98a53e4 100644
--- 
a/client/console/src/main/java/org/apache/syncope/client/console/widgets/JobWidget.java
+++ 
b/client/console/src/main/java/org/apache/syncope/client/console/widgets/JobWidget.java
@@ -357,9 +357,9 @@ public class JobWidget extends BaseWidget {
                         final IModel<JobTO> rowModel) {
 
                     JobTO jobTO = rowModel.getObject();
-                    JobActionPanel panel = new JobActionPanel(componentId, 
jobTO, JobWidget.this, pageRef);
+                    JobActionPanel panel = new JobActionPanel(componentId, 
jobTO, true, JobWidget.this, pageRef);
                     MetaDataRoleAuthorizationStrategy.authorize(panel, 
WebPage.ENABLE,
-                            String.format("%s,%s%s,%s",
+                            String.format("%s,%s,%s,%s",
                                     StandardEntitlement.TASK_EXECUTE,
                                     StandardEntitlement.REPORT_EXECUTE,
                                     StandardEntitlement.TASK_UPDATE,

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/client/console/src/main/resources/META-INF/resources/css/syncopeConsole.css
----------------------------------------------------------------------
diff --git 
a/client/console/src/main/resources/META-INF/resources/css/syncopeConsole.css 
b/client/console/src/main/resources/META-INF/resources/css/syncopeConsole.css
index 294c978..c2dcf2c 100644
--- 
a/client/console/src/main/resources/META-INF/resources/css/syncopeConsole.css
+++ 
b/client/console/src/main/resources/META-INF/resources/css/syncopeConsole.css
@@ -879,6 +879,10 @@ li.todoitem a {
   cursor: default;
 }
 
+.popover{
+    max-width: 100%;
+}
+
 #popover:hover {
   cursor: pointer;
 }
@@ -1157,4 +1161,4 @@ div#inline-actions ul.menu i, div#tablehandling ul.menu i 
{
 
 div#tablehandling ul.menu li a {
   padding: 0px !important;
-}
\ No newline at end of file
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/client/console/src/main/resources/org/apache/syncope/client/console/widgets/JobActionPanel.html
----------------------------------------------------------------------
diff --git 
a/client/console/src/main/resources/org/apache/syncope/client/console/widgets/JobActionPanel.html
 
b/client/console/src/main/resources/org/apache/syncope/client/console/widgets/JobActionPanel.html
index e94f292..8e31f36 100644
--- 
a/client/console/src/main/resources/org/apache/syncope/client/console/widgets/JobActionPanel.html
+++ 
b/client/console/src/main/resources/org/apache/syncope/client/console/widgets/JobActionPanel.html
@@ -22,6 +22,8 @@ under the License.
     <wicket:fragment wicket:id="runningFragment">
       <i id="actionLink" class="fa fa-refresh fa-spin"></i>
       &nbsp;
+      <div wicket:id="status" class="fa fa-binoculars"/>
+      &nbsp;
       <a href="#" wicket:id="stop" class="fa fa-stop-circle"></a>
     </wicket:fragment>
     <wicket:fragment wicket:id="notRunningFragment">

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/common/lib/src/main/java/org/apache/syncope/common/lib/to/JobTO.java
----------------------------------------------------------------------
diff --git 
a/common/lib/src/main/java/org/apache/syncope/common/lib/to/JobTO.java 
b/common/lib/src/main/java/org/apache/syncope/common/lib/to/JobTO.java
index 2cb5690..cee8db2 100644
--- a/common/lib/src/main/java/org/apache/syncope/common/lib/to/JobTO.java
+++ b/common/lib/src/main/java/org/apache/syncope/common/lib/to/JobTO.java
@@ -42,6 +42,8 @@ public class JobTO extends AbstractBaseBean {
 
     private Date start;
 
+    private String status;
+
     public JobType getType() {
         return type;
     }
@@ -93,4 +95,13 @@ public class JobTO extends AbstractBaseBean {
                 ? null
                 : new Date(start.getTime());
     }
+
+    public String getStatus() {
+        return status;
+    }
+
+    public void setStatus(final String status) {
+        this.status = status;
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ExecutableService.java
----------------------------------------------------------------------
diff --git 
a/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ExecutableService.java
 
b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ExecutableService.java
index e7271b2..37301bd 100644
--- 
a/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ExecutableService.java
+++ 
b/common/rest-api/src/main/java/org/apache/syncope/common/rest/api/service/ExecutableService.java
@@ -101,6 +101,17 @@ public interface ExecutableService extends JAXRSService {
     ExecTO execute(@BeanParam ExecuteQuery query);
 
     /**
+     * Returns job (running or scheduled) for the executable matching the 
given key.
+     *
+     * @param key executable key
+     * @return job (running or scheduled) for the given key
+     */
+    @GET
+    @Path("jobs/{key}")
+    @Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
+    JobTO getJob(@PathParam("key") String key);
+
+    /**
      * List jobs (running and / or scheduled).
      *
      * @return jobs (running and / or scheduled)

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/core/logic/src/main/java/org/apache/syncope/core/logic/AbstractExecutableLogic.java
----------------------------------------------------------------------
diff --git 
a/core/logic/src/main/java/org/apache/syncope/core/logic/AbstractExecutableLogic.java
 
b/core/logic/src/main/java/org/apache/syncope/core/logic/AbstractExecutableLogic.java
index d1b7cb3..6b34bc7 100644
--- 
a/core/logic/src/main/java/org/apache/syncope/core/logic/AbstractExecutableLogic.java
+++ 
b/core/logic/src/main/java/org/apache/syncope/core/logic/AbstractExecutableLogic.java
@@ -42,6 +42,8 @@ public abstract class AbstractExecutableLogic<T extends 
AbstractBaseBean> extend
     public abstract BulkActionResult deleteExecutions(
             String key, Date startedBefore, Date startedAfter, Date 
endedBefore, Date endedAfter);
 
+    public abstract JobTO getJob(String key);
+
     public abstract List<JobTO> listJobs();
 
     public abstract void actionJob(String key, JobAction action);

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/core/logic/src/main/java/org/apache/syncope/core/logic/AbstractJobLogic.java
----------------------------------------------------------------------
diff --git 
a/core/logic/src/main/java/org/apache/syncope/core/logic/AbstractJobLogic.java 
b/core/logic/src/main/java/org/apache/syncope/core/logic/AbstractJobLogic.java
index a93ae2d..844d353 100644
--- 
a/core/logic/src/main/java/org/apache/syncope/core/logic/AbstractJobLogic.java
+++ 
b/core/logic/src/main/java/org/apache/syncope/core/logic/AbstractJobLogic.java
@@ -26,11 +26,14 @@ import org.apache.syncope.common.lib.to.JobTO;
 import org.apache.syncope.common.lib.types.JobAction;
 import org.apache.syncope.common.lib.types.JobType;
 import org.apache.syncope.core.provisioning.api.job.JobManager;
+import org.apache.syncope.core.provisioning.java.job.AbstractInterruptableJob;
+import org.apache.syncope.core.spring.ApplicationContextProvider;
 import org.quartz.JobKey;
 import org.quartz.Scheduler;
 import org.quartz.SchedulerException;
 import org.quartz.Trigger;
 import org.quartz.impl.matchers.GroupMatcher;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.quartz.SchedulerFactoryBean;
 
@@ -44,32 +47,54 @@ abstract class AbstractJobLogic<T extends AbstractBaseBean> 
extends AbstractTran
 
     protected abstract Triple<JobType, String, String> getReference(final 
JobKey jobKey);
 
-    protected List<JobTO> doListJobs() {
-        List<JobTO> jobTOs = new ArrayList<>();
+    protected JobTO getJobTO(final JobKey jobKey) throws SchedulerException {
+        JobTO jobTO = null;
 
-        try {
-            for (JobKey jobKey : scheduler.getScheduler().
-                    
getJobKeys(GroupMatcher.jobGroupEquals(Scheduler.DEFAULT_GROUP))) {
+        Triple<JobType, String, String> reference = getReference(jobKey);
+        if (reference != null) {
+            jobTO = new JobTO();
 
-                JobTO jobTO = new JobTO();
+            jobTO.setType(reference.getLeft());
+            jobTO.setRefKey(reference.getMiddle());
+            jobTO.setRefDesc(reference.getRight());
 
-                Triple<JobType, String, String> reference = 
getReference(jobKey);
-                if (reference != null) {
-                    jobTOs.add(jobTO);
+            List<? extends Trigger> jobTriggers = 
scheduler.getScheduler().getTriggersOfJob(jobKey);
+            if (jobTriggers.isEmpty()) {
+                jobTO.setScheduled(false);
+            } else {
+                jobTO.setScheduled(true);
+                jobTO.setStart(jobTriggers.get(0).getStartTime());
+            }
+
+            jobTO.setRunning(jobManager.isRunning(jobKey));
 
-                    jobTO.setType(reference.getLeft());
-                    jobTO.setRefKey(reference.getMiddle());
-                    jobTO.setRefDesc(reference.getRight());
+            jobTO.setStatus("UNKNOWN");
+            if (jobTO.isRunning()) {
+                try {
+                    Object job = 
ApplicationContextProvider.getBeanFactory().getBean(jobKey.getName());
+                    if (job instanceof AbstractInterruptableJob
+                            && ((AbstractInterruptableJob) job).getDelegate() 
!= null) {
 
-                    List<? extends Trigger> jobTriggers = 
scheduler.getScheduler().getTriggersOfJob(jobKey);
-                    if (jobTriggers.isEmpty()) {
-                        jobTO.setScheduled(false);
-                    } else {
-                        jobTO.setScheduled(true);
-                        jobTO.setStart(jobTriggers.get(0).getStartTime());
+                        jobTO.setStatus(((AbstractInterruptableJob) 
job).getDelegate().currentStatus());
                     }
+                } catch (NoSuchBeanDefinitionException e) {
+                    LOG.warn("Could not find job {} implementation", jobKey, 
e);
+                }
+            }
+        }
+
+        return jobTO;
+    }
+
+    protected List<JobTO> doListJobs() {
+        List<JobTO> jobTOs = new ArrayList<>();
+        try {
+            for (JobKey jobKey : scheduler.getScheduler().
+                    
getJobKeys(GroupMatcher.jobGroupEquals(Scheduler.DEFAULT_GROUP))) {
 
-                    jobTO.setRunning(jobManager.isRunning(jobKey));
+                JobTO jobTO = getJobTO(jobKey);
+                if (jobTO != null) {
+                    jobTOs.add(jobTO);
                 }
             }
         } catch (SchedulerException e) {

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/core/logic/src/main/java/org/apache/syncope/core/logic/ReportLogic.java
----------------------------------------------------------------------
diff --git 
a/core/logic/src/main/java/org/apache/syncope/core/logic/ReportLogic.java 
b/core/logic/src/main/java/org/apache/syncope/core/logic/ReportLogic.java
index 9505303..da8f2b7 100644
--- a/core/logic/src/main/java/org/apache/syncope/core/logic/ReportLogic.java
+++ b/core/logic/src/main/java/org/apache/syncope/core/logic/ReportLogic.java
@@ -66,6 +66,7 @@ import 
org.apache.syncope.core.provisioning.api.data.ReportDataBinder;
 import org.apache.syncope.core.provisioning.api.job.JobNamer;
 import org.apache.xmlgraphics.util.MimeConstants;
 import org.quartz.JobKey;
+import org.quartz.SchedulerException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Component;
@@ -387,6 +388,30 @@ public class ReportLogic extends 
AbstractExecutableLogic<ReportTO> {
         return super.doListJobs();
     }
 
+    @PreAuthorize("hasRole('" + StandardEntitlement.REPORT_READ + "')")
+    @Override
+    public JobTO getJob(final String key) {
+        Report report = reportDAO.find(key);
+        if (report == null) {
+            throw new NotFoundException("Report " + key);
+        }
+
+        JobTO jobTO = null;
+        try {
+            jobTO = getJobTO(JobNamer.getJobKey(report));
+        } catch (SchedulerException e) {
+            LOG.error("Problems while retrieving scheduled job {}", 
JobNamer.getJobKey(report), e);
+
+            SyncopeClientException sce = 
SyncopeClientException.build(ClientExceptionType.Scheduling);
+            sce.getElements().add(e.getMessage());
+            throw sce;
+        }
+        if (jobTO == null) {
+            throw new NotFoundException("Job for report " + key);
+        }
+        return jobTO;
+    }
+
     @PreAuthorize("hasRole('" + StandardEntitlement.REPORT_EXECUTE + "')")
     @Override
     public void actionJob(final String key, final JobAction action) {

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/core/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java
----------------------------------------------------------------------
diff --git 
a/core/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java 
b/core/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java
index 9603bc8..25f06a1 100644
--- a/core/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java
+++ b/core/logic/src/main/java/org/apache/syncope/core/logic/TaskLogic.java
@@ -57,10 +57,11 @@ import 
org.apache.syncope.core.provisioning.api.propagation.PropagationTaskExecu
 import org.apache.syncope.core.persistence.api.dao.ConfDAO;
 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.java.job.TaskJob;
-import 
org.apache.syncope.core.provisioning.java.job.notification.NotificationJobDelegate;
 import org.quartz.JobDataMap;
 import org.quartz.JobKey;
+import org.quartz.SchedulerException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.stereotype.Component;
@@ -402,6 +403,30 @@ public class TaskLogic extends 
AbstractExecutableLogic<TaskTO> {
         return super.doListJobs();
     }
 
+    @PreAuthorize("hasRole('" + StandardEntitlement.TASK_READ + "')")
+    @Override
+    public JobTO getJob(final String key) {
+        Task task = taskDAO.find(key);
+        if (task == null) {
+            throw new NotFoundException("Task " + key);
+        }
+
+        JobTO jobTO = null;
+        try {
+            jobTO = getJobTO(JobNamer.getJobKey(task));
+        } catch (SchedulerException e) {
+            LOG.error("Problems while retrieving scheduled job {}", 
JobNamer.getJobKey(task), e);
+
+            SyncopeClientException sce = 
SyncopeClientException.build(ClientExceptionType.Scheduling);
+            sce.getElements().add(e.getMessage());
+            throw sce;
+        }
+        if (jobTO == null) {
+            throw new NotFoundException("Job for task " + key);
+        }
+        return jobTO;
+    }
+
     @PreAuthorize("hasRole('" + StandardEntitlement.TASK_EXECUTE + "')")
     @Override
     public void actionJob(final String key, final JobAction action) {

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/Reportlet.java
----------------------------------------------------------------------
diff --git 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/Reportlet.java
 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/Reportlet.java
index 8180831..11e6b25 100644
--- 
a/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/Reportlet.java
+++ 
b/core/persistence-api/src/main/java/org/apache/syncope/core/persistence/api/dao/Reportlet.java
@@ -18,6 +18,7 @@
  */
 package org.apache.syncope.core.persistence.api.dao;
 
+import java.util.concurrent.atomic.AtomicReference;
 import org.apache.syncope.common.lib.report.ReportletConf;
 import org.xml.sax.ContentHandler;
 import org.xml.sax.SAXException;
@@ -34,7 +35,8 @@ public interface Reportlet {
      *
      * @param conf configuration
      * @param handler SAX content handler for streaming result
+     * @param status current report status (for job reporting)
      * @throws SAXException if there is any problem in SAX handling
      */
-    void extract(ReportletConf conf, ContentHandler handler) throws 
SAXException;
+    void extract(ReportletConf conf, ContentHandler handler, 
AtomicReference<String> status) throws SAXException;
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/Connector.java
----------------------------------------------------------------------
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 901cdc7..2098466 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
@@ -20,6 +20,7 @@ package org.apache.syncope.core.provisioning.api;
 
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
 import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
 import org.apache.syncope.core.persistence.api.entity.ConnInstance;
 import org.identityconnectors.framework.common.objects.Attribute;
@@ -63,7 +64,7 @@ public interface Connector {
             ObjectClass objectClass,
             Set<Attribute> attrs,
             OperationOptions options,
-            Boolean[] propagationAttempted);
+            AtomicReference<Boolean> propagationAttempted);
 
     /**
      * Update user / group on a connector instance.
@@ -80,7 +81,7 @@ public interface Connector {
             Uid uid,
             Set<Attribute> attrs,
             OperationOptions options,
-            Boolean[] propagationAttempted);
+            AtomicReference<Boolean> propagationAttempted);
 
     /**
      * Delete user / group on a connector instance.
@@ -94,7 +95,7 @@ public interface Connector {
             ObjectClass objectClass,
             Uid uid,
             OperationOptions options,
-            Boolean[] propagationAttempted);
+            AtomicReference<Boolean> propagationAttempted);
 
     /**
      * Fetches all remote objects (for use during full reconciliation).

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/job/JobDelegate.java
----------------------------------------------------------------------
diff --git 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/job/JobDelegate.java
 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/job/JobDelegate.java
new file mode 100644
index 0000000..3bfa292
--- /dev/null
+++ 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/job/JobDelegate.java
@@ -0,0 +1,27 @@
+/*
+ * 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.job;
+
+/**
+ * Implementations of this interface will perform the actual operations 
required to Quartz's {@link org.quartz.Job}.
+ */
+public interface JobDelegate {
+
+    String currentStatus();
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/job/SchedTaskJobDelegate.java
----------------------------------------------------------------------
diff --git 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/job/SchedTaskJobDelegate.java
 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/job/SchedTaskJobDelegate.java
index a03f36b..bb69b10 100644
--- 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/job/SchedTaskJobDelegate.java
+++ 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/job/SchedTaskJobDelegate.java
@@ -21,7 +21,7 @@ package org.apache.syncope.core.provisioning.api.job;
 import org.quartz.JobExecutionContext;
 import org.quartz.JobExecutionException;
 
-public interface SchedTaskJobDelegate {
+public interface SchedTaskJobDelegate extends JobDelegate {
 
     void execute(String taskKey, boolean dryRun, JobExecutionContext context) 
throws JobExecutionException;
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/job/report/ReportJobDelegate.java
----------------------------------------------------------------------
diff --git 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/job/report/ReportJobDelegate.java
 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/job/report/ReportJobDelegate.java
new file mode 100644
index 0000000..bbf455f
--- /dev/null
+++ 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/job/report/ReportJobDelegate.java
@@ -0,0 +1,27 @@
+/*
+ * 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.job.report;
+
+import org.apache.syncope.core.provisioning.api.job.JobDelegate;
+import org.quartz.JobExecutionException;
+
+public interface ReportJobDelegate extends JobDelegate {
+
+    void execute(String reportKey) throws JobExecutionException;
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/notification/NotificationJobDelegate.java
----------------------------------------------------------------------
diff --git 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/notification/NotificationJobDelegate.java
 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/notification/NotificationJobDelegate.java
new file mode 100644
index 0000000..3dfcddd
--- /dev/null
+++ 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/notification/NotificationJobDelegate.java
@@ -0,0 +1,31 @@
+/*
+ * 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.notification;
+
+import org.apache.syncope.core.persistence.api.entity.task.NotificationTask;
+import org.apache.syncope.core.persistence.api.entity.task.TaskExec;
+import org.apache.syncope.core.provisioning.api.job.JobDelegate;
+import org.quartz.JobExecutionException;
+
+public interface NotificationJobDelegate extends JobDelegate {
+
+    TaskExec executeSingle(NotificationTask task);
+
+    void execute() throws JobExecutionException;
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopePullExecutor.java
----------------------------------------------------------------------
diff --git 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopePullExecutor.java
 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopePullExecutor.java
index ab02282..39eed32 100644
--- 
a/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopePullExecutor.java
+++ 
b/core/provisioning-api/src/main/java/org/apache/syncope/core/provisioning/api/pushpull/SyncopePullExecutor.java
@@ -18,10 +18,13 @@
  */
 package org.apache.syncope.core.provisioning.api.pushpull;
 
+import org.identityconnectors.framework.common.objects.Name;
 import org.identityconnectors.framework.common.objects.ObjectClass;
 import org.identityconnectors.framework.common.objects.SyncToken;
 
 public interface SyncopePullExecutor {
 
     void setLatestSyncToken(ObjectClass objectClass, SyncToken 
latestSyncToken);
+
+    void reportHandled(ObjectClass objectClass, Name name);
 }

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/ConnectorFacadeProxy.java
----------------------------------------------------------------------
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 7e94c5b..aea4e27 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
@@ -25,6 +25,7 @@ import java.util.List;
 import java.util.Set;
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.commons.collections4.Transformer;
 import org.apache.syncope.common.lib.types.ConnConfProperty;
@@ -165,12 +166,12 @@ public class ConnectorFacadeProxy implements Connector {
             final ObjectClass objectClass,
             final Set<Attribute> attrs,
             final OperationOptions options,
-            final Boolean[] propagationAttempted) {
+            final AtomicReference<Boolean> propagationAttempted) {
 
         Uid result = null;
 
         if 
(connInstance.getCapabilities().contains(ConnectorCapability.CREATE)) {
-            propagationAttempted[0] = true;
+            propagationAttempted.set(true);
 
             Future<Uid> future = asyncFacade.create(connector, objectClass, 
attrs, options);
             try {
@@ -200,12 +201,12 @@ public class ConnectorFacadeProxy implements Connector {
             final Uid uid,
             final Set<Attribute> attrs,
             final OperationOptions options,
-            final Boolean[] propagationAttempted) {
+            final AtomicReference<Boolean> propagationAttempted) {
 
         Uid result = null;
 
         if 
(connInstance.getCapabilities().contains(ConnectorCapability.UPDATE)) {
-            propagationAttempted[0] = true;
+            propagationAttempted.set(true);
 
             Future<Uid> future = asyncFacade.update(connector, objectClass, 
uid, attrs, options);
 
@@ -236,10 +237,10 @@ public class ConnectorFacadeProxy implements Connector {
             final ObjectClass objectClass,
             final Uid uid,
             final OperationOptions options,
-            final Boolean[] propagationAttempted) {
+            final AtomicReference<Boolean> propagationAttempted) {
 
         if 
(connInstance.getCapabilities().contains(ConnectorCapability.DELETE)) {
-            propagationAttempted[0] = true;
+            propagationAttempted.set(true);
 
             Future<Uid> future = asyncFacade.delete(connector, objectClass, 
uid, options);
 

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/AbstractInterruptableJob.java
----------------------------------------------------------------------
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/AbstractInterruptableJob.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/AbstractInterruptableJob.java
index 19bbf1e..86a7f49 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/AbstractInterruptableJob.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/AbstractInterruptableJob.java
@@ -20,6 +20,7 @@ package org.apache.syncope.core.provisioning.java.job;
 
 import java.util.Date;
 import java.util.concurrent.atomic.AtomicReference;
+import org.apache.syncope.core.provisioning.api.job.JobDelegate;
 import org.apache.syncope.core.provisioning.api.utils.FormatUtils;
 import org.apache.syncope.core.provisioning.api.job.JobManager;
 import org.quartz.DisallowConcurrentExecution;
@@ -40,13 +41,25 @@ public abstract class AbstractInterruptableJob implements 
InterruptableJob {
      */
     private final AtomicReference<Thread> runningThread = new 
AtomicReference<>();
 
+    private final JobDelegate embeddedDelegate = new JobDelegate() {
+
+        @Override
+        public String currentStatus() {
+            return "RUNNING THREAD: " + runningThread.get();
+        }
+    };
+
     private long interruptMaxRetries = 1;
 
+    public JobDelegate getDelegate() {
+        return embeddedDelegate;
+    }
+
     @Override
     public void execute(final JobExecutionContext context) throws 
JobExecutionException {
-        this.runningThread.set(Thread.currentThread());
+        runningThread.set(Thread.currentThread());
         try {
-            this.interruptMaxRetries = 
context.getMergedJobDataMap().getLong(JobManager.INTERRUPT_MAX_RETRIES_KEY);
+            interruptMaxRetries = 
context.getMergedJobDataMap().getLong(JobManager.INTERRUPT_MAX_RETRIES_KEY);
         } catch (Exception e) {
             LOG.debug("Could not set {}, defaults to {}", 
JobManager.INTERRUPT_MAX_RETRIES_KEY, interruptMaxRetries, e);
         }
@@ -54,7 +67,7 @@ public abstract class AbstractInterruptableJob implements 
InterruptableJob {
 
     @Override
     public void interrupt() throws UnableToInterruptJobException {
-        Thread thread = this.runningThread.getAndSet(null);
+        Thread thread = runningThread.getAndSet(null);
         if (thread == null) {
             LOG.warn("Unable to retrieve the thread of the current job 
execution");
         } else {
@@ -68,7 +81,7 @@ public abstract class AbstractInterruptableJob implements 
InterruptableJob {
             }
             // if the thread is still alive, it should be available in the 
next stop
             if (thread.isAlive()) {
-                this.runningThread.set(thread);
+                runningThread.set(thread);
             }
         }
     }

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/AbstractSchedTaskJobDelegate.java
----------------------------------------------------------------------
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/AbstractSchedTaskJobDelegate.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/AbstractSchedTaskJobDelegate.java
index 5fc5405..7103b7b 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/AbstractSchedTaskJobDelegate.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/AbstractSchedTaskJobDelegate.java
@@ -19,6 +19,7 @@
 package org.apache.syncope.core.provisioning.java.job;
 
 import java.util.Date;
+import java.util.concurrent.atomic.AtomicReference;
 import org.apache.syncope.common.lib.types.AuditElements;
 import org.apache.syncope.core.provisioning.api.utils.ExceptionUtils2;
 import org.apache.syncope.core.persistence.api.dao.TaskDAO;
@@ -72,6 +73,13 @@ public abstract class AbstractSchedTaskJobDelegate 
implements SchedTaskJobDelega
     @Autowired
     protected AuditManager auditManager;
 
+    protected final AtomicReference<String> status = new AtomicReference<>();
+
+    @Override
+    public String currentStatus() {
+        return status.get();
+    }
+
     @Transactional
     @Override
     public void execute(final String taskKey, final boolean dryRun, final 
JobExecutionContext context)
@@ -90,6 +98,8 @@ public abstract class AbstractSchedTaskJobDelegate implements 
SchedTaskJobDelega
         execution.setStart(new Date());
         execution.setTask(task);
 
+        status.set("Initialization completed");
+
         AuditElements.Result result;
 
         try {
@@ -110,6 +120,8 @@ public abstract class AbstractSchedTaskJobDelegate 
implements SchedTaskJobDelega
         }
         task = taskDAO.save(task);
 
+        status.set("Done");
+
         notificationManager.createTasks(
                 AuditElements.EventCategoryType.TASK,
                 this.getClass().getSimpleName(),

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/GroupMemberProvisionTaskJobDelegate.java
----------------------------------------------------------------------
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/GroupMemberProvisionTaskJobDelegate.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/GroupMemberProvisionTaskJobDelegate.java
index 4868bbc..4f1a1eb 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/GroupMemberProvisionTaskJobDelegate.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/GroupMemberProvisionTaskJobDelegate.java
@@ -82,20 +82,25 @@ public class GroupMemberProvisionTaskJobDelegate extends 
AbstractSchedTaskJobDel
         }
         result.append("provision\n\n");
 
+        status.set(result.toString());
+
         MembershipCond membershipCond = new MembershipCond();
         membershipCond.setGroup(groupKey);
         List<User> users = 
searchDAO.search(SearchCond.getLeafCond(membershipCond), AnyTypeKind.USER);
         Collection<String> groupResourceKeys = 
groupDAO.findAllResourceKeys(groupKey);
+        status.set("About to "
+                + (actionType == BulkMembersActionType.DEPROVISION ? "de" : 
"") + "provision "
+                + users.size() + " users from " + groupResourceKeys);
         for (User user : users) {
             List<PropagationStatus> statuses = actionType == 
BulkMembersActionType.DEPROVISION
                     ? userProvisioningManager.deprovision(user.getKey(), 
groupResourceKeys, false)
                     : userProvisioningManager.provision(user.getKey(), true, 
null, groupResourceKeys, false);
-            for (PropagationStatus status : statuses) {
+            for (PropagationStatus propagationStatus : statuses) {
                 result.append("User ").append(user.getKey()).append('\t').
-                        append("Resource 
").append(status.getResource()).append('\t').
-                        append(status.getStatus());
-                if (StringUtils.isNotBlank(status.getFailureReason())) {
-                    
result.append('\n').append(status.getFailureReason()).append('\n');
+                        append("Resource 
").append(propagationStatus.getResource()).append('\t').
+                        append(propagationStatus.getStatus());
+                if 
(StringUtils.isNotBlank(propagationStatus.getFailureReason())) {
+                    
result.append('\n').append(propagationStatus.getFailureReason()).append('\n');
                 }
                 result.append("\n");
             }
@@ -105,17 +110,20 @@ public class GroupMemberProvisionTaskJobDelegate extends 
AbstractSchedTaskJobDel
         membershipCond = new MembershipCond();
         membershipCond.setGroup(groupKey);
         List<AnyObject> anyObjects = 
searchDAO.search(SearchCond.getLeafCond(membershipCond), 
AnyTypeKind.ANY_OBJECT);
+        status.set("About to "
+                + (actionType == BulkMembersActionType.DEPROVISION ? "de" : 
"") + "provision "
+                + anyObjects.size() + " any objects from " + 
groupResourceKeys);
         for (AnyObject anyObject : anyObjects) {
             List<PropagationStatus> statuses = actionType == 
BulkMembersActionType.DEPROVISION
                     ? 
anyObjectProvisioningManager.deprovision(anyObject.getKey(), groupResourceKeys, 
false)
                     : 
anyObjectProvisioningManager.provision(anyObject.getKey(), groupResourceKeys, 
false);
 
-            for (PropagationStatus status : statuses) {
+            for (PropagationStatus propagationStatus : statuses) {
                 result.append(anyObject.getType().getKey()).append(' 
').append(anyObject.getKey()).append('\t').
-                        append("Resource 
").append(status.getResource()).append('\t').
-                        append(status.getStatus());
-                if (StringUtils.isNotBlank(status.getFailureReason())) {
-                    
result.append('\n').append(status.getFailureReason()).append('\n');
+                        append("Resource 
").append(propagationStatus.getResource()).append('\t').
+                        append(propagationStatus.getStatus());
+                if 
(StringUtils.isNotBlank(propagationStatus.getFailureReason())) {
+                    
result.append('\n').append(propagationStatus.getFailureReason()).append('\n');
                 }
                 result.append("\n");
             }

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/IdentityRecertification.java
----------------------------------------------------------------------
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/IdentityRecertification.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/IdentityRecertification.java
index 8b3f4e5..332af19 100755
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/IdentityRecertification.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/IdentityRecertification.java
@@ -87,8 +87,15 @@ public class IdentityRecertification extends 
AbstractSchedTaskJobDelegate {
             return "DRY RUN";
         }
 
+        int total = userDAO.count();
+        int pages = (total / AnyDAO.DEFAULT_PAGE_SIZE) + 1;
+
+        status.set("Processing " + total + " users in " + pages + " pages");
+
         long now = System.currentTimeMillis();
-        for (int page = 1; page <= (userDAO.count() / 
AnyDAO.DEFAULT_PAGE_SIZE) + 1; page++) {
+        for (int page = 1; page <= pages; page++) {
+            status.set("Processing " + total + " users: page " + page + " of " 
+ pages);
+
             for (User user : userDAO.findAll(page, AnyDAO.DEFAULT_PAGE_SIZE)) {
                 LOG.debug("Processing user: {}", user.getUsername());
 

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/TaskJob.java
----------------------------------------------------------------------
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/TaskJob.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/TaskJob.java
index 041d3b4..d688179 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/TaskJob.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/TaskJob.java
@@ -19,6 +19,7 @@
 package org.apache.syncope.core.provisioning.java.job;
 
 import org.apache.commons.lang3.ClassUtils;
+import org.apache.syncope.core.provisioning.api.job.JobDelegate;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.apache.syncope.core.spring.ApplicationContextProvider;
 import org.apache.syncope.core.provisioning.api.job.SchedTaskJobDelegate;
@@ -52,6 +53,8 @@ public class TaskJob extends AbstractInterruptableJob {
      */
     private String taskKey;
 
+    private SchedTaskJobDelegate delegate;
+
     /**
      * Task key setter.
      *
@@ -62,6 +65,11 @@ public class TaskJob extends AbstractInterruptableJob {
     }
 
     @Override
+    public JobDelegate getDelegate() {
+        return delegate;
+    }
+
+    @Override
     public void execute(final JobExecutionContext context) throws 
JobExecutionException {
         super.execute(context);
 
@@ -75,11 +83,12 @@ public class TaskJob extends AbstractInterruptableJob {
                         Class<?> delegateClass =
                                 
ClassUtils.getClass(context.getMergedJobDataMap().getString(DELEGATE_CLASS_KEY));
 
-                        ((SchedTaskJobDelegate) 
ApplicationContextProvider.getBeanFactory().
-                                createBean(delegateClass, 
AbstractBeanDefinition.AUTOWIRE_BY_NAME, false)).
-                                execute(taskKey,
-                                        
context.getMergedJobDataMap().getBoolean(DRY_RUN_JOBDETAIL_KEY),
-                                        context);
+                        delegate = ((SchedTaskJobDelegate) 
ApplicationContextProvider.getBeanFactory().
+                                createBean(delegateClass, 
AbstractBeanDefinition.AUTOWIRE_BY_NAME, false));
+                        delegate.execute(
+                                taskKey,
+                                
context.getMergedJobDataMap().getBoolean(DRY_RUN_JOBDETAIL_KEY),
+                                context);
                     } catch (Exception e) {
                         LOG.error("While executing task {}", taskKey, e);
                         throw new RuntimeException(e);

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/notification/DefaultNotificationJobDelegate.java
----------------------------------------------------------------------
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/notification/DefaultNotificationJobDelegate.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/notification/DefaultNotificationJobDelegate.java
new file mode 100644
index 0000000..7ab218b
--- /dev/null
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/notification/DefaultNotificationJobDelegate.java
@@ -0,0 +1,296 @@
+/*
+ * 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.job.notification;
+
+import java.io.PrintStream;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Properties;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.mail.Session;
+import javax.mail.internet.MimeMessage;
+import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.common.lib.LogOutputStream;
+import org.apache.syncope.common.lib.PropertyUtils;
+import org.apache.syncope.common.lib.types.AuditElements;
+import org.apache.syncope.common.lib.types.TaskType;
+import org.apache.syncope.common.lib.types.TraceLevel;
+import org.apache.syncope.core.provisioning.api.utils.ExceptionUtils2;
+import org.apache.syncope.core.persistence.api.dao.TaskDAO;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.task.NotificationTask;
+import org.apache.syncope.core.persistence.api.entity.task.TaskExec;
+import org.apache.syncope.core.provisioning.api.AuditManager;
+import 
org.apache.syncope.core.provisioning.api.notification.NotificationJobDelegate;
+import 
org.apache.syncope.core.provisioning.api.notification.NotificationManager;
+import org.apache.syncope.core.spring.security.Encryptor;
+import org.quartz.JobExecutionException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.JavaMailSenderImpl;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+@Component
+public class DefaultNotificationJobDelegate implements InitializingBean, 
NotificationJobDelegate {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(NotificationJobDelegate.class);
+
+    @Autowired
+    private TaskDAO taskDAO;
+
+    @Autowired
+    private JavaMailSender mailSender;
+
+    @Autowired
+    private EntityFactory entityFactory;
+
+    @Autowired
+    private AuditManager auditManager;
+
+    @Autowired
+    private NotificationManager notificationManager;
+
+    private final AtomicReference<String> status = new AtomicReference<>();
+
+    @Override
+    public void afterPropertiesSet() throws Exception {
+        if (mailSender instanceof JavaMailSenderImpl) {
+            JavaMailSenderImpl javaMailSender = (JavaMailSenderImpl) 
mailSender;
+
+            Properties javaMailProperties = 
javaMailSender.getJavaMailProperties();
+
+            Properties props = PropertyUtils.read(Encryptor.class, 
"mail.properties", "conf.directory").getLeft();
+            for (Enumeration<?> e = props.propertyNames(); 
e.hasMoreElements();) {
+                String prop = (String) e.nextElement();
+                if (prop.startsWith("mail.smtp.")) {
+                    javaMailProperties.setProperty(prop, 
props.getProperty(prop));
+                }
+            }
+
+            if (StringUtils.isNotBlank(javaMailSender.getUsername())) {
+                javaMailProperties.setProperty("mail.smtp.auth", "true");
+            }
+
+            javaMailSender.setJavaMailProperties(javaMailProperties);
+
+            String mailDebug = props.getProperty("mail.debug", "false");
+            if (BooleanUtils.toBoolean(mailDebug)) {
+                Session session = javaMailSender.getSession();
+                session.setDebug(true);
+                session.setDebugOut(new PrintStream(new LogOutputStream(LOG)));
+            }
+        }
+    }
+
+    @Override
+    public String currentStatus() {
+        return status.get();
+    }
+
+    @Transactional
+    @Override
+    public TaskExec executeSingle(final NotificationTask task) {
+        TaskExec execution = entityFactory.newEntity(TaskExec.class);
+        execution.setTask(task);
+        execution.setStart(new Date());
+
+        boolean retryPossible = true;
+
+        if (StringUtils.isBlank(task.getSubject()) || 
task.getRecipients().isEmpty()
+                || StringUtils.isBlank(task.getHtmlBody()) || 
StringUtils.isBlank(task.getTextBody())) {
+
+            String message = "Could not fetch all required information for 
sending e-mails:\n"
+                    + task.getRecipients() + "\n"
+                    + task.getSender() + "\n"
+                    + task.getSubject() + "\n"
+                    + task.getHtmlBody() + "\n"
+                    + task.getTextBody();
+            LOG.error(message);
+
+            execution.setStatus(NotificationJob.Status.NOT_SENT.name());
+            retryPossible = false;
+
+            if (task.getTraceLevel().ordinal() >= 
TraceLevel.FAILURES.ordinal()) {
+                execution.setMessage(message);
+            }
+        } else {
+            if (LOG.isDebugEnabled()) {
+                LOG.debug("About to send e-mails:\n"
+                        + task.getRecipients() + "\n"
+                        + task.getSender() + "\n"
+                        + task.getSubject() + "\n"
+                        + task.getHtmlBody() + "\n"
+                        + task.getTextBody() + "\n");
+            }
+
+            status.set("Sending notifications to " + task.getRecipients());
+
+            for (String to : task.getRecipients()) {
+                try {
+                    MimeMessage message = mailSender.createMimeMessage();
+                    MimeMessageHelper helper = new MimeMessageHelper(message, 
true);
+                    helper.setTo(to);
+                    helper.setFrom(task.getSender());
+                    helper.setSubject(task.getSubject());
+                    helper.setText(task.getTextBody(), task.getHtmlBody());
+
+                    mailSender.send(message);
+
+                    execution.setStatus(NotificationJob.Status.SENT.name());
+
+                    StringBuilder report = new StringBuilder();
+                    switch (task.getTraceLevel()) {
+                        case ALL:
+                            report.append("FROM: 
").append(task.getSender()).append('\n').
+                                    append("TO: ").append(to).append('\n').
+                                    append("SUBJECT: 
").append(task.getSubject()).append('\n').append('\n').
+                                    
append(task.getTextBody()).append('\n').append('\n').
+                                    append(task.getHtmlBody()).append('\n');
+                            break;
+
+                        case SUMMARY:
+                            report.append("E-mail sent to 
").append(to).append('\n');
+                            break;
+
+                        case FAILURES:
+                        case NONE:
+                        default:
+                    }
+                    if (report.length() > 0) {
+                        execution.setMessage(report.toString());
+                    }
+
+                    notificationManager.createTasks(
+                            AuditElements.EventCategoryType.TASK,
+                            "notification",
+                            null,
+                            "send",
+                            AuditElements.Result.SUCCESS,
+                            null,
+                            null,
+                            task,
+                            "Successfully sent notification to " + to);
+                } catch (Exception e) {
+                    LOG.error("Could not send e-mail", e);
+
+                    
execution.setStatus(NotificationJob.Status.NOT_SENT.name());
+                    if (task.getTraceLevel().ordinal() >= 
TraceLevel.FAILURES.ordinal()) {
+                        
execution.setMessage(ExceptionUtils2.getFullStackTrace(e));
+                    }
+
+                    notificationManager.createTasks(
+                            AuditElements.EventCategoryType.TASK,
+                            "notification",
+                            null,
+                            "send",
+                            AuditElements.Result.FAILURE,
+                            null,
+                            null,
+                            task,
+                            "Could not send notification to " + to, e);
+                }
+
+                execution.setEnd(new Date());
+            }
+        }
+
+        if (hasToBeRegistered(execution)) {
+            execution = notificationManager.storeExec(execution);
+            if (retryPossible
+                    && (NotificationJob.Status.valueOf(execution.getStatus()) 
== NotificationJob.Status.NOT_SENT)) {
+
+                handleRetries(execution);
+            }
+        } else {
+            notificationManager.setTaskExecuted(execution.getTask().getKey(), 
true);
+        }
+
+        return execution;
+    }
+
+    @Transactional
+    @Override
+    public void execute() throws JobExecutionException {
+        List<NotificationTask> tasks = 
taskDAO.<NotificationTask>findToExec(TaskType.NOTIFICATION);
+
+        status.set("Sending out " + tasks.size() + " notifications");
+
+        for (NotificationTask task : tasks) {
+            LOG.debug("Found notification task {} to be executed: 
starting...", task);
+            executeSingle(task);
+            LOG.debug("Notification task {} executed", task);
+        }
+    }
+
+    private boolean hasToBeRegistered(final TaskExec execution) {
+        NotificationTask task = (NotificationTask) execution.getTask();
+
+        // True if either failed and failures have to be registered, or if ALL
+        // has to be registered.
+        return (NotificationJob.Status.valueOf(execution.getStatus()) == 
NotificationJob.Status.NOT_SENT
+                && task.getTraceLevel().ordinal() >= 
TraceLevel.FAILURES.ordinal())
+                || task.getTraceLevel() == TraceLevel.ALL;
+    }
+
+    private void handleRetries(final TaskExec execution) {
+        if (notificationManager.getMaxRetries() <= 0) {
+            return;
+        }
+
+        long failedExecutionsCount = 
notificationManager.countExecutionsWithStatus(
+                execution.getTask().getKey(), 
NotificationJob.Status.NOT_SENT.name());
+
+        if (failedExecutionsCount <= notificationManager.getMaxRetries()) {
+            LOG.debug("Execution of notification task {} will be retried 
[{}/{}]",
+                    execution.getTask(), failedExecutionsCount, 
notificationManager.getMaxRetries());
+            notificationManager.setTaskExecuted(execution.getTask().getKey(), 
false);
+
+            auditManager.audit(
+                    AuditElements.EventCategoryType.TASK,
+                    "notification",
+                    null,
+                    "retry",
+                    AuditElements.Result.SUCCESS,
+                    null,
+                    null,
+                    execution,
+                    "Notification task " + execution.getTask().getKey() + " 
will be retried");
+        } else {
+            LOG.error("Maximum number of retries reached for task {} - giving 
up", execution.getTask());
+
+            auditManager.audit(
+                    AuditElements.EventCategoryType.TASK,
+                    "notification",
+                    null,
+                    "retry",
+                    AuditElements.Result.FAILURE,
+                    null,
+                    null,
+                    execution,
+                    "Giving up retries on notification task " + 
execution.getTask().getKey());
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/799f079f/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/notification/NotificationJob.java
----------------------------------------------------------------------
diff --git 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/notification/NotificationJob.java
 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/notification/NotificationJob.java
index 153a221..7edcce2 100644
--- 
a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/notification/NotificationJob.java
+++ 
b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/notification/NotificationJob.java
@@ -20,6 +20,8 @@ package 
org.apache.syncope.core.provisioning.java.job.notification;
 
 import org.apache.syncope.core.spring.security.AuthContextUtils;
 import org.apache.syncope.core.persistence.api.DomainsHolder;
+import org.apache.syncope.core.provisioning.api.job.JobDelegate;
+import 
org.apache.syncope.core.provisioning.api.notification.NotificationJobDelegate;
 import org.apache.syncope.core.provisioning.java.job.AbstractInterruptableJob;
 import org.quartz.JobExecutionContext;
 import org.quartz.JobExecutionException;
@@ -54,6 +56,11 @@ public class NotificationJob extends 
AbstractInterruptableJob {
     private NotificationJobDelegate delegate;
 
     @Override
+    public JobDelegate getDelegate() {
+        return delegate;
+    }
+
+    @Override
     public void execute(final JobExecutionContext context) throws 
JobExecutionException {
         super.execute(context);
 

Reply via email to