org.entcore.timeline.services.impl.DefaultTimelineMailerService.java Source code

Java tutorial

Introduction

Here is the source code for org.entcore.timeline.services.impl.DefaultTimelineMailerService.java

Source

/*
 * Copyright  "Open Digital Education", 2017
 *
 * This program is published by "Open Digital Education".
 * You must indicate the name of the software and the company in any production /contribution
 * using the software and indicate on the home page of the software industry in question,
 * "powered by Open Digital Education" with a reference to the website: https://opendigitaleducation.com/.
 *
 * This program is free software, licensed under the terms of the GNU Affero General Public License
 * as published by the Free Software Foundation, version 3 of the License.
 *
 * You can redistribute this application and/or modify it since you respect the terms of the GNU Affero General Public License.
 * If you modify the source code and then use this modified source code in your creation, you must make available the source code of your modifications.
 *
 * You should have received a copy of the GNU Affero General Public License along with the software.
 * If not, please see : <http://www.gnu.org/licenses/>. Full compliance requires reading the terms of this license and following its directives.
    
 */

package org.entcore.timeline.services.impl;

import com.mongodb.QueryBuilder;
import fr.wseduc.mongodb.MongoDb;
import fr.wseduc.mongodb.MongoQueryBuilder;
import fr.wseduc.webutils.Either;
import fr.wseduc.webutils.I18n;
import fr.wseduc.webutils.Server;
import fr.wseduc.webutils.email.EmailSender;
import fr.wseduc.webutils.http.Renders;
import io.vertx.core.AsyncResult;
import io.vertx.core.shareddata.LocalMap;
import org.entcore.common.email.EmailFactory;
import org.entcore.common.http.request.JsonHttpServerRequest;
import org.entcore.common.neo4j.Neo4j;
import org.entcore.common.neo4j.Neo4jResult;
import org.entcore.common.notification.NotificationUtils;
import org.entcore.common.notification.TimelineNotificationsLoader;
import org.entcore.timeline.controllers.TimelineLambda;
import org.entcore.timeline.services.TimelineConfigService;
import org.entcore.timeline.services.TimelineMailerService;
import io.vertx.core.Handler;
import io.vertx.core.Vertx;
import io.vertx.core.eventbus.EventBus;
import io.vertx.core.eventbus.Message;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;

import java.io.StringReader;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

import static fr.wseduc.webutils.Utils.getOrElse;

public class DefaultTimelineMailerService extends Renders implements TimelineMailerService {

    private static final Logger log = LoggerFactory.getLogger(DefaultTimelineMailerService.class);
    private static final String USERBOOK_ADDRESS = "userbook.preferences";
    private final EventBus eb;
    private Map<String, String> registeredNotifications;
    private TimelineConfigService configService;
    private LocalMap<String, String> eventsI18n;
    private HashMap<String, JsonObject> lazyEventsI18n;
    private final EmailSender emailSender;
    private final int USERS_LIMIT;
    private final MongoDb mongo = MongoDb.getInstance();
    private final Neo4j neo4j = Neo4j.getInstance();

    public DefaultTimelineMailerService(Vertx vertx, JsonObject config) {
        super(vertx, config);
        eb = Server.getEventBus(vertx);
        EmailFactory emailFactory = new EmailFactory(this.vertx, config);
        emailSender = emailFactory.getSender();
        USERS_LIMIT = config.getInteger("users-loop-limit", 25);
    }

    /* Override i18n to use additional timeline translations and nested templates */
    @Override
    protected void setLambdaTemplateRequest(final HttpServerRequest request, final Map<String, Object> ctx) {
        super.setLambdaTemplateRequest(request, ctx);
        TimelineLambda.setLambdaTemplateRequest(request, ctx, eventsI18n, lazyEventsI18n);
    }

