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 2017/06/06 13:06:28 UTC

[02/10] syncope git commit: [SYNCOPE-1103] Option to put Quartz scheduler in standby, from provisioning.properties

http://git-wip-us.apache.org/repos/asf/syncope/blob/67ecbea3/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/notification/NotificationJobDelegate.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/notification/NotificationJobDelegate.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/notification/NotificationJobDelegate.java
new file mode 100644
index 0000000..0d37d86
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/notification/NotificationJobDelegate.java
@@ -0,0 +1,268 @@
+/*
+ * 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.util.Date;
+import java.util.Properties;
+import javax.mail.internet.MimeMessage;
+import org.apache.commons.lang3.StringUtils;
+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.event.AfterHandlingEvent;
+import org.apache.syncope.core.provisioning.api.notification.NotificationManager;
+import org.quartz.JobExecutionException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationEventPublisher;
+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 NotificationJobDelegate {
+
+    private static final Logger LOG = LoggerFactory.getLogger(NotificationJobDelegate.class);
+
+    /**
+     * Task DAO.
+     */
+    @Autowired
+    private TaskDAO taskDAO;
+
+    @Autowired
+    private JavaMailSender mailSender;
+
+    @Autowired
+    private EntityFactory entityFactory;
+
+    @Autowired
+    private AuditManager auditManager;
+
+    @Autowired
+    private NotificationManager notificationManager;
+
+    @Autowired
+    private ApplicationEventPublisher publisher;
+
+    private long maxRetries;
+
+    private void init() {
+        maxRetries = notificationManager.getMaxRetries();
+
+        if (mailSender instanceof JavaMailSenderImpl
+                && StringUtils.isNotBlank(((JavaMailSenderImpl) mailSender).getUsername())) {
+
+            Properties javaMailProperties = ((JavaMailSenderImpl) mailSender).getJavaMailProperties();
+            javaMailProperties.setProperty("mail.smtp.auth", "true");
+            ((JavaMailSenderImpl) mailSender).setJavaMailProperties(javaMailProperties);
+        }
+    }
+
+    @Transactional
+    public TaskExec executeSingle(final NotificationTask task) {
+        init();
+
+        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");
+            }
+
+            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());
+                    }
+
+                    publisher.publishEvent(new AfterHandlingEvent(this,
+                            true,
+                            true,
+                            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));
+                    }
+
+                    publisher.publishEvent(new AfterHandlingEvent(this,
+                            true,
+                            true,
+                            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
+    public void execute() throws JobExecutionException {
+        for (NotificationTask task : taskDAO.<NotificationTask>findToExec(TaskType.NOTIFICATION)) {
+            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 (maxRetries <= 0) {
+            return;
+        }
+
+        long failedExecutionsCount = notificationManager.countExecutionsWithStatus(
+                execution.getTask().getKey(), NotificationJob.Status.NOT_SENT.name());
+
+        if (failedExecutionsCount <= maxRetries) {
+            LOG.debug("Execution of notification task {} will be retried [{}/{}]",
+                    execution.getTask(), failedExecutionsCount, maxRetries);
+            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/67ecbea3/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/AbstractReportlet.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/AbstractReportlet.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/AbstractReportlet.java
new file mode 100644
index 0000000..2c85b20
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/AbstractReportlet.java
@@ -0,0 +1,52 @@
+/*
+ * 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.report;
+
+import org.apache.syncope.core.persistence.api.dao.Reportlet;
+import org.apache.syncope.common.lib.report.ReportletConf;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.transaction.annotation.Transactional;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.AttributesImpl;
+
+public abstract class AbstractReportlet implements Reportlet {
+
+    protected static final Logger LOG = LoggerFactory.getLogger(AbstractReportlet.class);
+
+    protected abstract void doExtract(ReportletConf conf, ContentHandler handler) throws SAXException;
+
+    @Override
+    @Transactional(readOnly = true)
+    public void extract(final ReportletConf conf, final ContentHandler handler) throws SAXException {
+        if (conf == null) {
+            throw new ReportException(new IllegalArgumentException("No configuration provided"));
+        }
+
+        AttributesImpl atts = new AttributesImpl();
+        atts.addAttribute("", "", ReportXMLConst.ATTR_NAME, ReportXMLConst.XSD_STRING, conf.getName());
+        atts.addAttribute("", "", ReportXMLConst.ATTR_CLASS, ReportXMLConst.XSD_STRING, getClass().getName());
+        handler.startElement("", "", ReportXMLConst.ELEMENT_REPORTLET, atts);
+
+        doExtract(conf, handler);
+
+        handler.endElement("", "", ReportXMLConst.ELEMENT_REPORTLET);
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/67ecbea3/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/AuditReportlet.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/AuditReportlet.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/AuditReportlet.java
new file mode 100644
index 0000000..96039f0
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/AuditReportlet.java
@@ -0,0 +1,141 @@
+/*
+ * 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.report;
+
+import java.util.List;
+import java.util.Map;
+import javax.sql.DataSource;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.apache.commons.lang3.builder.ToStringStyle;
+import org.apache.syncope.common.lib.report.AuditReportletConf;
+import org.apache.syncope.common.lib.report.ReportletConf;
+import org.apache.syncope.core.provisioning.java.AuditEntry;
+import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
+import org.apache.syncope.core.persistence.api.DomainsHolder;
+import org.apache.syncope.core.persistence.api.dao.ReportletConfClass;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.AttributesImpl;
+
+@ReportletConfClass(AuditReportletConf.class)
+public class AuditReportlet extends AbstractReportlet {
+
+    @Autowired
+    private DomainsHolder domainsHolder;
+
+    private AuditReportletConf conf;
+
+    private DataSource datasource;
+
+    private void doExtractConf(final ContentHandler handler) throws SAXException {
+        JdbcTemplate jdbcTemplate = new JdbcTemplate(datasource);
+        jdbcTemplate.setMaxRows(conf.getSize());
+        List<Map<String, Object>> rows = jdbcTemplate.
+                queryForList("SELECT * FROM SYNCOPEAUDIT ORDER BY EVENT_DATE DESC");
+
+        handler.startElement("", "", "events", null);
+        AttributesImpl atts = new AttributesImpl();
+        for (Map<String, Object> row : rows) {
+            AuditEntry auditEntry = POJOHelper.deserialize(row.get("MESSAGE").toString(), AuditEntry.class);
+
+            atts.clear();
+            if (StringUtils.isNotBlank(auditEntry.getWho())) {
+                atts.addAttribute("", "", "who", ReportXMLConst.XSD_STRING, auditEntry.getWho());
+            }
+            handler.startElement("", "", "event", atts);
+
+            atts.clear();
+            if (StringUtils.isNotBlank(auditEntry.getLogger().getCategory())) {
+                atts.addAttribute("", "", "category",
+                        ReportXMLConst.XSD_STRING, auditEntry.getLogger().getCategory());
+            }
+            if (StringUtils.isNotBlank(auditEntry.getLogger().getSubcategory())) {
+                atts.addAttribute("", "", "subcategory",
+                        ReportXMLConst.XSD_STRING, auditEntry.getLogger().getSubcategory());
+            }
+            if (StringUtils.isNotBlank(auditEntry.getLogger().getEvent())) {
+                atts.addAttribute("", "", "event",
+                        ReportXMLConst.XSD_STRING, auditEntry.getLogger().getEvent());
+            }
+            if (auditEntry.getLogger().getResult() != null) {
+                atts.addAttribute("", "", "result",
+                        ReportXMLConst.XSD_STRING, auditEntry.getLogger().getResult().name());
+            }
+            handler.startElement("", "", "logger", atts);
+            handler.endElement("", "", "logger");
+
+            if (auditEntry.getBefore() != null) {
+                char[] before = ToStringBuilder.reflectionToString(
+                        auditEntry.getBefore(), ToStringStyle.MULTI_LINE_STYLE).toCharArray();
+                handler.startElement("", "", "before", null);
+                handler.characters(before, 0, before.length);
+                handler.endElement("", "", "before");
+            }
+
+            if (auditEntry.getInput() != null) {
+                handler.startElement("", "", "inputs", null);
+                for (Object inputObj : auditEntry.getInput()) {
+                    char[] input = ToStringBuilder.reflectionToString(
+                            inputObj, ToStringStyle.MULTI_LINE_STYLE).toCharArray();
+                    handler.startElement("", "", "input", null);
+                    handler.characters(input, 0, input.length);
+                    handler.endElement("", "", "input");
+                }
+                handler.endElement("", "", "inputs");
+            }
+
+            if (auditEntry.getOutput() != null) {
+                char[] output = ToStringBuilder.reflectionToString(
+                        auditEntry.getOutput(), ToStringStyle.MULTI_LINE_STYLE).toCharArray();
+                handler.startElement("", "", "output", null);
+                handler.characters(output, 0, output.length);
+                handler.endElement("", "", "output");
+            }
+
+            handler.startElement("", "", "throwable", null);
+            char[] throwable = row.get("THROWABLE").toString().toCharArray();
+            handler.characters(throwable, 0, throwable.length);
+            handler.endElement("", "", "throwable");
+
+            handler.endElement("", "", "event");
+        }
+        handler.endElement("", "", "events");
+    }
+
+    @Override
+    protected void doExtract(final ReportletConf conf, final ContentHandler handler) throws SAXException {
+        if (conf instanceof AuditReportletConf) {
+            this.conf = AuditReportletConf.class.cast(conf);
+        } else {
+            throw new ReportException(new IllegalArgumentException("Invalid configuration provided"));
+        }
+
+        datasource = domainsHolder.getDomains().get(AuthContextUtils.getDomain());
+        if (datasource == null) {
+            throw new ReportException(new IllegalArgumentException("Could not get to DataSource"));
+        }
+
+        doExtractConf(handler);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/67ecbea3/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/GroupReportlet.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/GroupReportlet.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/GroupReportlet.java
new file mode 100644
index 0000000..fa558b6
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/GroupReportlet.java
@@ -0,0 +1,316 @@
+/*
+ * 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.report;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.report.GroupReportletConf;
+import org.apache.syncope.common.lib.report.GroupReportletConf.Feature;
+import org.apache.syncope.common.lib.report.ReportletConf;
+import org.apache.syncope.common.lib.to.AnyTO;
+import org.apache.syncope.common.lib.to.AttrTO;
+import org.apache.syncope.common.lib.to.GroupTO;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.core.persistence.api.dao.AnyDAO;
+import org.apache.syncope.core.persistence.api.dao.GroupDAO;
+import org.apache.syncope.core.persistence.api.dao.search.OrderByClause;
+import org.apache.syncope.core.persistence.api.entity.group.Group;
+import org.apache.syncope.core.persistence.api.search.SearchCondConverter;
+import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
+import org.apache.syncope.core.persistence.api.dao.ReportletConfClass;
+import org.apache.syncope.core.persistence.api.entity.user.UMembership;
+import org.apache.syncope.core.provisioning.api.data.GroupDataBinder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.AttributesImpl;
+
+@ReportletConfClass(GroupReportletConf.class)
+public class GroupReportlet extends AbstractReportlet {
+
+    @Autowired
+    private GroupDAO groupDAO;
+
+    @Autowired
+    private AnySearchDAO searchDAO;
+
+    @Autowired
+    private GroupDataBinder groupDataBinder;
+
+    private GroupReportletConf conf;
+
+    private void doExtractResources(final ContentHandler handler, final AnyTO anyTO)
+            throws SAXException {
+
+        if (anyTO.getResources().isEmpty()) {
+            LOG.debug("No resources found for {}[{}]", anyTO.getClass().getSimpleName(), anyTO.getKey());
+        } else {
+            AttributesImpl atts = new AttributesImpl();
+            handler.startElement("", "", "resources", null);
+
+            for (String resourceName : anyTO.getResources()) {
+                atts.clear();
+
+                atts.addAttribute("", "", ReportXMLConst.ATTR_NAME, ReportXMLConst.XSD_STRING, resourceName);
+                handler.startElement("", "", "resource", atts);
+                handler.endElement("", "", "resource");
+            }
+
+            handler.endElement("", "", "resources");
+        }
+    }
+
+    private void doExtractAttributes(final ContentHandler handler, final AnyTO anyTO,
+            final Collection<String> attrs, final Collection<String> derAttrs, final Collection<String> virAttrs)
+            throws SAXException {
+
+        AttributesImpl atts = new AttributesImpl();
+        if (!attrs.isEmpty()) {
+            Map<String, AttrTO> attrMap = anyTO.getPlainAttrMap();
+
+            handler.startElement("", "", "attributes", null);
+            for (String attrName : attrs) {
+                atts.clear();
+
+                atts.addAttribute("", "", ReportXMLConst.ATTR_NAME, ReportXMLConst.XSD_STRING, attrName);
+                handler.startElement("", "", "attribute", atts);
+
+                if (attrMap.containsKey(attrName)) {
+                    for (String value : attrMap.get(attrName).getValues()) {
+                        handler.startElement("", "", "value", null);
+                        handler.characters(value.toCharArray(), 0, value.length());
+                        handler.endElement("", "", "value");
+                    }
+                } else {
+                    LOG.debug("{} not found for {}[{}]", attrName,
+                            anyTO.getClass().getSimpleName(), anyTO.getKey());
+                }
+
+                handler.endElement("", "", "attribute");
+            }
+            handler.endElement("", "", "attributes");
+        }
+
+        if (!derAttrs.isEmpty()) {
+            Map<String, AttrTO> derAttrMap = anyTO.getDerAttrMap();
+
+            handler.startElement("", "", "derivedAttributes", null);
+            for (String attrName : derAttrs) {
+                atts.clear();
+
+                atts.addAttribute("", "", ReportXMLConst.ATTR_NAME, ReportXMLConst.XSD_STRING, attrName);
+                handler.startElement("", "", "derivedAttribute", atts);
+
+                if (derAttrMap.containsKey(attrName)) {
+                    for (String value : derAttrMap.get(attrName).getValues()) {
+                        handler.startElement("", "", "value", null);
+                        handler.characters(value.toCharArray(), 0, value.length());
+                        handler.endElement("", "", "value");
+                    }
+                } else {
+                    LOG.debug("{} not found for {}[{}]", attrName,
+                            anyTO.getClass().getSimpleName(), anyTO.getKey());
+                }
+
+                handler.endElement("", "", "derivedAttribute");
+            }
+            handler.endElement("", "", "derivedAttributes");
+        }
+
+        if (!virAttrs.isEmpty()) {
+            Map<String, AttrTO> virAttrMap = anyTO.getVirAttrMap();
+
+            handler.startElement("", "", "virtualAttributes", null);
+            for (String attrName : virAttrs) {
+                atts.clear();
+
+                atts.addAttribute("", "", ReportXMLConst.ATTR_NAME, ReportXMLConst.XSD_STRING, attrName);
+                handler.startElement("", "", "virtualAttribute", atts);
+
+                if (virAttrMap.containsKey(attrName)) {
+                    for (String value : virAttrMap.get(attrName).getValues()) {
+                        handler.startElement("", "", "value", null);
+                        handler.characters(value.toCharArray(), 0, value.length());
+                        handler.endElement("", "", "value");
+                    }
+                } else {
+                    LOG.debug("{} not found for {}[{}]", attrName,
+                            anyTO.getClass().getSimpleName(), anyTO.getKey());
+                }
+
+                handler.endElement("", "", "virtualAttribute");
+            }
+            handler.endElement("", "", "virtualAttributes");
+        }
+    }
+
+    private void doExtract(final ContentHandler handler, final List<Group> groups) throws SAXException {
+        AttributesImpl atts = new AttributesImpl();
+        for (Group group : groups) {
+            atts.clear();
+
+            for (Feature feature : conf.getFeatures()) {
+                String type = null;
+                String value = null;
+                switch (feature) {
+                    case key:
+                        type = ReportXMLConst.XSD_STRING;
+                        value = group.getKey();
+                        break;
+
+                    case name:
+                        type = ReportXMLConst.XSD_STRING;
+                        value = String.valueOf(group.getName());
+                        break;
+
+                    case groupOwner:
+                        type = ReportXMLConst.XSD_STRING;
+                        value = group.getGroupOwner().getKey();
+                        break;
+
+                    case userOwner:
+                        type = ReportXMLConst.XSD_STRING;
+                        value = group.getUserOwner().getKey();
+                        break;
+
+                    default:
+                }
+
+                if (type != null && value != null) {
+                    atts.addAttribute("", "", feature.name(), type, value);
+                }
+            }
+
+            handler.startElement("", "", "group", atts);
+
+            // Using GroupTO for attribute values, since the conversion logic of
+            // values to String is already encapsulated there
+            GroupTO groupTO = groupDataBinder.getGroupTO(group, true);
+
+            doExtractAttributes(handler, groupTO, conf.getPlainAttrs(), conf.getDerAttrs(), conf.getVirAttrs());
+
+            // to get resources associated to a group
+            if (conf.getFeatures().contains(Feature.resources)) {
+                doExtractResources(handler, groupTO);
+            }
+            //to get users asscoiated to a group is preferred GroupDAO to GroupTO
+            if (conf.getFeatures().contains(Feature.users)) {
+                handler.startElement("", "", "users", null);
+
+                for (UMembership memb : groupDAO.findUMemberships(group)) {
+                    atts.clear();
+
+                    atts.addAttribute("", "", "key", ReportXMLConst.XSD_STRING,
+                            memb.getLeftEnd().getKey());
+                    atts.addAttribute("", "", "username", ReportXMLConst.XSD_STRING,
+                            String.valueOf(memb.getLeftEnd().getUsername()));
+
+                    handler.startElement("", "", "user", atts);
+                    handler.endElement("", "", "user");
+                }
+
+                handler.endElement("", "", "users");
+            }
+
+            handler.endElement("", "", "group");
+        }
+    }
+
+    private void doExtractConf(final ContentHandler handler) throws SAXException {
+        if (conf == null) {
+            LOG.debug("Report configuration is not present");
+        }
+
+        AttributesImpl atts = new AttributesImpl();
+        handler.startElement("", "", "configurations", null);
+        handler.startElement("", "", "groupAttributes", atts);
+
+        if (conf != null) {
+            for (Feature feature : conf.getFeatures()) {
+                atts.clear();
+                handler.startElement("", "", "feature", atts);
+                handler.characters(feature.name().toCharArray(), 0, feature.name().length());
+                handler.endElement("", "", "feature");
+            }
+
+            for (String attr : conf.getPlainAttrs()) {
+                atts.clear();
+                handler.startElement("", "", "attribute", atts);
+                handler.characters(attr.toCharArray(), 0, attr.length());
+                handler.endElement("", "", "attribute");
+            }
+
+            for (String derAttr : conf.getDerAttrs()) {
+                atts.clear();
+                handler.startElement("", "", "derAttribute", atts);
+                handler.characters(derAttr.toCharArray(), 0, derAttr.length());
+                handler.endElement("", "", "derAttribute");
+            }
+
+            for (String virAttr : conf.getVirAttrs()) {
+                atts.clear();
+                handler.startElement("", "", "virAttribute", atts);
+                handler.characters(virAttr.toCharArray(), 0, virAttr.length());
+                handler.endElement("", "", "virAttribute");
+            }
+        }
+
+        handler.endElement("", "", "groupAttributes");
+        handler.endElement("", "", "configurations");
+    }
+
+    private int count() {
+        return StringUtils.isBlank(conf.getMatchingCond())
+                ? groupDAO.count()
+                : searchDAO.count(SyncopeConstants.FULL_ADMIN_REALMS,
+                        SearchCondConverter.convert(conf.getMatchingCond()), AnyTypeKind.GROUP);
+    }
+
+    @Override
+    protected void doExtract(final ReportletConf conf, final ContentHandler handler) throws SAXException {
+        if (conf instanceof GroupReportletConf) {
+            this.conf = GroupReportletConf.class.cast(conf);
+        } else {
+            throw new ReportException(new IllegalArgumentException("Invalid configuration provided"));
+        }
+
+        doExtractConf(handler);
+
+        for (int page = 1; page <= (count() / AnyDAO.DEFAULT_PAGE_SIZE) + 1; page++) {
+            List<Group> groups;
+            if (StringUtils.isBlank(this.conf.getMatchingCond())) {
+                groups = groupDAO.findAll(page, AnyDAO.DEFAULT_PAGE_SIZE);
+            } else {
+                groups = searchDAO.search(
+                        SyncopeConstants.FULL_ADMIN_REALMS,
+                        SearchCondConverter.convert(this.conf.getMatchingCond()),
+                        page,
+                        AnyDAO.DEFAULT_PAGE_SIZE,
+                        Collections.<OrderByClause>emptyList(),
+                        AnyTypeKind.USER);
+            }
+
+            doExtract(handler, groups);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/67ecbea3/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReconciliationReportlet.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReconciliationReportlet.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReconciliationReportlet.java
new file mode 100644
index 0000000..f39ee1c
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReconciliationReportlet.java
@@ -0,0 +1,519 @@
+/*
+ * 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.report;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import org.apache.commons.collections4.Closure;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.collections4.IterableUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.common.lib.SyncopeConstants;
+import org.apache.syncope.common.lib.report.ReconciliationReportletConf;
+import org.apache.syncope.common.lib.report.ReconciliationReportletConf.Feature;
+import org.apache.syncope.common.lib.report.ReportletConf;
+import org.apache.syncope.common.lib.types.AnyTypeKind;
+import org.apache.syncope.core.persistence.api.dao.AnyDAO;
+import org.apache.syncope.core.persistence.api.search.SearchCondConverter;
+import org.apache.syncope.core.provisioning.api.utils.FormatUtils;
+import org.apache.syncope.core.persistence.api.dao.AnySearchDAO;
+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.dao.ReportletConfClass;
+import org.apache.syncope.core.persistence.api.dao.UserDAO;
+import org.apache.syncope.core.persistence.api.dao.search.AnyTypeCond;
+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.Any;
+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.AnyUtilsFactory;
+import org.apache.syncope.core.persistence.api.entity.anyobject.AnyObject;
+import org.apache.syncope.core.persistence.api.entity.group.Group;
+import org.apache.syncope.core.persistence.api.entity.resource.ExternalResource;
+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.user.User;
+import org.apache.syncope.core.provisioning.api.Connector;
+import org.apache.syncope.core.provisioning.api.ConnectorFactory;
+import org.apache.syncope.core.provisioning.api.MappingManager;
+import org.apache.syncope.core.provisioning.java.utils.MappingUtils;
+import org.identityconnectors.common.Base64;
+import org.identityconnectors.framework.common.objects.Attribute;
+import org.identityconnectors.framework.common.objects.AttributeBuilder;
+import org.identityconnectors.framework.common.objects.ConnectorObject;
+import org.identityconnectors.framework.common.objects.OperationalAttributes;
+import org.identityconnectors.framework.common.objects.Uid;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.AttributesImpl;
+
+/**
+ * Reportlet for extracting information for the current reconciliation status, e.g. the coherence between Syncope
+ * information and mapped entities on external resources.
+ */
+@ReportletConfClass(ReconciliationReportletConf.class)
+public class ReconciliationReportlet extends AbstractReportlet {
+
+    private static final int PAGE_SIZE = 10;
+
+    @Autowired
+    private UserDAO userDAO;
+
+    @Autowired
+    private GroupDAO groupDAO;
+
+    @Autowired
+    private AnyTypeDAO anyTypeDAO;
+
+    @Autowired
+    private AnySearchDAO searchDAO;
+
+    @Autowired
+    private MappingManager mappingManager;
+
+    @Autowired
+    private ConnectorFactory connFactory;
+
+    @Autowired
+    private AnyUtilsFactory anyUtilsFactory;
+
+    private ReconciliationReportletConf conf;
+
+    private String getAnyElementName(final AnyTypeKind anyTypeKind) {
+        String elementName;
+
+        switch (anyTypeKind) {
+            case USER:
+                elementName = "user";
+                break;
+
+            case GROUP:
+                elementName = "group";
+                break;
+
+            case ANY_OBJECT:
+            default:
+                elementName = "anyObject";
+        }
+
+        return elementName;
+    }
+
+    private void doExtract(
+            final ContentHandler handler,
+            final Any<?> any,
+            final Set<Missing> missing,
+            final Set<Misaligned> misaligned)
+            throws SAXException {
+
+        AttributesImpl atts = new AttributesImpl();
+
+        for (Feature feature : conf.getFeatures()) {
+            String type = null;
+            String value = null;
+            switch (feature) {
+                case key:
+                    type = ReportXMLConst.XSD_STRING;
+                    value = any.getKey();
+                    break;
+
+                case username:
+                    if (any instanceof User) {
+                        type = ReportXMLConst.XSD_STRING;
+                        value = User.class.cast(any).getUsername();
+                    }
+                    break;
+
+                case groupName:
+                    if (any instanceof Group) {
+                        type = ReportXMLConst.XSD_STRING;
+                        value = Group.class.cast(any).getName();
+                    }
+                    break;
+
+                case workflowId:
+                    type = ReportXMLConst.XSD_STRING;
+                    value = any.getWorkflowId();
+                    break;
+
+                case status:
+                    type = ReportXMLConst.XSD_STRING;
+                    value = any.getStatus();
+                    break;
+
+                case creationDate:
+                    type = ReportXMLConst.XSD_DATETIME;
+                    value = any.getCreationDate() == null
+                            ? StringUtils.EMPTY
+                            : FormatUtils.format(any.getCreationDate());
+                    break;
+
+                case lastLoginDate:
+                    if (any instanceof User) {
+                        type = ReportXMLConst.XSD_DATETIME;
+                        value = User.class.cast(any).getLastLoginDate() == null
+                                ? StringUtils.EMPTY
+                                : FormatUtils.format(User.class.cast(any).getLastLoginDate());
+                    }
+                    break;
+
+                case changePwdDate:
+                    if (any instanceof User) {
+                        type = ReportXMLConst.XSD_DATETIME;
+                        value = User.class.cast(any).getChangePwdDate() == null
+                                ? StringUtils.EMPTY
+                                : FormatUtils.format(User.class.cast(any).getChangePwdDate());
+                    }
+                    break;
+
+                case passwordHistorySize:
+                    if (any instanceof User) {
+                        type = ReportXMLConst.XSD_INT;
+                        value = String.valueOf(User.class.cast(any).getPasswordHistory().size());
+                    }
+                    break;
+
+                case failedLoginCount:
+                    if (any instanceof User) {
+                        type = ReportXMLConst.XSD_INT;
+                        value = String.valueOf(User.class.cast(any).getFailedLogins());
+                    }
+                    break;
+
+                default:
+            }
+
+            if (type != null && value != null) {
+                atts.addAttribute("", "", feature.name(), type, value);
+            }
+        }
+
+        handler.startElement("", "", getAnyElementName(any.getType().getKind()), atts);
+
+        for (Missing item : missing) {
+            atts.clear();
+            atts.addAttribute("", "", "resource", ReportXMLConst.XSD_STRING, item.getResource());
+            atts.addAttribute("", "", "connObjectKeyValue", ReportXMLConst.XSD_STRING, item.getConnObjectKeyValue());
+
+            handler.startElement("", "", "missing", atts);
+            handler.endElement("", "", "missing");
+        }
+        for (Misaligned item : misaligned) {
+            atts.clear();
+            atts.addAttribute("", "", "resource", ReportXMLConst.XSD_STRING, item.getResource());
+            atts.addAttribute("", "", "connObjectKeyValue", ReportXMLConst.XSD_STRING, item.getConnObjectKeyValue());
+            atts.addAttribute("", "", ReportXMLConst.ATTR_NAME, ReportXMLConst.XSD_STRING, item.getName());
+
+            handler.startElement("", "", "misaligned", atts);
+
+            handler.startElement("", "", "onSyncope", null);
+            if (item.getOnSyncope() != null) {
+                for (Object value : item.getOnSyncope()) {
+                    char[] asChars = value.toString().toCharArray();
+
+                    handler.startElement("", "", "value", null);
+                    handler.characters(asChars, 0, asChars.length);
+                    handler.endElement("", "", "value");
+                }
+            }
+            handler.endElement("", "", "onSyncope");
+
+            handler.startElement("", "", "onResource", null);
+            if (item.getOnResource() != null) {
+                for (Object value : item.getOnResource()) {
+                    char[] asChars = value.toString().toCharArray();
+
+                    handler.startElement("", "", "value", null);
+                    handler.characters(asChars, 0, asChars.length);
+                    handler.endElement("", "", "value");
+                }
+            }
+            handler.endElement("", "", "onResource");
+
+            handler.endElement("", "", "misaligned");
+        }
+
+        handler.endElement("", "", getAnyElementName(any.getType().getKind()));
+    }
+
+    private Set<Object> getValues(final Attribute attr) {
+        Set<Object> values;
+        if (attr.getValue() == null || attr.getValue().isEmpty()) {
+            values = Collections.emptySet();
+        } else if (attr.getValue().get(0) instanceof byte[]) {
+            values = new HashSet<>(attr.getValue().size());
+            for (Object single : attr.getValue()) {
+                values.add(Base64.encode((byte[]) single));
+            }
+        } else {
+            values = new HashSet<>(attr.getValue());
+        }
+
+        return values;
+    }
+
+    private void doExtract(final ContentHandler handler, final List<? extends Any<?>> anys)
+            throws SAXException, ReportException {
+
+        final Set<Missing> missing = new HashSet<>();
+        final Set<Misaligned> misaligned = new HashSet<>();
+
+        for (Any<?> any : anys) {
+            missing.clear();
+            misaligned.clear();
+
+            AnyUtils anyUtils = anyUtilsFactory.getInstance(any);
+            for (final ExternalResource resource : anyUtils.getAllResources(any)) {
+                Provision provision = resource.getProvision(any.getType());
+                MappingItem connObjectKeyItem = MappingUtils.getConnObjectKeyItem(provision);
+                final String connObjectKeyValue = connObjectKeyItem == null
+                        ? StringUtils.EMPTY
+                        : mappingManager.getConnObjectKeyValue(any, provision);
+                if (provision != null && connObjectKeyItem != null && StringUtils.isNotBlank(connObjectKeyValue)) {
+                    // 1. read from the underlying connector
+                    Connector connector = connFactory.getConnector(resource);
+                    ConnectorObject connectorObject = connector.getObject(provision.getObjectClass(),
+                            new Uid(connObjectKeyValue),
+                            MappingUtils.buildOperationOptions(provision.getMapping().getItems().iterator()));
+
+                    if (connectorObject == null) {
+                        // 2. not found on resource?
+                        LOG.error("Object {} with class {} not found on resource {}",
+                                connObjectKeyValue, provision.getObjectClass(), resource);
+
+                        missing.add(new Missing(resource.getKey(), connObjectKeyValue));
+                    } else {
+                        // 3. found but misaligned?
+                        Pair<String, Set<Attribute>> preparedAttrs =
+                                mappingManager.prepareAttrs(any, null, false, null, provision);
+                        preparedAttrs.getRight().add(AttributeBuilder.build(
+                                Uid.NAME, preparedAttrs.getLeft()));
+                        preparedAttrs.getRight().add(AttributeBuilder.build(
+                                connObjectKeyItem.getExtAttrName(), preparedAttrs.getLeft()));
+
+                        final Map<String, Set<Object>> syncopeAttrs = new HashMap<>();
+                        for (Attribute attr : preparedAttrs.getRight()) {
+                            syncopeAttrs.put(attr.getName(), getValues(attr));
+                        }
+
+                        final Map<String, Set<Object>> resourceAttrs = new HashMap<>();
+                        for (Attribute attr : connectorObject.getAttributes()) {
+                            if (!OperationalAttributes.PASSWORD_NAME.equals(attr.getName())
+                                    && !OperationalAttributes.ENABLE_NAME.equals(attr.getName())) {
+
+                                resourceAttrs.put(attr.getName(), getValues(attr));
+                            }
+                        }
+
+                        IterableUtils.forEach(CollectionUtils.subtract(syncopeAttrs.keySet(), resourceAttrs.keySet()),
+                                new Closure<String>() {
+
+                            @Override
+                            public void execute(final String name) {
+                                misaligned.add(new Misaligned(
+                                        resource.getKey(),
+                                        connObjectKeyValue,
+                                        name,
+                                        syncopeAttrs.get(name),
+                                        Collections.emptySet()));
+                            }
+                        });
+
+                        for (Map.Entry<String, Set<Object>> entry : resourceAttrs.entrySet()) {
+                            if (syncopeAttrs.containsKey(entry.getKey())) {
+                                if (!Objects.equals(syncopeAttrs.get(entry.getKey()), entry.getValue())) {
+                                    misaligned.add(new Misaligned(
+                                            resource.getKey(),
+                                            connObjectKeyValue,
+                                            entry.getKey(),
+                                            syncopeAttrs.get(entry.getKey()),
+                                            entry.getValue()));
+                                }
+                            } else {
+                                misaligned.add(new Misaligned(
+                                        resource.getKey(),
+                                        connObjectKeyValue,
+                                        entry.getKey(),
+                                        Collections.emptySet(),
+                                        entry.getValue()));
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (!missing.isEmpty() || !misaligned.isEmpty()) {
+                doExtract(handler, any, missing, misaligned);
+            }
+        }
+    }
+
+    private void doExtract(
+            final ContentHandler handler, final int count, final SearchCond cond, final AnyTypeKind anyTypeKind)
+            throws SAXException {
+
+        for (int page = 1; page <= (count / PAGE_SIZE) + 1; page++) {
+            List<AnyObject> anys = searchDAO.search(
+                    SyncopeConstants.FULL_ADMIN_REALMS,
+                    cond,
+                    page,
+                    PAGE_SIZE,
+                    Collections.<OrderByClause>emptyList(),
+                    anyTypeKind);
+
+            doExtract(handler, anys);
+        }
+    }
+
+    @Override
+    protected void doExtract(final ReportletConf conf, final ContentHandler handler) throws SAXException {
+        if (conf instanceof ReconciliationReportletConf) {
+            this.conf = ReconciliationReportletConf.class.cast(conf);
+        } else {
+            throw new ReportException(new IllegalArgumentException("Invalid configuration provided"));
+        }
+
+        AttributesImpl atts = new AttributesImpl();
+
+        if (StringUtils.isBlank(this.conf.getUserMatchingCond())) {
+            atts.addAttribute("", "", "total", ReportXMLConst.XSD_INT, String.valueOf(userDAO.count()));
+            handler.startElement("", "", getAnyElementName(AnyTypeKind.USER) + "s", atts);
+
+            for (int page = 1; page <= (userDAO.count() / AnyDAO.DEFAULT_PAGE_SIZE) + 1; page++) {
+                doExtract(handler, userDAO.findAll(page, AnyDAO.DEFAULT_PAGE_SIZE));
+            }
+        } else {
+            SearchCond cond = SearchCondConverter.convert(this.conf.getUserMatchingCond());
+
+            int count = searchDAO.count(SyncopeConstants.FULL_ADMIN_REALMS, cond, AnyTypeKind.USER);
+            atts.addAttribute("", "", "total", ReportXMLConst.XSD_INT, String.valueOf(count));
+            handler.startElement("", "", getAnyElementName(AnyTypeKind.USER) + "s", atts);
+
+            doExtract(handler, count, cond, AnyTypeKind.USER);
+        }
+        handler.endElement("", "", getAnyElementName(AnyTypeKind.USER) + "s");
+
+        atts.clear();
+        if (StringUtils.isBlank(this.conf.getGroupMatchingCond())) {
+            atts.addAttribute("", "", "total", ReportXMLConst.XSD_INT, String.valueOf(groupDAO.count()));
+            handler.startElement("", "", getAnyElementName(AnyTypeKind.GROUP) + "s", atts);
+
+            for (int page = 1; page <= (groupDAO.count() / AnyDAO.DEFAULT_PAGE_SIZE) + 1; page++) {
+                doExtract(handler, groupDAO.findAll(page, AnyDAO.DEFAULT_PAGE_SIZE));
+            }
+        } else {
+            SearchCond cond = SearchCondConverter.convert(this.conf.getUserMatchingCond());
+
+            int count = searchDAO.count(SyncopeConstants.FULL_ADMIN_REALMS, cond, AnyTypeKind.GROUP);
+            atts.addAttribute("", "", "total", ReportXMLConst.XSD_INT, String.valueOf(count));
+            handler.startElement("", "", getAnyElementName(AnyTypeKind.GROUP) + "s", atts);
+
+            doExtract(handler, count, cond, AnyTypeKind.GROUP);
+        }
+        handler.endElement("", "", getAnyElementName(AnyTypeKind.GROUP) + "s");
+
+        for (AnyType anyType : anyTypeDAO.findAll()) {
+            if (!anyType.equals(anyTypeDAO.findUser()) && !anyType.equals(anyTypeDAO.findGroup())) {
+                AnyTypeCond anyTypeCond = new AnyTypeCond();
+                anyTypeCond.setAnyTypeKey(anyType.getKey());
+                SearchCond cond = StringUtils.isBlank(this.conf.getAnyObjectMatchingCond())
+                        ? SearchCond.getLeafCond(anyTypeCond)
+                        : SearchCond.getAndCond(
+                                SearchCond.getLeafCond(anyTypeCond),
+                                SearchCondConverter.convert(this.conf.getAnyObjectMatchingCond()));
+
+                int count = searchDAO.count(SyncopeConstants.FULL_ADMIN_REALMS, cond, AnyTypeKind.ANY_OBJECT);
+
+                atts.clear();
+                atts.addAttribute("", "", "type", ReportXMLConst.XSD_STRING, anyType.getKey());
+                atts.addAttribute("", "", "total", ReportXMLConst.XSD_INT, String.valueOf(count));
+                handler.startElement("", "", getAnyElementName(AnyTypeKind.ANY_OBJECT) + "s", atts);
+
+                doExtract(handler, count, cond, AnyTypeKind.ANY_OBJECT);
+
+                handler.endElement("", "", getAnyElementName(AnyTypeKind.ANY_OBJECT) + "s");
+            }
+        }
+    }
+
+    private static class Missing {
+
+        private final String resource;
+
+        private final String connObjectKeyValue;
+
+        Missing(final String resource, final String connObjectKeyValue) {
+            this.resource = resource;
+            this.connObjectKeyValue = connObjectKeyValue;
+        }
+
+        public String getResource() {
+            return resource;
+        }
+
+        public String getConnObjectKeyValue() {
+            return connObjectKeyValue;
+        }
+
+    }
+
+    private static class Misaligned extends Missing {
+
+        private final String name;
+
+        private final Set<Object> onSyncope;
+
+        private final Set<Object> onResource;
+
+        Misaligned(
+                final String resource,
+                final String connObjectKeyValue,
+                final String name,
+                final Set<Object> onSyncope,
+                final Set<Object> onResource) {
+
+            super(resource, connObjectKeyValue);
+
+            this.name = name;
+            this.onSyncope = onSyncope;
+            this.onResource = onResource;
+        }
+
+        public String getName() {
+            return name;
+        }
+
+        public Set<Object> getOnSyncope() {
+            return onSyncope;
+        }
+
+        public Set<Object> getOnResource() {
+            return onResource;
+        }
+
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/67ecbea3/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReportException.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReportException.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReportException.java
new file mode 100644
index 0000000..db42152
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReportException.java
@@ -0,0 +1,32 @@
+/*
+ * 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.report;
+
+public class ReportException extends RuntimeException {
+
+    private static final long serialVersionUID = 6719507778589395283L;
+
+    public ReportException(final Throwable cause) {
+        super(cause);
+    }
+
+    public ReportException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/67ecbea3/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReportJob.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReportJob.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReportJob.java
new file mode 100644
index 0000000..78183d5
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReportJob.java
@@ -0,0 +1,80 @@
+/*
+ * 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.report;
+
+import org.apache.syncope.core.spring.security.AuthContextUtils;
+import org.apache.syncope.core.provisioning.java.job.AbstractInterruptableJob;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.apache.syncope.core.provisioning.api.job.JobManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Quartz job for executing a given report.
+ */
+public class ReportJob extends AbstractInterruptableJob {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ReportJob.class);
+
+    /**
+     * Key, set by the caller, for identifying the report to be executed.
+     */
+    private String reportKey;
+
+    @Autowired
+    private ReportJobDelegate delegate;
+
+    /**
+     * Report id setter.
+     *
+     * @param reportKey to be set
+     */
+    public void setReportKey(final String reportKey) {
+        this.reportKey = reportKey;
+    }
+
+    @Override
+    public void execute(final JobExecutionContext context) throws JobExecutionException {
+        super.execute(context);
+
+        try {
+            AuthContextUtils.execWithAuthContext(context.getMergedJobDataMap().getString(JobManager.DOMAIN_KEY),
+                    new AuthContextUtils.Executable<Void>() {
+
+                @Override
+                public Void exec() {
+                    try {
+                        delegate.execute(reportKey);
+                    } catch (Exception e) {
+                        LOG.error("While executing report {}", reportKey, e);
+                        throw new RuntimeException(e);
+                    }
+
+                    return null;
+                }
+            });
+        } catch (RuntimeException e) {
+            LOG.error("While executing report {}", reportKey, e);
+            throw new JobExecutionException("While executing report " + reportKey, e);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/67ecbea3/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReportJobDelegate.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReportJobDelegate.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReportJobDelegate.java
new file mode 100644
index 0000000..28f4894
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReportJobDelegate.java
@@ -0,0 +1,198 @@
+/*
+ * 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.report;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Date;
+import java.util.zip.Deflater;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.sax.SAXTransformerFactory;
+import javax.xml.transform.sax.TransformerHandler;
+import javax.xml.transform.stream.StreamResult;
+import org.apache.commons.io.IOUtils;
+import org.apache.syncope.common.lib.report.ReportletConf;
+import org.apache.syncope.common.lib.types.ReportExecStatus;
+import org.apache.syncope.core.provisioning.api.utils.ExceptionUtils2;
+import org.apache.syncope.core.spring.ApplicationContextProvider;
+import org.apache.syncope.core.persistence.api.ImplementationLookup;
+import org.apache.syncope.core.persistence.api.dao.ReportDAO;
+import org.apache.syncope.core.persistence.api.dao.ReportExecDAO;
+import org.apache.syncope.core.persistence.api.dao.Reportlet;
+import org.apache.syncope.core.persistence.api.entity.EntityFactory;
+import org.apache.syncope.core.persistence.api.entity.Report;
+import org.apache.syncope.core.persistence.api.entity.ReportExec;
+import org.quartz.JobExecutionException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.support.AbstractBeanDefinition;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+import org.xml.sax.helpers.AttributesImpl;
+
+@Component
+public class ReportJobDelegate {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ReportJobDelegate.class);
+
+    /**
+     * Report DAO.
+     */
+    @Autowired
+    private ReportDAO reportDAO;
+
+    /**
+     * Report execution DAO.
+     */
+    @Autowired
+    private ReportExecDAO reportExecDAO;
+
+    @Autowired
+    private EntityFactory entityFactory;
+
+    @Autowired
+    private ImplementationLookup implementationLookup;
+
+    @Transactional
+    public void execute(final String reportKey) throws JobExecutionException {
+        Report report = reportDAO.find(reportKey);
+        if (report == null) {
+            throw new JobExecutionException("Report " + reportKey + " not found");
+        }
+
+        if (!report.isActive()) {
+            LOG.info("Report {} not active, aborting...", reportKey);
+            return;
+        }
+
+        // 1. create execution
+        ReportExec execution = entityFactory.newEntity(ReportExec.class);
+        execution.setStatus(ReportExecStatus.STARTED);
+        execution.setStart(new Date());
+        execution.setReport(report);
+        execution = reportExecDAO.save(execution);
+
+        report.add(execution);
+        report = reportDAO.save(report);
+
+        // 2. define a SAX handler for generating result as XML
+        TransformerHandler handler;
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ZipOutputStream zos = new ZipOutputStream(baos);
+        zos.setLevel(Deflater.BEST_COMPRESSION);
+        try {
+            SAXTransformerFactory tFactory = (SAXTransformerFactory) SAXTransformerFactory.newInstance();
+            tFactory.setFeature(javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING, true);
+            handler = tFactory.newTransformerHandler();
+            Transformer serializer = handler.getTransformer();
+            serializer.setOutputProperty(OutputKeys.ENCODING, StandardCharsets.UTF_8.name());
+            serializer.setOutputProperty(OutputKeys.INDENT, "yes");
+
+            // a single ZipEntry in the ZipOutputStream
+            zos.putNextEntry(new ZipEntry(report.getName()));
+
+            // streaming SAX handler in a compressed byte array stream
+            handler.setResult(new StreamResult(zos));
+        } catch (Exception e) {
+            throw new JobExecutionException("While configuring for SAX generation", e, true);
+        }
+
+        execution.setStatus(ReportExecStatus.RUNNING);
+        execution = reportExecDAO.save(execution);
+
+        // 3. actual report execution
+        StringBuilder reportExecutionMessage = new StringBuilder();
+        try {
+            // report header
+            handler.startDocument();
+            AttributesImpl atts = new AttributesImpl();
+            atts.addAttribute("", "", ReportXMLConst.ATTR_NAME, ReportXMLConst.XSD_STRING, report.getName());
+            handler.startElement("", "", ReportXMLConst.ELEMENT_REPORT, atts);
+
+            // iterate over reportlet instances defined for this report
+            for (ReportletConf reportletConf : report.getReportletConfs()) {
+                Class<? extends Reportlet> reportletClass =
+                        implementationLookup.getReportletClass(reportletConf.getClass());
+                if (reportletClass == null) {
+                    LOG.warn("Could not find matching reportlet for {}", reportletConf.getClass());
+                } else {
+                    // fetch (or create) reportlet
+                    Reportlet reportlet;
+                    if (ApplicationContextProvider.getBeanFactory().containsSingleton(reportletClass.getName())) {
+                        reportlet = (Reportlet) ApplicationContextProvider.getBeanFactory().
+                                getSingleton(reportletClass.getName());
+                    } else {
+                        reportlet = (Reportlet) ApplicationContextProvider.getBeanFactory().
+                                createBean(reportletClass, AbstractBeanDefinition.AUTOWIRE_BY_TYPE, false);
+                        ApplicationContextProvider.getBeanFactory().
+                                registerSingleton(reportletClass.getName(), reportlet);
+                    }
+
+                    // invoke reportlet
+                    try {
+                        reportlet.extract(reportletConf, handler);
+                    } catch (Throwable t) {
+                        LOG.error("While executing reportlet {} for report {}", reportlet, reportKey, t);
+
+                        execution.setStatus(ReportExecStatus.FAILURE);
+
+                        Throwable effective = t instanceof ReportException
+                                ? t.getCause()
+                                : t;
+                        reportExecutionMessage.
+                                append(ExceptionUtils2.getFullStackTrace(effective)).
+                                append("\n==================\n");
+                    }
+                }
+            }
+
+            // report footer
+            handler.endElement("", "", ReportXMLConst.ELEMENT_REPORT);
+            handler.endDocument();
+
+            if (!ReportExecStatus.FAILURE.name().equals(execution.getStatus())) {
+                execution.setStatus(ReportExecStatus.SUCCESS);
+            }
+        } catch (Exception e) {
+            execution.setStatus(ReportExecStatus.FAILURE);
+            reportExecutionMessage.append(ExceptionUtils2.getFullStackTrace(e));
+
+            throw new JobExecutionException(e, true);
+        } finally {
+            try {
+                zos.closeEntry();
+                IOUtils.closeQuietly(zos);
+                IOUtils.closeQuietly(baos);
+            } catch (IOException e) {
+                LOG.error("While closing StreamResult's backend", e);
+            }
+
+            execution.setExecResult(baos.toByteArray());
+            execution.setMessage(reportExecutionMessage.toString());
+            execution.setEnd(new Date());
+            reportExecDAO.save(execution);
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/67ecbea3/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReportXMLConst.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReportXMLConst.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReportXMLConst.java
new file mode 100644
index 0000000..6b21695
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/ReportXMLConst.java
@@ -0,0 +1,44 @@
+/*
+ * 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.report;
+
+public final class ReportXMLConst {
+
+    public static final String XSD_STRING = "xsd:string";
+
+    public static final String XSD_INT = "xsd:integer";
+
+    public static final String XSD_LONG = "xsd:long";
+
+    public static final String XSD_BOOLEAN = "xsd:boolean";
+
+    public static final String XSD_DATETIME = "xsd:dateTime";
+
+    public static final String ELEMENT_REPORT = "report";
+
+    public static final String ATTR_NAME = "name";
+
+    public static final String ATTR_CLASS = "class";
+
+    public static final String ELEMENT_REPORTLET = "reportlet";
+
+    private ReportXMLConst() {
+        // empty private constructor for static utility class
+    }
+}

http://git-wip-us.apache.org/repos/asf/syncope/blob/67ecbea3/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/StaticReportlet.java
----------------------------------------------------------------------
diff --git a/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/StaticReportlet.java b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/StaticReportlet.java
new file mode 100644
index 0000000..1156d25
--- /dev/null
+++ b/core/provisioning-java/src/main/java/org/apache/syncope/core/provisioning/java/job/report/StaticReportlet.java
@@ -0,0 +1,128 @@
+/*
+ * 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.report;
+
+import org.apache.syncope.core.persistence.api.dao.ReportletConfClass;
+import org.apache.syncope.common.lib.report.ReportletConf;
+import org.apache.syncope.common.lib.report.StaticReportletConf;
+import org.apache.syncope.core.provisioning.api.utils.FormatUtils;
+import org.springframework.util.StringUtils;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.AttributesImpl;
+
+@ReportletConfClass(StaticReportletConf.class)
+public class StaticReportlet extends AbstractReportlet {
+
+    private StaticReportletConf conf;
+
+    private void doExtractConf(final ContentHandler handler) throws SAXException {
+        AttributesImpl atts = new AttributesImpl();
+        handler.startElement("", "", "configurations", null);
+        handler.startElement("", "", "staticAttributes", atts);
+
+        handler.startElement("", "", "string", atts);
+        handler.characters("string".toCharArray(), 0, "string".length());
+        handler.endElement("", "", "string");
+
+        handler.startElement("", "", "long", atts);
+        handler.characters("long".toCharArray(), 0, "long".length());
+        handler.endElement("", "", "long");
+
+        handler.startElement("", "", "double", atts);
+        handler.characters("double".toCharArray(), 0, "double".length());
+        handler.endElement("", "", "double");
+
+        handler.startElement("", "", "date", atts);
+        handler.characters("date".toCharArray(), 0, "date".length());
+        handler.endElement("", "", "date");
+
+        handler.startElement("", "", "double", atts);
+        handler.characters("double".toCharArray(), 0, "double".length());
+        handler.endElement("", "", "double");
+
+        handler.startElement("", "", "enum", atts);
+        handler.characters("enum".toCharArray(), 0, "enum".length());
+        handler.endElement("", "", "enum");
+
+        handler.startElement("", "", "list", atts);
+        handler.characters("list".toCharArray(), 0, "list".length());
+        handler.endElement("", "", "list");
+
+        handler.endElement("", "", "staticAttributes");
+        handler.endElement("", "", "configurations");
+    }
+
+    @Override
+    protected void doExtract(final ReportletConf conf, final ContentHandler handler) throws SAXException {
+        if (conf instanceof StaticReportletConf) {
+            this.conf = StaticReportletConf.class.cast(conf);
+        } else {
+            throw new ReportException(new IllegalArgumentException("Invalid configuration provided"));
+        }
+
+        doExtractConf(handler);
+
+        if (StringUtils.hasText(this.conf.getStringField())) {
+            handler.startElement("", "", "string", null);
+            handler.characters(this.conf.getStringField().toCharArray(), 0, this.conf.getStringField().length());
+            handler.endElement("", "", "string");
+        }
+
+        if (this.conf.getLongField() != null) {
+            handler.startElement("", "", "long", null);
+            String printed = String.valueOf(this.conf.getLongField());
+            handler.characters(printed.toCharArray(), 0, printed.length());
+            handler.endElement("", "", "long");
+        }
+
+        if (this.conf.getDoubleField() != null) {
+            handler.startElement("", "", "double", null);
+            String printed = String.valueOf(this.conf.getDoubleField());
+            handler.characters(printed.toCharArray(), 0, printed.length());
+            handler.endElement("", "", "double");
+        }
+
+        if (this.conf.getDateField() != null) {
+            handler.startElement("", "", "date", null);
+            String printed = FormatUtils.format(this.conf.getDateField());
+            handler.characters(printed.toCharArray(), 0, printed.length());
+            handler.endElement("", "", "date");
+        }
+
+        if (this.conf.getTraceLevel() != null) {
+            handler.startElement("", "", "enum", null);
+            String printed = this.conf.getTraceLevel().name();
+            handler.characters(printed.toCharArray(), 0, printed.length());
+            handler.endElement("", "", "enum");
+        }
+
+        if (this.conf.getListField() != null && !this.conf.getListField().isEmpty()) {
+            handler.startElement("", "", "list", null);
+            for (String item : this.conf.getListField()) {
+                if (StringUtils.hasText(item)) {
+                    handler.startElement("", "", "string", null);
+                    handler.characters(item.toCharArray(), 0, item.length());
+                    handler.endElement("", "", "string");
+                }
+            }
+            handler.endElement("", "", "list");
+        }
+    }
+}