    @Override
    public void sendImmediateMails(HttpServerRequest request, String notificationName, JsonObject notification,
            JsonObject templateParameters, JsonArray userList, JsonObject notificationProperties) {

        final Map<String, Map<String, String>> processedTemplates = new HashMap<>();
        templateParameters.put("innerTemplate", notification.getString("template", ""));

        final Map<String, Map<String, List<Object>>> toByDomainLang = new HashMap<>();

        final AtomicInteger userCount = new AtomicInteger(userList.size());
        final Handler<Void> templatesHandler = new Handler<Void>() {
            public void handle(Void v) {
                if (userCount.decrementAndGet() == 0) {
                    if (toByDomainLang.size() > 0) {
                        //On completion : log
                        final Handler<AsyncResult<Message<JsonObject>>> completionHandler = event -> {
                            if (event.failed()
                                    || "error".equals(event.result().body().getString("status", "error"))) {
                                log.error("[Timeline immediate emails] Error while sending mails : ",
                                        event.cause());
                            } else {
                                log.debug("[Timeline immediate emails] Immediate mails sent.");
                            }
                        };

                        JsonArray keys = new fr.wseduc.webutils.collections.JsonArray()
                                .add("timeline.immediate.mail.subject.header").add(notificationName.toLowerCase());

                        for (final String domain : toByDomainLang.keySet()) {
                            for (final String lang : toByDomainLang.get(domain).keySet()) {
                                translateTimeline(keys, domain, lang, new Handler<JsonArray>() {
                                    public void handle(JsonArray translations) {
                                        //Send mail containing the "immediate" notification
                                        emailSender.sendEmail(request, toByDomainLang.get(domain).get(lang), null,
                                                null, translations.getString(0) + translations.getString(1),
                                                processedTemplates.get(domain).get(lang), null, false,
                                                completionHandler);
                                    }
                                });
                            }
                        }
                    }
                }
            }
        };

        for (Object userObj : userList) {
            final JsonObject userPref = ((JsonObject) userObj);
            final String userDomain = userPref.getString("lastDomain", I18n.DEFAULT_DOMAIN);
            final String userScheme = userPref.getString("lastScheme", "http");
            String mutableLanguage = "fr";
            try {
                mutableLanguage = getOrElse(new JsonObject(getOrElse(userPref.getString("language"), "{}", false))
                        .getString("default-domain"), "fr", false);
            } catch (Exception e) {
                log.error("UserId [" + userPref.getString("userId", "") + "] - Bad language preferences format");
            }
            final String userLanguage = mutableLanguage;

            if (!processedTemplates.containsKey(userDomain))
                processedTemplates.put(userDomain, new HashMap<String, String>());
            JsonObject notificationPreference = userPref.getJsonObject("preferences", new JsonObject())
                    .getJsonObject("config", new JsonObject()).getJsonObject(notificationName, new JsonObject());
            // If the frequency is IMMEDIATE
            // and the restriction is not INTERNAL (timeline only)
            // and if the user has provided an email
            if (TimelineNotificationsLoader.Frequencies.IMMEDIATE.name()
                    .equals(notificationPreference.getString("defaultFrequency",
                            notificationProperties.getString("defaultFrequency")))
                    && !TimelineNotificationsLoader.Restrictions.INTERNAL.name()
                            .equals(notificationPreference.getString("restriction",
                                    notificationProperties.getString("restriction")))
                    && !TimelineNotificationsLoader.Restrictions.HIDDEN.name()
                            .equals(notificationPreference.getString("restriction",
                                    notificationProperties.getString("restriction")))
                    && userPref.getString("userMail") != null && !userPref.getString("userMail").trim().isEmpty()) {
                if (!toByDomainLang.containsKey(userDomain)) {
                    toByDomainLang.put(userDomain, new HashMap<String, List<Object>>());
                }
                if (!toByDomainLang.get(userDomain).containsKey(userLanguage)) {
                    toByDomainLang.get(userDomain).put(userLanguage, new ArrayList<Object>());
                }
                toByDomainLang.get(userDomain).get(userLanguage).add(userPref.getString("userMail"));
            }
            if (!processedTemplates.get(userDomain).containsKey(userLanguage)) {
                processTimelineTemplate(templateParameters, "", "notifications/immediate-mail.html", userDomain,
                        userScheme, userLanguage, false, new Handler<String>() {
                            public void handle(String processedTemplate) {
                                processedTemplates.get(userDomain).put(userLanguage, processedTemplate);
                                templatesHandler.handle(null);
                            }
                        });
            } else {
                templatesHandler.handle(null);
            }
        }
    }

    @Override
    public void sendImmediateMails(final HttpServerRequest request, final String notificationName,
            final JsonObject notification, final JsonObject templateParameters, final JsonArray recipientIds) {
        //Get notification properties (mixin : admin console configuration which overrides default properties)
        configService.getNotificationProperties(notificationName, new Handler<Either<String, JsonObject>>() {
            public void handle(final Either<String, JsonObject> properties) {
                if (properties.isLeft() || properties.right().getValue() == null) {
                    log.error("[sendImmediateMails] Issue while retrieving notification (" + notificationName
                            + ") properties.");
                    return;
                }
                //Get users preferences (overrides notification properties)
                NotificationUtils.getUsersPreferences(eb, recipientIds, "language: uac.language",
                        new Handler<JsonArray>() {
                            public void handle(final JsonArray userList) {
                                if (userList == null) {
                                    log.error("[sendImmediateMails] Issue while retrieving users preferences.");
                                    return;
                                }

                                sendImmediateMails(request, notificationName, notification, templateParameters,
                                        userList, properties.right().getValue());
                            }
                        });
            }
        });
    }

    @Override
    public void translateTimeline(JsonArray i18nKeys, String domain, String language, Handler<JsonArray> handler) {
        String i18n = eventsI18n.get(language.split(",")[0].split("-")[0]);
        final JsonObject timelineI18n;
        if (i18n == null) {
            timelineI18n = new JsonObject();
        } else {
            timelineI18n = new JsonObject("{" + i18n.substring(0, i18n.length() - 1) + "}");
        }
        timelineI18n.mergeIn(I18n.getInstance().load(language, domain));
        JsonArray translations = new fr.wseduc.webutils.collections.JsonArray();
        for (Object keyObj : i18nKeys) {
            String key = (String) keyObj;
            translations.add(timelineI18n.getString(key, key));
        }
        handler.handle(translations);
    }

    @Override
    public void processTimelineTemplate(JsonObject parameters, String resourceName, String template, String domain,
            String scheme, String language, boolean reader, final Handler<String> handler) {
        final HttpServerRequest request = new JsonHttpServerRequest(new JsonObject().put("headers", new JsonObject()
                .put("Host", domain).put("X-Forwarded-Proto", scheme).put("Accept-Language", language)));
        if (reader) {
            final StringReader templateReader = new StringReader(template);
            processTemplate(request, parameters, resourceName, templateReader, new Handler<Writer>() {
                public void handle(Writer writer) {
                    handler.handle(writer.toString());
                }
            });

        } else {
            processTemplate(request, template, parameters, handler);
        }
    }

    @Override
    public void sendDailyMails(int dayDelta, final Handler<Either<String, JsonObject>> handler) {

        final HttpServerRequest request = new JsonHttpServerRequest(new JsonObject());
        final AtomicInteger userPagination = new AtomicInteger(0);
        final AtomicInteger endPage = new AtomicInteger(0);
        final Calendar dayDate = Calendar.getInstance();
        dayDate.add(Calendar.DAY_OF_MONTH, dayDelta);
        dayDate.set(Calendar.HOUR_OF_DAY, 0);
        dayDate.set(Calendar.MINUTE, 0);
        dayDate.set(Calendar.SECOND, 0);
        dayDate.set(Calendar.MILLISECOND, 0);

        final JsonObject results = new JsonObject().put("mails.sent", 0).put("users.ko", 0);
        final JsonObject notificationsDefaults = new JsonObject();
        final List<String> notifiedUsers = new ArrayList<>();

        final Handler<Boolean> userContinuationHandler = new Handler<Boolean>() {

            private final Handler<Boolean> continuation = this;
            private final Handler<JsonArray> usersHandler = new Handler<JsonArray>() {
                public void handle(final JsonArray users) {
                    final int nbUsers = users.size();
                    if (nbUsers == 0) {
                        log.info("[DailyMails] Page0 : " + userPagination.get() + "/" + endPage.get());
                        continuation.handle(userPagination.get() != endPage.get());
                        return;
                    }
                    final AtomicInteger usersCountdown = new AtomicInteger(nbUsers);

                    final Handler<Void> usersEndHandler = new Handler<Void>() {
                        public void handle(Void v) {
                            if (usersCountdown.decrementAndGet() <= 0) {
                                log.info("[DailyMails] Page : " + userPagination.get() + "/" + endPage.get());
                                continuation.handle(userPagination.get() != endPage.get());
                            }
                        }
                    };

                    final JsonArray userIds = new fr.wseduc.webutils.collections.JsonArray();
                    for (Object userObj : users)
                        userIds.add(((JsonObject) userObj).getString("id", ""));
                    NotificationUtils.getUsersPreferences(eb, userIds, "language: uac.language",
                            new Handler<JsonArray>() {
                                public void handle(JsonArray preferences) {
                                    for (Object userObj : preferences) {
                                        final JsonObject userPrefs = (JsonObject) userObj;
                                        final String userDomain = userPrefs.getString("lastDomain",
                                                I18n.DEFAULT_DOMAIN);
                                        final String userScheme = userPrefs.getString("lastScheme", "http");
                                        String mutableUserLanguage = "fr";
                                        try {
                                            mutableUserLanguage = getOrElse(new JsonObject(
                                                    getOrElse(userPrefs.getString("language"), "{}", false))
                                                            .getString("default-domain"),
                                                    "fr", false);
                                        } catch (Exception e) {
                                            log.error("UserId [" + userPrefs.getString("userId", "")
                                                    + "] - Bad language preferences format");
                                        }
                                        final String userLanguage = mutableUserLanguage;

                                        getUserNotifications(userPrefs.getString("userId", ""), dayDate.getTime(),
                                                new Handler<JsonArray>() {
                                                    public void handle(JsonArray notifications) {
                                                        if (notifications.size() == 0) {
                                                            usersEndHandler.handle(null);
                                                            return;
                                                        }

                                                        SimpleDateFormat formatter = new SimpleDateFormat(
                                                                "EEE, d MMM yyyy HH:mm:ss",
                                                                Locale.forLanguageTag(userLanguage));
                                                        final JsonArray dates = new fr.wseduc.webutils.collections.JsonArray();
                                                        final JsonArray templates = new fr.wseduc.webutils.collections.JsonArray();

                                                        for (Object notificationObj : notifications) {
                                                            JsonObject notification = (JsonObject) notificationObj;
                                                            final String notificationName = notification
                                                                    .getString("type", "").toLowerCase() + "."
                                                                    + notification.getString("event-type", "")
                                                                            .toLowerCase();
                                                            if (notificationsDefaults
                                                                    .getJsonObject(notificationName) == null)
                                                                continue;

                                                            JsonObject notificationPreference = userPrefs
                                                                    .getJsonObject("preferences", new JsonObject())
                                                                    .getJsonObject("config", new JsonObject())
                                                                    .getJsonObject(notificationName,
                                                                            new JsonObject());
                                                            if (TimelineNotificationsLoader.Frequencies.DAILY.name()
                                                                    .equals(notificationPrefsMixin(
                                                                            "defaultFrequency",
                                                                            notificationPreference,
                                                                            notificationsDefaults.getJsonObject(
                                                                                    notificationName)))
                                                                    && !TimelineNotificationsLoader.Restrictions.INTERNAL
                                                                            .name()
                                                                            .equals(notificationPrefsMixin(
                                                                                    "restriction",
                                                                                    notificationPreference,
                                                                                    notificationsDefaults
                                                                                            .getJsonObject(
                                                                                                    notificationName)))
                                                                    && !TimelineNotificationsLoader.Restrictions.HIDDEN
                                                                            .name()
                                                                            .equals(notificationPrefsMixin(
                                                                                    "restriction",
                                                                                    notificationPreference,
                                                                                    notificationsDefaults
                                                                                            .getJsonObject(
                                                                                                    notificationName)))) {
                                                                templates.add(new JsonObject()
                                                                        .put("template", notificationsDefaults
                                                                                .getJsonObject(notificationName,
                                                                                        new JsonObject())
                                                                                .getString("template", ""))
                                                                        .put("params", notification.getJsonObject(
                                                                                "params", new JsonObject())));
                                                                dates.add(formatter.format(MongoDb.parseIsoDate(
                                                                        notification.getJsonObject("date"))));
                                                            }
                                                        }
                                                        if (templates.size() > 0) {
                                                            JsonObject templateParams = new JsonObject()
                                                                    .put("nestedTemplatesArray", templates)
                                                                    .put("notificationDates", dates);
                                                            processTimelineTemplate(templateParams, "",
                                                                    "notifications/daily-mail.html", userDomain,
                                                                    userScheme, userLanguage, false,
                                                                    new Handler<String>() {
                                                                        public void handle(
                                                                                final String processedTemplate) {
                                                                            //On completion : log
                                                                            final Handler<AsyncResult<Message<JsonObject>>> completionHandler = event -> {
                                                                                if (event.failed()
                                                                                        || "error".equals(event
                                                                                                .result().body()
                                                                                                .getString("status",
                                                                                                        "error"))) {
                                                                                    log.error(
                                                                                            "[Timeline daily emails] Error while sending mail : ",
                                                                                            event.cause());
                                                                                    results.put("users.ko",
                                                                                            results.getInteger(
                                                                                                    "users.ko")
                                                                                                    + 1);
                                                                                } else {
                                                                                    results.put("mails.sent",
                                                                                            results.getInteger(
                                                                                                    "mails.sent")
                                                                                                    + 1);
                                                                                }
                                                                                usersEndHandler.handle(null);
                                                                            };

                                                                            //Translate mail title
                                                                            JsonArray keys = new fr.wseduc.webutils.collections.JsonArray()
                                                                                    .add("timeline.daily.mail.subject.header");
                                                                            translateTimeline(keys, userDomain,
                                                                                    userLanguage,
                                                                                    new Handler<JsonArray>() {
                                                                                        public void handle(
                                                                                                JsonArray translations) {
                                                                                            //Send mail containing the "daily" notifications
                                                                                            emailSender.sendEmail(
                                                                                                    request,
                                                                                                    userPrefs
                                                                                                            .getString(
                                                                                                                    "userMail",
                                                                                                                    ""),
                                                                                                    null, null,
                                                                                                    translations
                                                                                                            .getString(
                                                                                                                    0),
                                                                                                    processedTemplate,
                                                                                                    null, false,
                                                                                                    completionHandler);
                                                                                        }
                                                                                    });
                                                                        }
                                                                    });
                                                        } else {
                                                            usersEndHandler.handle(null);
                                                        }

                                                    }
                                                });
                                    }
                                }
                            });
                }
            };

            public void handle(Boolean continuation) {
                if (continuation) {
                    getImpactedUsers(notifiedUsers, userPagination.getAndIncrement(),
                            new Handler<Either<String, JsonArray>>() {
                                public void handle(Either<String, JsonArray> event) {
                                    if (event.isLeft()) {
                                        log.error("[sendDailyMails] Error while retrieving impacted users : "
                                                + event.left().getValue());
                                        handler.handle(
                                                new Either.Left<String, JsonObject>(event.left().getValue()));
                                    } else {
                                        JsonArray users = event.right().getValue();
                                        usersHandler.handle(users);
                                    }
                                }
                            });
                } else {
                    handler.handle(new Either.Right<String, JsonObject>(results));
                }
            }
        };

        getRecipientsUsers(dayDate.getTime(), new Handler<JsonArray>() {
            @Override
            public void handle(JsonArray event) {
                if (event != null && event.size() > 0) {
                    notifiedUsers.addAll(event.getList());
                    endPage.set((event.size() / USERS_LIMIT) + (event.size() % USERS_LIMIT != 0 ? 1 : 0));
                } else {
                    handler.handle(new Either.Right<String, JsonObject>(results));
                    return;
                }
                getNotificationsDefaults(new Handler<JsonArray>() {
                    public void handle(final JsonArray notifications) {
                        if (notifications == null) {
                            log.error("[sendDailyMails] Error while retrieving notifications defaults.");
                        } else {
                            for (Object notifObj : notifications) {
                                final JsonObject notif = (JsonObject) notifObj;
                                notificationsDefaults.put(notif.getString("key", ""), notif);
                            }
                            userContinuationHandler.handle(true);
                        }
                    }
                });
            }
        });
    }

    public void sendWeeklyMails(int dayDelta, final Handler<Either<String, JsonObject>> handler) {

        final HttpServerRequest request = new JsonHttpServerRequest(new JsonObject());
        final AtomicInteger userPagination = new AtomicInteger(0);
        final AtomicInteger endPage = new AtomicInteger(0);
        final Calendar weekDate = Calendar.getInstance();
        weekDate.add(Calendar.DAY_OF_MONTH, dayDelta - 6);
        weekDate.set(Calendar.HOUR_OF_DAY, 0);
        weekDate.set(Calendar.MINUTE, 0);
        weekDate.set(Calendar.SECOND, 0);
        weekDate.set(Calendar.MILLISECOND, 0);

        final JsonObject results = new JsonObject().put("mails.sent", 0).put("users.ko", 0);
        final JsonObject notificationsDefaults = new JsonObject();
        final List<String> notifiedUsers = new ArrayList<>();

        final Handler<Boolean> userContinuationHandler = new Handler<Boolean>() {

            private final Handler<Boolean> continuation = this;
            private final Handler<JsonArray> usersHandler = new Handler<JsonArray>() {
                public void handle(final JsonArray users) {
                    final int nbUsers = users.size();
                    if (nbUsers == 0) {
                        log.info("[WeeklyMails] Page0 : " + userPagination.get() + "/" + endPage.get());
                        continuation.handle(userPagination.get() != endPage.get());
                        return;
                    }
                    final AtomicInteger usersCountdown = new AtomicInteger(nbUsers);

                    final Handler<Void> usersEndHandler = new Handler<Void>() {
                        public void handle(Void v) {
                            if (usersCountdown.decrementAndGet() <= 0) {
                                log.info("[WeeklyMails] Page : " + userPagination.get() + "/" + endPage.get());
                                continuation.handle(userPagination.get() != endPage.get());
                            }
                        }
                    };

                    final JsonArray userIds = new fr.wseduc.webutils.collections.JsonArray();
                    for (Object userObj : users)
                        userIds.add(((JsonObject) userObj).getString("id", ""));
                    NotificationUtils.getUsersPreferences(eb, userIds, "language: uac.language",
                            new Handler<JsonArray>() {
                                public void handle(JsonArray preferences) {
                                    for (Object userObj : preferences) {
                                        final JsonObject userPrefs = (JsonObject) userObj;
                                        final String userDomain = userPrefs.getString("lastDomain",
                                                I18n.DEFAULT_DOMAIN);
                                        final String userScheme = userPrefs.getString("lastScheme", "http");
                                        String mutableUserLanguage = "fr";
                                        try {
                                            mutableUserLanguage = getOrElse(new JsonObject(
                                                    getOrElse(userPrefs.getString("language"), "{}", false))
                                                            .getString("default-domain"),
                                                    "fr", false);
                                        } catch (Exception e) {
                                            log.error("UserId [" + userPrefs.getString("userId", "")
                                                    + "] - Bad language preferences format");
                                        }
                                        final String userLanguage = mutableUserLanguage;

                                        getAggregatedUserNotifications(userPrefs.getString("userId", ""),
                                                weekDate.getTime(), new Handler<JsonArray>() {
                                                    public void handle(JsonArray notifications) {
                                                        if (notifications.size() == 0) {
                                                            usersEndHandler.handle(null);
                                                            return;
                                                        }

                                                        final JsonArray weeklyNotifications = new fr.wseduc.webutils.collections.JsonArray();

                                                        for (Object notificationObj : notifications) {
                                                            JsonObject notification = (JsonObject) notificationObj;
                                                            final String notificationName = notification
                                                                    .getString("type", "").toLowerCase() + "."
                                                                    + notification.getString("event-type", "")
                                                                            .toLowerCase();
                                                            if (notificationsDefaults
                                                                    .getJsonObject(notificationName) == null)
                                                                continue;

                                                            JsonObject notificationPreference = userPrefs
                                                                    .getJsonObject("preferences", new JsonObject())
                                                                    .getJsonObject("config", new JsonObject())
                                                                    .getJsonObject(notificationName,
                                                                            new JsonObject());
                                                            if (TimelineNotificationsLoader.Frequencies.WEEKLY
                                                                    .name()
                                                                    .equals(notificationPrefsMixin(
                                                                            "defaultFrequency",
                                                                            notificationPreference,
                                                                            notificationsDefaults.getJsonObject(
                                                                                    notificationName)))
                                                                    && !TimelineNotificationsLoader.Restrictions.INTERNAL
                                                                            .name()
                                                                            .equals(notificationPrefsMixin(
                                                                                    "restriction",
                                                                                    notificationPreference,
                                                                                    notificationsDefaults
                                                                                            .getJsonObject(
                                                                                                    notificationName)))
                                                                    && !TimelineNotificationsLoader.Restrictions.HIDDEN
                                                                            .name()
                                                                            .equals(notificationPrefsMixin(
                                                                                    "restriction",
                                                                                    notificationPreference,
                                                                                    notificationsDefaults
                                                                                            .getJsonObject(
                                                                                                    notificationName)))) {
                                                                notification.put("notificationName",
                                                                        notificationName);
                                                                weeklyNotifications.add(notification);
                                                            }
                                                        }

                                                        final JsonObject weeklyNotificationsObj = new JsonObject();
                                                        final JsonArray weeklyNotificationsGroupedArray = new fr.wseduc.webutils.collections.JsonArray();
                                                        for (Object notif : weeklyNotifications) {
                                                            JsonObject notification = (JsonObject) notif;
                                                            if (!weeklyNotificationsObj.containsKey(
                                                                    notification.getString("type").toLowerCase()))
                                                                weeklyNotificationsObj.put(
                                                                        notification.getString("type")
                                                                                .toLowerCase(),
                                                                        new JsonObject().put("link",
                                                                                notificationsDefaults.getJsonObject(
                                                                                        notification.getString(
                                                                                                "notificationName"))
                                                                                        .getString("app-address",
                                                                                                ""))
                                                                                .put("event-types",
                                                                                        new fr.wseduc.webutils.collections.JsonArray()));
                                                            weeklyNotificationsObj
                                                                    .getJsonObject(notification.getString("type")
                                                                            .toLowerCase())
                                                                    .getJsonArray(("event-types"),
                                                                            new fr.wseduc.webutils.collections.JsonArray())
                                                                    .add(notification);
                                                        }

                                                        for (String key : weeklyNotificationsObj.getMap()
                                                                .keySet()) {
                                                            weeklyNotificationsGroupedArray.add(new JsonObject()
                                                                    .put("type", key)
                                                                    .put("link",
                                                                            weeklyNotificationsObj
                                                                                    .getJsonObject(key)
                                                                                    .getString("link", ""))
                                                                    .put("event-types",
                                                                            weeklyNotificationsObj
                                                                                    .getJsonObject(key)
                                                                                    .getJsonArray("event-types")));
                                                        }

                                                        if (weeklyNotifications.size() > 0) {
                                                            JsonObject templateParams = new JsonObject().put(
                                                                    "notifications",
                                                                    weeklyNotificationsGroupedArray);
                                                            processTimelineTemplate(templateParams, "",
                                                                    "notifications/weekly-mail.html", userDomain,
                                                                    userScheme, userLanguage, false,
                                                                    new Handler<String>() {
                                                                        public void handle(
                                                                                final String processedTemplate) {
                                                                            //On completion : log
                                                                            final Handler<AsyncResult<Message<JsonObject>>> completionHandler = event -> {
                                                                                if (event.failed()
                                                                                        || "error".equals(event
                                                                                                .result().body()
                                                                                                .getString("status",
                                                                                                        "error"))) {
                                                                                    log.error(
                                                                                            "[Timeline weekly emails] Error while sending mail : ",
                                                                                            event.cause());
                                                                                    results.put("users.ko",
                                                                                            results.getInteger(
                                                                                                    "users.ko")
                                                                                                    + 1);
                                                                                } else {
                                                                                    results.put("mails.sent",
                                                                                            results.getInteger(
                                                                                                    "mails.sent")
                                                                                                    + 1);
                                                                                }
                                                                                usersEndHandler.handle(null);
                                                                            };

                                                                            //Translate mail title
                                                                            JsonArray keys = new fr.wseduc.webutils.collections.JsonArray()
                                                                                    .add("timeline.weekly.mail.subject.header");
                                                                            translateTimeline(keys, userDomain,
                                                                                    userLanguage,
                                                                                    new Handler<JsonArray>() {
                                                                                        public void handle(
                                                                                                JsonArray translations) {
                                                                                            //Send mail containing the "weekly" notifications
                                                                                            emailSender.sendEmail(
                                                                                                    request,
                                                                                                    userPrefs
                                                                                                            .getString(
                                                                                                                    "userMail",
                                                                                                                    ""),
                                                                                                    null, null,
                                                                                                    translations
                                                                                                            .getString(
                                                                                                                    0),
                                                                                                    processedTemplate,
                                                                                                    null, false,
                                                                                                    completionHandler);
                                                                                        }
                                                                                    });
                                                                        }
                                                                    });
                                                        } else {
                                                            usersEndHandler.handle(null);
                                                        }

                                                    }
                                                });
                                    }
                                }
                            });
                }
            };

            public void handle(Boolean continuation) {
                if (continuation) {
                    getImpactedUsers(notifiedUsers, userPagination.getAndIncrement(),
                            new Handler<Either<String, JsonArray>>() {
                                public void handle(Either<String, JsonArray> event) {
                                    if (event.isLeft()) {
                                        log.error("[sendWeeklyMails] Error while retrieving impacted users : "
                                                + event.left().getValue());
                                        handler.handle(
                                                new Either.Left<String, JsonObject>(event.left().getValue()));
                                    } else {
                                        JsonArray users = event.right().getValue();
                                        usersHandler.handle(users);
                                    }
                                }
                            });
                } else {
                    handler.handle(new Either.Right<String, JsonObject>(results));
                }
            }
        };
        getRecipientsUsers(weekDate.getTime(), new Handler<JsonArray>() {
            @Override
            public void handle(JsonArray event) {
                if (event != null && event.size() > 0) {
                    notifiedUsers.addAll(event.getList());
                    endPage.set((event.size() / USERS_LIMIT) + (event.size() % USERS_LIMIT != 0 ? 1 : 0));
                } else {
                    handler.handle(new Either.Right<String, JsonObject>(results));
                    return;
                }
                getNotificationsDefaults(new Handler<JsonArray>() {
                    public void handle(final JsonArray notifications) {
                        if (notifications == null) {
                            log.error("[sendWeeklyMails] Error while retrieving notifications defaults.");
                        } else {
                            for (Object notifObj : notifications) {
                                final JsonObject notif = (JsonObject) notifObj;
                                notificationsDefaults.put(notif.getString("key", ""), notif);
                            }
                            userContinuationHandler.handle(true);
                        }
                    }
                });

            }
        });
    }

    @Override
    public void getNotificationsDefaults(final Handler<JsonArray> handler) {
        configService.list(new Handler<Either<String, JsonArray>>() {
            public void handle(Either<String, JsonArray> event) {
                if (event.isLeft()) {
                    handler.handle(null);
                } else {
                    JsonArray config = event.right().getValue();

                    JsonArray notificationsList = new fr.wseduc.webutils.collections.JsonArray();
                    for (String key : registeredNotifications.keySet()) {
                        JsonObject notif = new JsonObject(registeredNotifications.get(key));
                        notif.put("key", key);
                        for (Object notifConfigObj : config) {
                            JsonObject notifConfig = (JsonObject) notifConfigObj;
                            if (notifConfig.getString("key", "").equals(key)) {
                                notif.put("defaultFrequency", notifConfig.getString("defaultFrequency",
                                        notif.getString("defaultFrequency")));
                                notif.put("restriction",
                                        notifConfig.getString("restriction", notif.getString("restriction")));
                                break;
                            }
                        }
                        notificationsList.add(notif);
                    }
                    handler.handle(notificationsList);
                }
            }
        });
    }

    /**
     * Retrieves all timeline notifications from mongodb for a single user, from a specific date in the past.
     *
     * @param userId : Userid
     * @param from : The starting date
     * @param handler : Handles the notifications
     */
    private void getUserNotifications(String userId, Date from, final Handler<JsonArray> handler) {
        JsonObject matcher = MongoQueryBuilder.build(QueryBuilder.start("recipients")
                .elemMatch(QueryBuilder.start("userId").is(userId).get()).and("date").greaterThanEquals(from));

        JsonObject keys = new JsonObject().put("_id", 0).put("type", 1).put("event-type", 1).put("params", 1)
                .put("date", 1);
        mongo.find("timeline", matcher, null, keys, new Handler<Message<JsonObject>>() {
            public void handle(Message<JsonObject> event) {
                if ("error".equals(event.body().getString("status", "error"))) {
                    handler.handle(new fr.wseduc.webutils.collections.JsonArray());
                } else {
                    handler.handle(event.body().getJsonArray("results"));
                }
            }
        });
    }

    /**
     * Returns either user preferences or defaults when the user has not chosen specific values.
     *
     * @param field : Which preference
     * @param userPrefs : User preferences
     * @param defaultPrefs : Default preferences
     * @return The prevailing preference
     */
    private String notificationPrefsMixin(String field, JsonObject userPrefs, JsonObject defaultPrefs) {
        return userPrefs.getString(field, defaultPrefs.getString(field, ""));
    }

    private void getRecipientsUsers(Date from, final Handler<JsonArray> handler) {
        final JsonObject aggregation = new JsonObject();
        JsonArray pipeline = new fr.wseduc.webutils.collections.JsonArray();
        aggregation.put("aggregate", "timeline").put("allowDiskUse", true).put("pipeline", pipeline);

        JsonObject matcher = MongoQueryBuilder.build(QueryBuilder.start("date").greaterThanEquals(from));
        JsonObject grouper = new JsonObject(
                "{ \"_id\" : \"notifiedUsers\", \"recipients\" : {\"$addToSet\" : \"$recipients.userId\"}}");

        pipeline.add(new JsonObject().put("$match", matcher));
        pipeline.add(new JsonObject().put("$unwind", "$recipients"));
        pipeline.add(new JsonObject().put("$group", grouper));

        mongo.command(aggregation.toString(), new Handler<Message<JsonObject>>() {
            @Override
            public void handle(Message<JsonObject> event) {
                if ("error".equals(event.body().getString("status", "error"))) {
                    handler.handle(new fr.wseduc.webutils.collections.JsonArray());
                } else {
                    JsonArray r = event.body().getJsonObject("result", new JsonObject()).getJsonArray("result");
                    if (r != null && r.size() > 0) {
                        handler.handle(r.getJsonObject(0).getJsonArray("recipients",
                                new fr.wseduc.webutils.collections.JsonArray()));
                    } else {
                        handler.handle(new fr.wseduc.webutils.collections.JsonArray());
                    }
                }
            }

        });
    }

    /**
     * Retrieves users having an email address, paginated.
     *
     * @param page : Page number
     * @param handler : Handles the users
     */
    private void getImpactedUsers(List<String> recipients, int page,
            final Handler<Either<String, JsonArray>> handler) {
        int fromIdx = page * USERS_LIMIT;
        int toIdx = page * USERS_LIMIT + USERS_LIMIT;
        if (fromIdx >= recipients.size()) {
            handler.handle(new Either.Right<String, JsonArray>(new fr.wseduc.webutils.collections.JsonArray()));
            return;
        }
        if (toIdx > recipients.size()) {
            toIdx = recipients.size();
        }
        final String query = "MATCH (u:User)-[:IN]->(g:Group)-[:AUTHORIZED]->(r:Role)-[:AUTHORIZE]->(act:WorkflowAction) "
                + "WHERE u.id IN {notifiedUsers} AND u.activationCode IS NULL AND u.email IS NOT NULL AND length(u.email) > 0 "
                + "AND act.name = \"org.entcore.timeline.controllers.TimelineController|mixinConfig\""
                + "RETURN DISTINCT u.email as mail, u.id as id ";
        JsonObject params = new JsonObject().put("notifiedUsers",
                new fr.wseduc.webutils.collections.JsonArray(recipients.subList(fromIdx, toIdx)));
        neo4j.execute(query, params, Neo4jResult.validResultHandler(handler));
    }

    /**
     * Retrieves an aggregated list of notifications from mongodb for a single user.
     *
     *  Notifications are grouped by type & event-type.
     * @param userId : Userid
     * @param from : Starting date in the past
     * @param handler: Handles the notifications
     */
    private void getAggregatedUserNotifications(String userId, Date from, final Handler<JsonArray> handler) {
        final JsonObject aggregation = new JsonObject();
        JsonArray pipeline = new fr.wseduc.webutils.collections.JsonArray();
        aggregation.put("aggregate", "timeline").put("allowDiskUse", true).put("pipeline", pipeline);

        JsonObject matcher = MongoQueryBuilder.build(QueryBuilder.start("recipients")
                .elemMatch(QueryBuilder.start("userId").is(userId).get()).and("date").greaterThanEquals(from));
        JsonObject grouper = new JsonObject(
                "{ \"_id\" : { \"type\": \"$type\", \"event-type\": \"$event-type\"}, \"count\": { \"$sum\": 1 } }");
        JsonObject transformer = new JsonObject(
                "{ \"type\": \"$_id.type\", \"event-type\": \"$_id.event-type\", \"count\": 1, \"_id\": 0 }");

        pipeline.add(new JsonObject().put("$match", matcher));
        pipeline.add(new JsonObject().put("$group", grouper));
        pipeline.add(new JsonObject().put("$project", transformer));

        mongo.command(aggregation.toString(), new Handler<Message<JsonObject>>() {
            @Override
            public void handle(Message<JsonObject> event) {
                if ("error".equals(event.body().getString("status", "error"))) {
                    handler.handle(new fr.wseduc.webutils.collections.JsonArray());
                } else {
                    handler.handle(event.body().getJsonObject("result", new JsonObject()).getJsonArray("result",
                            new fr.wseduc.webutils.collections.JsonArray()));
                }
            }

        });
    }

    public void setConfigService(TimelineConfigService configService) {
        this.configService = configService;
    }

    public void setRegisteredNotifications(Map<String, String> registeredNotifications) {
        this.registeredNotifications = registeredNotifications;
    }

    public void setEventsI18n(LocalMap<String, String> eventsI18n) {
        this.eventsI18n = eventsI18n;
    }

    public void setLazyEventsI18n(HashMap<String, JsonObject> lazyEventsI18n) {
        this.lazyEventsI18n = lazyEventsI18n;
    }
}