org.entcore.conversation.service.impl.DefaultConversationService.java Source code

Java tutorial

Introduction

Here is the source code for org.entcore.conversation.service.impl.DefaultConversationService.java

Source

/* Copyright  "Open Digital Education", 2014
 *
 * 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.conversation.service.impl;

import static org.entcore.common.neo4j.Neo4jResult.*;
import static org.entcore.common.user.UserUtils.findVisibleUsers;
import static org.entcore.common.user.UserUtils.findVisibles;
import fr.wseduc.webutils.Server;

import org.entcore.common.neo4j.Neo;
import org.entcore.common.neo4j.StatementsBuilder;
import org.entcore.common.user.UserInfos;
import org.entcore.common.user.UserUtils;
import org.entcore.common.utils.Config;
import org.entcore.conversation.Conversation;
import org.entcore.conversation.service.ConversationService;

import fr.wseduc.webutils.Either;
import fr.wseduc.webutils.Utils;

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.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.logging.LoggerFactory;

import java.util.*;

public class DefaultConversationService implements ConversationService {

    private final EventBus eb;
    private final Neo neo;
    private final String applicationName;
    private final int maxFolderDepth;

    public DefaultConversationService(Vertx vertx, String applicationName) {
        eb = Server.getEventBus(vertx);
        neo = new Neo(vertx, eb, LoggerFactory.getLogger(Neo.class));
        this.applicationName = applicationName;
        this.maxFolderDepth = Config.getConf().getInteger("max-folder-depth", Conversation.DEFAULT_FOLDER_DEPTH);
    }

    @Override
    public void saveDraft(final String parentMessageId, final String threadId, final JsonObject message,
            final UserInfos user, final Handler<Either<String, JsonObject>> result) {
        if (displayNamesCondition(message)) {
            addDisplayNames(message, new Handler<JsonObject>() {
                @Override
                public void handle(JsonObject object) {
                    save(parentMessageId, object, user, result);
                }
            });
        } else {
            save(parentMessageId, message, user, result);
        }
    }

    private void addDisplayNames(final JsonObject message, final Handler<JsonObject> handler) {
        String query = "MATCH (v:Visible) " + "WHERE v.id IN {ids} "
                + "RETURN COLLECT(distinct (v.id + '$' + coalesce(v.displayName, ' ') + '$' + "
                + "coalesce(v.name, ' ') + '$' + coalesce(v.groupDisplayName, ' '))) as displayNames ";
        Set<String> ids = new HashSet<>();
        ids.addAll(message.getJsonArray("to", new fr.wseduc.webutils.collections.JsonArray()).getList());
        ids.addAll(message.getJsonArray("cc", new fr.wseduc.webutils.collections.JsonArray()).getList());
        if (message.containsKey("from")) {
            ids.add(message.getString("from"));
        }
        neo.execute(query,
                new JsonObject().put("ids", new fr.wseduc.webutils.collections.JsonArray(new ArrayList<>(ids))),
                new Handler<Message<JsonObject>>() {
                    @Override
                    public void handle(Message<JsonObject> m) {
                        JsonArray r = m.body().getJsonArray("result");
                        if ("ok".equals(m.body().getString("status")) && r != null && r.size() == 1) {
                            JsonObject j = r.getJsonObject(0);
                            JsonArray d = j.getJsonArray("displayNames");
                            if (d != null && d.size() > 0) {
                                message.put("displayNames", d);
                            }
                        }
                        handler.handle(message);
                    }
                });
    }

    private boolean displayNamesCondition(JsonObject message) {
        return message != null && ((message.containsKey("from") && !message.getString("from").trim().isEmpty())
                || (message.containsKey("to") && message.getJsonArray("to").size() > 0)
                || (message.containsKey("cc") && message.getJsonArray("cc").size() > 0));
    }

    private void save(String parentMessageId, JsonObject message, UserInfos user,
            Handler<Either<String, JsonObject>> result) {
        if (message == null) {
            message = new JsonObject();
        }
        message.put("id", UUID.randomUUID().toString()).put("date", System.currentTimeMillis())
                .put("from", user.getUserId()).put("state", State.DRAFT.name());
        JsonObject m = Utils.validAndGet(message, MESSAGE_FIELDS, DRAFT_REQUIRED_FIELDS);
        if (validationError(user, m, result))
            return;
        String query;
        JsonObject params = new JsonObject().put("userId", user.getUserId()).put("folderName", "DRAFT")
                .put("props", m).put("true", true);
        if (parentMessageId != null && !parentMessageId.trim().isEmpty()) { // reply
            query = "MATCH (c:Conversation)-[:HAS_CONVERSATION_FOLDER]->(f:ConversationSystemFolder), "
                    + "(c:Conversation)-[:HAS_CONVERSATION_FOLDER]->(af:ConversationSystemFolder)"
                    + "-[:HAS_CONVERSATION_MESSAGE]->(pm:ConversationMessage) "
                    + "WHERE c.userId = {userId} AND c.active = {true} AND f.name = {folderName} "
                    + "AND pm.id = {parentMessageId} " + "WITH distinct f, pm, count(distinct f) as nb "
                    + "WHERE nb = 1 " + "CREATE f-[:HAS_CONVERSATION_MESSAGE]->(m:ConversationMessage {props})"
                    + "-[:PARENT_CONVERSATION_MESSAGE]->pm " + "RETURN m.id as id";
            params.put("parentMessageId", parentMessageId);
        } else {
            query = "MATCH (c:Conversation)-[:HAS_CONVERSATION_FOLDER]->(f:ConversationSystemFolder) "
                    + "WHERE c.userId = {userId} AND c.active = {true} AND f.name = {folderName} "
                    + "WITH f, count(f) as nb " + "WHERE nb = 1 "
                    + "CREATE f-[:HAS_CONVERSATION_MESSAGE]->(m:ConversationMessage {props}) "
                    + "RETURN m.id as id";
        }
        neo.execute(query, params, validUniqueResultHandler(result));
    }

    @Override
    public void updateDraft(final String messageId, final JsonObject message, final UserInfos user,
            final Handler<Either<String, JsonObject>> result) {
        if (displayNamesCondition(message)) {
            addDisplayNames(message, new Handler<JsonObject>() {
                @Override
                public void handle(JsonObject object) {
                    update(messageId, object, user, result);
                }
            });
        } else {
            update(messageId, message, user, result);
        }
    }

    private void update(String messageId, JsonObject message, UserInfos user,
            Handler<Either<String, JsonObject>> result) {
        if (message == null) {
            message = new JsonObject();
        }
        message.put("date", System.currentTimeMillis());
        JsonObject m = Utils.validAndGet(message, UPDATE_DRAFT_FIELDS, UPDATE_DRAFT_REQUIRED_FIELDS);
        if (validationError(user, m, result, messageId))
            return;
        String query = "MATCH (m:ConversationMessage)<-[:HAS_CONVERSATION_MESSAGE]-f"
                + "<-[r:HAS_CONVERSATION_FOLDER]-(c:Conversation) "
                + "WHERE m.id = {id} AND m.from = {userId} AND m.state = {state} AND c.active = {true} " + "SET "
                + nodeSetPropertiesFromJson("m", m) + "RETURN m.id as id";
        m.put("userId", user.getUserId()).put("id", messageId).put("state", State.DRAFT.name()).put("true", true);
        neo.execute(query, m, validUniqueResultHandler(result));
    }

    @Override
    public void send(final String parentMessageId, final String draftId, JsonObject message, final UserInfos user,
            final Handler<Either<String, JsonObject>> result) {
        Handler<Either<String, JsonObject>> handler = new Handler<Either<String, JsonObject>>() {
            @Override
            public void handle(Either<String, JsonObject> event) {
                if (draftId != null && !draftId.trim().isEmpty()) {
                    send(parentMessageId, draftId, user, result);
                } else if (event.isRight()) {
                    send(parentMessageId, event.right().getValue().getString("id"), user, result);
                } else {
                    result.handle(event);
                }
            }
        };
        if (draftId != null && !draftId.trim().isEmpty()) {
            update(draftId, message, user, handler);
        } else {
            save(parentMessageId, message, user, handler);
        }
    }

    private void send(final String parentMessageId, final String messageId, final UserInfos user,
            final Handler<Either<String, JsonObject>> result) {
        if (validationParamsError(user, result, messageId))
            return;

        String attachmentsRetrieval = "MATCH (message:ConversationMessage)<-[link:HAS_CONVERSATION_MESSAGE]-(:ConversationSystemFolder)"
                + "<-[:HAS_CONVERSATION_FOLDER]-(c:Conversation) "
                + "WHERE message.id = {messageId} AND message.state = {draft} AND message.from = {userId} AND c.userId = {userId} AND c.active = {true} "
                + "OPTIONAL MATCH (message)-[:HAS_ATTACHMENT]->(a: MessageAttachment) "
                + "WHERE a.id IN link.attachments "
                + "WITH CASE WHEN a IS NULL THEN [] ELSE collect({id: a.id, size: a.size}) END as attachments "
                + "RETURN attachments";

        JsonObject params = new JsonObject().put("userId", user.getUserId()).put("messageId", messageId)
                .put("draft", State.DRAFT.name()).put("true", true);

        neo.execute(attachmentsRetrieval, params,
                validUniqueResultHandler(new Handler<Either<String, JsonObject>>() {
                    public void handle(Either<String, JsonObject> event) {
                        if (event.isLeft() || event.isRight() && event.right().getValue() == null) {
                            result.handle(new Either.Left<String, JsonObject>("conversation.send.error"));
                            return;
                        }

                        JsonArray attachments = event.right().getValue().getJsonArray("attachments",
                                new fr.wseduc.webutils.collections.JsonArray());

                        if (attachments.size() < 1) {
                            sendWithoutAttachments(parentMessageId, messageId, user, result);
                        } else {
                            sendWithAttachments(parentMessageId, messageId, attachments, user, result);
                        }

                    }
                }));
    }

    private void sendWithoutAttachments(final String parentMessageId, String messageId, UserInfos user,
            final Handler<Either<String, JsonObject>> result) {
        String usersQuery;
        JsonObject params = new JsonObject().put("userId", user.getUserId()).put("messageId", messageId)
                .put("draft", State.DRAFT.name()).put("outbox", "OUTBOX").put("inbox", "INBOX")
                .put("sent", State.SENT.name()).put("true", true);
        if (parentMessageId != null && !parentMessageId.trim().isEmpty()) { // reply
            usersQuery = "MATCH (m:ConversationMessage { id : {parentMessageId}}) "
                    + "WITH (COLLECT(visibles.id) + coalesce(m.to, '') + coalesce(m.cc, '') + coalesce(m.from, '')) as vis "
                    + "MATCH (v:Visible) " + "WHERE v.id IN vis " + "WITH DISTINCT v ";
            params.put("parentMessageId", parentMessageId);
        } else {
            usersQuery = "WITH visibles as v ";
        }
        String query = usersQuery + "MATCH v<-[:IN*0..1]-(u:User), (message:ConversationMessage) "
                + "WHERE (v: User or v:Group) "
                + "AND message.id = {messageId} AND message.state = {draft} AND message.from = {userId} AND "
                + "(v.id IN message.to OR v.id IN message.cc) "
                + "WITH DISTINCT u, message, (v.id + '$' + coalesce(v.displayName, ' ') + '$' + "
                + "coalesce(v.name, ' ') + '$' + coalesce(v.groupDisplayName, ' ')) as dNames "
                + "MATCH u-[:HAS_CONVERSATION]->(c:Conversation {active:{true}})"
                + "-[:HAS_CONVERSATION_FOLDER]->(f:ConversationSystemFolder {name:{inbox}}) "
                + "CREATE UNIQUE f-[:HAS_CONVERSATION_MESSAGE { unread: {true} }]->message "
                + "WITH COLLECT(c.userId) as sentIds, COLLECT(u) as users, message, "
                + "COLLECT(distinct dNames) as displayNames "
                + "MATCH (s:User {id : {userId}})-[:HAS_CONVERSATION]->(:Conversation)"
                + "-[:HAS_CONVERSATION_FOLDER]->(fOut:ConversationSystemFolder {name : {outbox}}), "
                + "message<-[r:HAS_CONVERSATION_MESSAGE]-(fDraft:ConversationSystemFolder {name : {draft}}) "
                + "SET message.state = {sent}, "
                + "message.displayNames = displayNames + (s.id + '$' + coalesce(s.displayName, ' ') + '$ $ ') "
                + "CREATE UNIQUE fOut-[:HAS_CONVERSATION_MESSAGE { insideFolder: r.insideFolder }]->message "
                + "DELETE r "
                + "RETURN EXTRACT(u IN FIlTER(x IN users WHERE NOT(x.id IN sentIds)) | u.displayName) as undelivered,  "
                + "EXTRACT(u IN FIlTER(x IN users WHERE x.id IN sentIds AND NOT(x.activationCode IS NULL)) "
                + "| u.displayName) as inactive, LENGTH(sentIds) as sent, "
                + "sentIds, message.id as id, message.subject as subject";
        findVisibles(eb, user.getUserId(), query, params, true, true, false, new Handler<JsonArray>() {
            @Override
            public void handle(JsonArray event) {
                if (event != null && event.size() == 1 && (event.getValue(0) instanceof JsonObject)) {
                    result.handle(new Either.Right<String, JsonObject>(event.getJsonObject(0)));
                } else {
                    result.handle(new Either.Left<String, JsonObject>("conversation.send.error"));
                }
            }
        });
    }

    private void sendWithAttachments(final String parentMessageId, final String messageId, JsonArray attachments,
            final UserInfos user, final Handler<Either<String, JsonObject>> result) {
        long totalAttachmentsSize = 0l;
        for (Object o : attachments) {
            totalAttachmentsSize = totalAttachmentsSize + ((JsonObject) o).getLong("size", 0l);
        }

        final String usersQuery;
        JsonObject params = new JsonObject().put("userId", user.getUserId()).put("messageId", messageId)
                .put("draft", State.DRAFT.name()).put("outbox", "OUTBOX").put("inbox", "INBOX")
                .put("sent", State.SENT.name()).put("attachmentsSize", totalAttachmentsSize).put("true", true);
        if (parentMessageId != null && !parentMessageId.trim().isEmpty()) { // reply
            usersQuery = "MATCH (m:ConversationMessage { id : {parentMessageId}}) "
                    + "WITH (COLLECT(visibles.id) + coalesce(m.to, '') + coalesce(m.cc, '') + coalesce(m.from, '')) as vis "
                    + "MATCH (v:Visible) " + "WHERE v.id IN vis " + "WITH DISTINCT v ";
            params.put("parentMessageId", parentMessageId);
        } else {
            usersQuery = "WITH visibles as v ";
        }

        String query = usersQuery
                + "MATCH v<-[:IN*0..1]-(u:User), (message:ConversationMessage)-[:HAS_ATTACHMENT]->(attachment: MessageAttachment) "
                + "WHERE (v: User or v:Group) "
                + "AND message.id = {messageId} AND message.state = {draft} AND message.from = {userId} AND "
                + "(v.id IN message.to OR v.id IN message.cc) "
                + "WITH DISTINCT u, message, (v.id + '$' + coalesce(v.displayName, ' ') + '$' + "
                + "coalesce(v.name, ' ') + '$' + coalesce(v.groupDisplayName, ' ')) as dNames, COLLECT(distinct attachment.id) as attachments "
                + "MATCH (ub: UserBook)<-[:USERBOOK]-(u)-[:HAS_CONVERSATION]->(c:Conversation {active:{true}})"
                + "-[:HAS_CONVERSATION_FOLDER]->(f:ConversationSystemFolder {name:{inbox}}) "
                + "WHERE (ub.quota - ub.storage) >= {attachmentsSize} "
                + "CREATE UNIQUE f-[:HAS_CONVERSATION_MESSAGE { unread: {true}, attachments: attachments }]->message "
                + "SET ub.storage = ub.storage + {attachmentsSize} "
                + "WITH message, COLLECT(c.userId) as sentIds, COLLECT(distinct u) as users, "
                + "COLLECT(distinct dNames) as displayNames "
                + "MATCH (s:User {id : {userId}})-[:HAS_CONVERSATION]->(:Conversation)"
                + "-[:HAS_CONVERSATION_FOLDER]->(fOut:ConversationSystemFolder {name : {outbox}}), "
                + "message<-[r:HAS_CONVERSATION_MESSAGE]-(fDraft:ConversationSystemFolder {name : {draft}}) "
                + "SET message.state = {sent}, "
                + "message.displayNames = displayNames + (s.id + '$' + coalesce(s.displayName, ' ') + '$ $ ') "
                + "CREATE UNIQUE fOut-[:HAS_CONVERSATION_MESSAGE { insideFolder: r.insideFolder, attachments: r.attachments }]->message "
                + "DELETE r " + "RETURN sentIds, message.id as id";

        findVisibles(eb, user.getUserId(), query, params, true, true, false, new Handler<JsonArray>() {
            @Override
            public void handle(JsonArray event) {
                if (event != null && event.size() == 1 && (event.getValue(0) instanceof JsonObject)) {

                    JsonObject resultObj = event.getJsonObject(0);
                    JsonArray sentIds = resultObj.getJsonArray("sentIds");
                    String messageId = resultObj.getString("id");

                    String query = usersQuery + "MATCH v<-[:IN*0..1]-(u:User), (message:ConversationMessage) "
                            + "WHERE (v: User or v:Group) "
                            + "AND message.id = {messageId} AND message.from = {userId} AND "
                            + "(v.id IN message.to OR v.id IN message.cc) "
                            + "RETURN EXTRACT(user IN FIlTER(x IN COLLECT(u) WHERE NOT(x.id IN {sentIds}))|user.displayName) as undelivered, {sentIds} as sentIds, [] as inactive, "
                            + "{sentIdsLength} as sent, message.id as id, message.subject as subject";

                    JsonObject params = new JsonObject().put("userId", user.getUserId()).put("messageId", messageId)
                            .put("sentIds", sentIds).put("sentIdsLength", sentIds.size());
                    if (parentMessageId != null && !parentMessageId.trim().isEmpty()) {
                        params.put("parentMessageId", parentMessageId);
                    }

                    findVisibles(eb, user.getUserId(), query, params, true, true, false, new Handler<JsonArray>() {
                        @Override
                        public void handle(JsonArray event) {
                            if (event != null && event.size() == 1 && (event.getValue(0) instanceof JsonObject)) {
                                result.handle(new Either.Right<String, JsonObject>(event.getJsonObject(0)));
                            } else {
                                result.handle(new Either.Left<String, JsonObject>("conversation.send.error"));
                            }
                        }
                    });
                } else {
                    result.handle(new Either.Left<String, JsonObject>("conversation.send.error"));
                }
            }
        });
    }

    @Override
    public void list(String folder, String restrain, Boolean unread, UserInfos user, int page, String searchWords,
            final Handler<Either<String, JsonArray>> results) {
        if (validationError(user, results, folder))
            return;
        int skip = page * LIST_LIMIT;

        JsonObject params = new JsonObject().put("userId", user.getUserId()).put("folder", folder).put("skip", skip)
                .put("limit", LIST_LIMIT).put("true", true);

        String messageFilter = "";
        if (restrain != null) {
            messageFilter = messageFilter + "-[:HAS_CONVERSATION_FOLDER]->(:ConversationUserFolder)"
                    + "-[:HAS_CHILD_FOLDER*0.." + (maxFolderDepth - 1)
                    + "]->(child: ConversationUserFolder {id: {userFolderId}})"
                    + "<-[i: INSIDE]-(m:ConversationMessage)<-[r:HAS_CONVERSATION_MESSAGE]-(f: ConversationSystemFolder)<-[:HAS_CONVERSATION_FOLDER]-(c) "
                    + "WHERE NOT HAS(i.trashed) ";

            params.put("userFolderId", folder);
        } else {
            messageFilter = messageFilter
                    + "-[:HAS_CONVERSATION_FOLDER]->(f:ConversationSystemFolder {name : {folder}})"
                    + "-[r:HAS_CONVERSATION_MESSAGE]->(m:ConversationMessage) " + "WHERE NOT HAS(r.insideFolder) ";
        }

        String query = "MATCH (c:Conversation {userId : {userId}, active : {true}})" + messageFilter
                + "RETURN DISTINCT m.id as id, m.to as to, m.from as from, m.state as state, "
                + "m.toName as toName, m.fromName as fromName, "
                + "m.subject as subject, m.date as date, r.unread as unread, m.displayNames as displayNames, coalesce(r.attachments, []) as attachments,  collect(f.name) as systemFolders "
                + "ORDER BY m.date DESC " + "SKIP {skip} " + "LIMIT {limit} ";

        neo.execute(query, params, validResultHandler(results));
    }

    @Override
    public void listThreads(UserInfos user, int page, Handler<Either<String, JsonArray>> results) {

    }

    @Override
    public void listThreadMessages(String threadId, int page, UserInfos user,
            Handler<Either<String, JsonArray>> results) {

    }

    @Override
    public void listThreadMessagesNavigation(String messageId, boolean previous, UserInfos user,
            Handler<Either<String, JsonArray>> results) {

    }

    @Override
    public void trash(List<String> messagesId, UserInfos user, Handler<Either<String, JsonObject>> result) {
        if (validationParamsError(user, result))
            return;

        String query = "MATCH (m:ConversationMessage)<-[r:HAS_CONVERSATION_MESSAGE]-(f:ConversationSystemFolder)"
                + "<-[:HAS_CONVERSATION_FOLDER]-(c:Conversation)-[:HAS_CONVERSATION_FOLDER]->"
                + "(df:ConversationSystemFolder) "
                + "WHERE m.id = {messageId} AND c.userId = {userId} AND c.active = {true} AND df.name = {trash} "
                + "CREATE UNIQUE df-[:HAS_CONVERSATION_MESSAGE { restoreFolder: f.name, wasInsideFolder: r.insideFolder, attachments: r.attachments }]->m "
                + "DELETE r " + "WITH c, m "
                + "MATCH (m)-[i: INSIDE]->(:ConversationUserFolder)<-[:HAS_CHILD_FOLDER*0.." + (maxFolderDepth - 1)
                + "]-(:ConversationUserFolder)<-[:HAS_CONVERSATION_FOLDER]-(c) " + "SET i.trashed = true";

        StatementsBuilder b = new StatementsBuilder();
        for (String id : messagesId) {
            JsonObject params = new JsonObject().put("userId", user.getUserId()).put("messageId", id)
                    .put("true", true).put("trash", "TRASH");
            b.add(query, params);
        }
        neo.executeTransaction(b.build(), null, true, validUniqueResultHandler(result));
    }

    @Override
    public void restore(List<String> messagesId, UserInfos user, Handler<Either<String, JsonObject>> result) {
        if (validationParamsError(user, result))
            return;

        String query = "MATCH (m:ConversationMessage)<-[r:HAS_CONVERSATION_MESSAGE]-(f:ConversationSystemFolder)"
                + "<-[:HAS_CONVERSATION_FOLDER]-(c:Conversation)-[:HAS_CONVERSATION_FOLDER]->"
                + "(df:ConversationSystemFolder) "
                + "WHERE m.id = {messageId} AND c.userId = {userId} AND c.active = {true} AND f.name = {trash} "
                + "AND df.name = r.restoreFolder "
                + "CREATE UNIQUE df-[:HAS_CONVERSATION_MESSAGE {insideFolder: r.wasInsideFolder, attachments: r.attachments}]->m "
                + "DELETE r " + "WITH c, m "
                + "MATCH (m)-[i: INSIDE]->(:ConversationUserFolder)<-[:HAS_CHILD_FOLDER*0.." + (maxFolderDepth - 1)
                + "]-(:ConversationUserFolder)<-[:HAS_CONVERSATION_FOLDER]-(c) " + "REMOVE i.trashed";

        StatementsBuilder b = new StatementsBuilder();
        for (String id : messagesId) {
            JsonObject params = new JsonObject().put("userId", user.getUserId()).put("messageId", id)
                    .put("true", true).put("trash", "TRASH");
            b.add(query, params);
        }
        neo.executeTransaction(b.build(), null, true, validUniqueResultHandler(result));
    }

    @Override
    public void delete(List<String> messagesId, Boolean deleteAll, UserInfos user,
            Handler<Either<String, JsonArray>> result) {
        if (validationError(user, result))
            return;

        final String getMessage = "MATCH (m:ConversationMessage)<-[r:HAD_CONVERSATION_MESSAGE]-(f:ConversationSystemFolder)"
                + "<-[:HAS_CONVERSATION_FOLDER]-(c:Conversation) "
                + "WHERE m.id = {messageId} AND c.userId = {userId} AND c.active = {true} AND f.name = {trash} ";
        final String getMessageWithAttachments = "MATCH (a: MessageAttachment)<-[aLink:HAS_ATTACHMENT]-(m:ConversationMessage)<-[r:HAD_CONVERSATION_MESSAGE]-(f:ConversationSystemFolder)"
                + "<-[:HAS_CONVERSATION_FOLDER]-(c:Conversation) "
                + "WHERE m.id = {messageId} AND c.userId = {userId} AND c.active = {true} AND f.name = {trash} ";

        String prepareMessage = "MATCH (m:ConversationMessage)<-[r:HAS_CONVERSATION_MESSAGE]-(f:ConversationSystemFolder)"
                + "<-[:HAS_CONVERSATION_FOLDER]-(c:Conversation) "
                + "WHERE m.id = {messageId} AND c.userId = {userId} AND c.active = {true} AND f.name = {trash} "
                + "OPTIONAL MATCH (m)-[i: INSIDE]->(:ConversationUserFolder)<-[:HAS_CHILD_FOLDER*0.."
                + (maxFolderDepth - 1) + "]-(:ConversationUserFolder)<-[:HAS_CONVERSATION_FOLDER]-(c) "
                + "CREATE f-[:HAD_CONVERSATION_MESSAGE {attachments: r.attachments}]->m " + "DELETE r, i ";

        String getAllAttachments = getMessageWithAttachments + "AND a.id IN r.attachments "
                + "RETURN collect({id: a.id, size: a.size}) as attachments";

        String deleteAndCollectAttachments = getMessageWithAttachments
                + "AND NOT(m-[:HAS_CONVERSATION_MESSAGE]-()) " + "DELETE aLink "
                + "WITH a, collect({id: a.id, size: a.size}) as attachments "
                + "WHERE NOT((:ConversationMessage)-[:HAS_ATTACHMENT]->(a)) " + "DELETE a " + "RETURN attachments";

        String deleteMessage = getMessage + "OPTIONAL MATCH m-[pr:PARENT_CONVERSATION_MESSAGE]-() "
                + "WITH m as message, pr " + "MATCH message<-[r:HAD_CONVERSATION_MESSAGE]-() "
                + "WHERE NOT(message-[:HAS_CONVERSATION_MESSAGE]-()) " + "DELETE r, pr, message";

        StatementsBuilder b = new StatementsBuilder();
        for (String id : messagesId) {
            JsonObject params = new JsonObject().put("userId", user.getUserId()).put("messageId", id)
                    .put("true", true).put("trash", "TRASH");
            b.add(prepareMessage, params);
            b.add(getAllAttachments, params);
            b.add(deleteAndCollectAttachments, params);
            b.add(deleteMessage, params);
        }
        neo.executeTransaction(b.build(), null, true, validResultsHandler(result));
    }

    @Override
    public void get(String messageId, UserInfos user, Handler<Either<String, JsonObject>> result) {
        if (validationParamsError(user, result, messageId))
            return;

        String query = "MATCH (m:ConversationMessage)<-[r:HAS_CONVERSATION_MESSAGE]-(f:ConversationSystemFolder)"
                + "<-[:HAS_CONVERSATION_FOLDER]-(c:Conversation) "
                + "WHERE m.id = {messageId} AND c.userId = {userId} AND c.active = {true} "
                + "OPTIONAL MATCH (m)-[:HAS_ATTACHMENT]->(attachments: MessageAttachment) "
                + "WHERE attachments.id IN r.attachments " + "WITH CASE WHEN count(attachments) > 0 THEN "
                + "collect({id: attachments.id, name: attachments.name, filename: attachments.filename, contentType: attachments.contentType, "
                + "contentTransferEncoding: attachments.contentTransferEncoding, charset: attachments.charset, size: attachments.size}) "
                + "ELSE [] END as attachments, m, r, f " + "SET r.unread = {false} "
                + "RETURN distinct m.id as id, m.to as to, m.cc as cc, m.from as from, m.state as state, "
                + "m.subject as subject, m.date as date, m.body as body, m.toName as toName, "
                + "m.ccName as ccName, m.fromName as fromName, m.displayNames as displayNames, attachments, collect(f.name) as systemFolders";
        JsonObject params = new JsonObject().put("userId", user.getUserId()).put("messageId", messageId)
                .put("true", true).put("false", false);
        neo.execute(query, params, validUniqueResultHandler(result));
    }

    @Override
    public void count(String folder, String restrain, Boolean unread, UserInfos user,
            Handler<Either<String, JsonObject>> result) {
        if (validationParamsError(user, result, folder))
            return;
        String condition = "";
        JsonObject params = new JsonObject().put("userId", user.getUserId()).put("folder", folder).put("true",
                true);
        if (unread != null) {
            params.put("unread", unread);
            if (unread) {
                condition = "AND r.unread = {unread} ";
            } else {
                condition = "AND (NOT(HAS(r.unread)) OR r.unread = {unread}) ";
            }
        }
        String query = "MATCH (c:Conversation)-[:HAS_CONVERSATION_FOLDER]->(f:ConversationFolder)"
                + "-[r:HAS_CONVERSATION_MESSAGE]->(m:ConversationMessage) "
                + "WHERE c.userId = {userId} AND c.active = {true} AND f.name = {folder} AND NOT HAS(r.insideFolder)"
                + condition + "RETURN count(m) as count";
        neo.execute(query, params, validUniqueResultHandler(result));
    }

    @Override
    public void findVisibleRecipients(final String parentMessageId, final UserInfos user,
            final String acceptLanguage, String search, final Handler<Either<String, JsonObject>> result) {
        if (validationParamsError(user, result))
            return;
        final JsonObject visible = new JsonObject();
        String replyGroupQuery;
        final JsonObject params = new JsonObject();
        if (parentMessageId != null && !parentMessageId.trim().isEmpty()) {
            params.put("conversation", applicationName);
            replyGroupQuery = ", (m:ConversationMessage)<-[:HAS_CONVERSATION_MESSAGE]-f"
                    + "<-[:HAS_CONVERSATION_FOLDER]-(c:Conversation) "
                    + "WHERE m.id = {parentMessageId} AND c.userId = {userId} "
                    + "AND (pg.id = visibles.id OR pg.id IN m.to OR pg.id IN m.cc) ";
            params.put("userId", user.getUserId()).put("parentMessageId", parentMessageId);
            String groups = "MATCH (app:Application)-[:PROVIDE]->(a:Action)<-[:AUTHORIZE]-(r:Role)"
                    + "<-[:AUTHORIZED]-(g:Group)<-[:DEPENDS*0..1]-(pg:Group) " + replyGroupQuery
                    + " AND app.name = {conversation} "
                    + "RETURN DISTINCT pg.id as id, pg.name as name, pg.groupDisplayName as groupDisplayName, pg.structureName as structureName ";
            findVisibles(eb, user.getUserId(), groups, params, false, true, false, acceptLanguage,
                    new Handler<JsonArray>() {
                        @Override
                        public void handle(JsonArray visibleGroups) {
                            visible.put("groups", visibleGroups);
                            String replyUserQuery;
                            replyUserQuery = ", (m:ConversationMessage)<-[:HAS_CONVERSATION_MESSAGE]-f"
                                    + "<-[:HAS_CONVERSATION_FOLDER]-(c:Conversation) "
                                    + "WHERE m.id = {parentMessageId} AND c.userId = {userId} "
                                    + "AND (u.id = visibles.id OR u.id IN m.to OR u.id IN m.cc) ";
                            String users = "MATCH (app:Application)-[:PROVIDE]->(a:Action)<-[:AUTHORIZE]-(r:Role)"
                                    + "<-[:AUTHORIZED]-(pg:Group)<-[:IN]-(u:User) " + replyUserQuery
                                    + "AND app.name = {conversation} "
                                    + "RETURN DISTINCT u.id as id, u.displayName as displayName, "
                                    + "visibles.profiles[0] as profile";
                            findVisibleUsers(eb, user.getUserId(), true, true, users, params,
                                    new Handler<JsonArray>() {
                                        @Override
                                        public void handle(JsonArray visibleUsers) {
                                            visible.put("users", visibleUsers);
                                            result.handle(new Either.Right<String, JsonObject>(visible));
                                        }
                                    });
                        }
                    });
        } else {
            params.put("true", true);
            String groups = "MATCH visibles<-[:IN*0..1]-(u:User)-[:HAS_CONVERSATION]->(c:Conversation {active:{true}}) "
                    + "RETURN DISTINCT visibles.id as id, visibles.name as name, "
                    + "visibles.displayName as displayName, visibles.groupDisplayName as groupDisplayName, "
                    + "visibles.profiles[0] as profile, visibles.structureName as structureName";
            findVisibles(eb, user.getUserId(), groups, params, true, true, false, new Handler<JsonArray>() {
                @Override
                public void handle(JsonArray visibles) {
                    JsonArray users = new fr.wseduc.webutils.collections.JsonArray();
                    JsonArray groups = new fr.wseduc.webutils.collections.JsonArray();
                    visible.put("groups", groups).put("users", users);
                    for (Object o : visibles) {
                        if (!(o instanceof JsonObject))
                            continue;
                        JsonObject j = (JsonObject) o;
                        if (j.getString("name") != null) {
                            j.remove("displayName");
                            UserUtils.groupDisplayName(j, acceptLanguage);
                            groups.add(j);
                        } else {
                            j.remove("name");
                            users.add(j);
                        }
                    }
                    result.handle(new Either.Right<String, JsonObject>(visible));
                }
            });
        }
    }

    @Override
    public void toggleUnread(List<String> messagesId, boolean unread, UserInfos user,
            Handler<Either<String, JsonObject>> result) {
        // Deprecated
    }

    @Override
    public void createFolder(String folderName, String parentFolderId, UserInfos user,
            Handler<Either<String, JsonObject>> result) {
        if (validationParamsError(user, result, folderName))
            return;

        JsonObject newFolderProps = new JsonObject().put("id", UUID.randomUUID().toString()).put("name",
                folderName);

        String completeQuery = "";

        JsonObject params = new JsonObject().put("userId", user.getUserId()).put("true", true).put("props",
                newFolderProps);

        if (parentFolderId == null) {
            completeQuery = completeQuery + "MATCH (c:Conversation) "
                    + "WHERE c.userId = {userId} AND c.active = {true} "
                    + "CREATE UNIQUE (c)-[:HAS_CONVERSATION_FOLDER]->(nf: ConversationFolder:ConversationUserFolder {props}) "
                    + "RETURN nf.id as id";
        } else {
            completeQuery = completeQuery
                    + "MATCH (c:Conversation)-[:HAS_CONVERSATION_FOLDER]->(f:ConversationUserFolder)-[:HAS_CHILD_FOLDER*0.."
                    + (maxFolderDepth - 1) + "]->(pf: ConversationUserFolder) "
                    + "WHERE c.userId = {userId} AND c.active = {true} AND pf.id = {parentId} "
                    + "CREATE UNIQUE (pf)-[:HAS_CHILD_FOLDER]->(nf: ConversationFolder:ConversationUserFolder {props}) "
                    + "RETURN nf.id as id";

            params.put("parentId", parentFolderId);
        }

        neo.execute(completeQuery, params, validUniqueResultHandler(result));
    }

    @Override
    public void updateFolder(String folderId, JsonObject data, UserInfos user,
            Handler<Either<String, JsonObject>> result) {
        final String name = data.getString("name");

        JsonObject params = new JsonObject().put("userId", user.getUserId()).put("true", true).put("targetId",
                folderId);

        if (name != null && name.trim().length() > 0) {
            params.put("newName", name);
        }

        String query = "MATCH (c:Conversation)-[:HAS_CONVERSATION_FOLDER]->(f:ConversationUserFolder)"
                + "-[:HAS_CHILD_FOLDER*0.." + (maxFolderDepth - 1) + "]->(target: ConversationUserFolder) "
                + "WHERE c.userId = {userId} AND c.active = {true} AND target.id = {targetId}"
                + "SET target.name = {newName}";

        neo.execute(query, params, validEmptyHandler(result));
    }

    @Override
    public void listFolders(String parentId, UserInfos user, Handler<Either<String, JsonArray>> result) {
        if (validationError(user, result))
            return;

        JsonObject params = new JsonObject().put("userId", user.getUserId()).put("true", true).put("parentId",
                parentId);

        String query = "";
        if (parentId == null) {
            query = "MATCH (c:Conversation)-[subLink:HAS_CONVERSATION_FOLDER]->(subFolders: ConversationUserFolder) "
                    + "WHERE c.userId = {userId} AND c.active = {true} AND NOT HAS (subLink.trashed) "
                    + "RETURN DISTINCT subFolders.name as name, subFolders.id as id";
        } else {
            query = "MATCH (c:Conversation)-[:HAS_CONVERSATION_FOLDER]->(f:ConversationUserFolder)"
                    + "-[:HAS_CHILD_FOLDER*0.." + (maxFolderDepth - 1) + "]->(target: ConversationUserFolder)"
                    + "-[subLink:HAS_CHILD_FOLDER]->(subFolders: ConversationUserFolder) "
                    + "WHERE target.id = {parentId} AND c.userId = {userId} AND c.active = {true} AND NOT HAS (subLink.trashed) "
                    + "RETURN DISTINCT subFolders.name as name, subFolders.id as id, target.id as parentId";
        }

        neo.execute(query, params, validResultHandler(result));
    }

    @Override
    public void listTrashedFolders(UserInfos user, Handler<Either<String, JsonArray>> result) {
        if (validationError(user, result))
            return;

        JsonObject params = new JsonObject().put("userId", user.getUserId()).put("true", true);

        String query = "MATCH (c:Conversation)-[:TRASHED_CONVERSATION_FOLDER]->(subFolders: ConversationUserFolder) "
                + "WHERE c.userId = {userId} AND c.active = {true} "
                + "RETURN DISTINCT subFolders.name as name, subFolders.id as id";

        neo.execute(query, params, validResultHandler(result));
    }

    @Override
    public void moveToFolder(List<String> messageIds, String folderId, UserInfos user,
            Handler<Either<String, JsonObject>> result) {
        if (validationParamsError(user, result, folderId))
            return;

        String query = "MATCH (c:Conversation)-[:HAS_CONVERSATION_FOLDER]->(f:ConversationSystemFolder)-[r:HAS_CONVERSATION_MESSAGE]->(m:ConversationMessage) "
                + "WHERE c.userId = {userId} AND c.active = {true} AND m.id = {messageId} "
                + "OPTIONAL MATCH (m)-[i: INSIDE]->(:ConversationUserFolder)<-[:HAS_CHILD_FOLDER*0.."
                + (maxFolderDepth - 1) + "]-(:ConversationUserFolder)<-[:HAS_CONVERSATION_FOLDER]-(c) "
                + "DELETE i " + "SET r.insideFolder = true " + "WITH f, m "
                + "MATCH c-[HAS_CONVERSATION_FOLDER]->()-[:HAS_CHILD_FOLDER*0.." + (maxFolderDepth - 1)
                + "]->(child: ConversationUserFolder) " + "WHERE child.id = {folderId} "
                + "CREATE UNIQUE m-[:INSIDE]->child";

        StatementsBuilder b = new StatementsBuilder();
        for (String id : messageIds) {
            JsonObject params = new JsonObject().put("userId", user.getUserId()).put("messageId", id)
                    .put("true", true).put("folderId", folderId);

            b.add(query, params);
        }

        neo.executeTransaction(b.build(), null, true, validEmptyHandler(result));
    }

    @Override
    public void backToSystemFolder(List<String> messageIds, UserInfos user,
            Handler<Either<String, JsonObject>> result) {
        if (validationParamsError(user, result))
            return;

        String query = "MATCH (c:Conversation)-[:HAS_CONVERSATION_FOLDER]->(f:ConversationSystemFolder)-[r:HAS_CONVERSATION_MESSAGE]->(m:ConversationMessage) "
                + "WHERE c.userId = {userId} AND c.active = {true} AND m.id = {messageId} "
                + "REMOVE r.insideFolder " + "WITH m, c "
                + "MATCH m-[i: INSIDE]->(:ConversationUserFolder)<-[:HAS_CHILD_FOLDER*0.." + (maxFolderDepth - 1)
                + "]-(:ConversationUserFolder)<-[:HAS_CONVERSATION_FOLDER]-(c) " + "DELETE i";

        StatementsBuilder b = new StatementsBuilder();
        for (String id : messageIds) {
            JsonObject params = new JsonObject().put("userId", user.getUserId()).put("messageId", id).put("true",
                    true);

            b.add(query, params);
        }

        neo.executeTransaction(b.build(), null, true, validEmptyHandler(result));
    }

    @Override
    public void trashFolder(String folderId, UserInfos user, Handler<Either<String, JsonObject>> result) {
        if (validationParamsError(user, result, folderId))
            return;

        //Trash actions on target folder and its subfolders
        String trashFolders = "MATCH (c:Conversation)-[:HAS_CONVERSATION_FOLDER]->(:ConversationUserFolder)-[:HAS_CHILD_FOLDER*0.."
                + (maxFolderDepth - 1) + "]->(targetFolder: ConversationUserFolder), "
                + "(targetFolder)-[:HAS_CHILD_FOLDER*0.." + (maxFolderDepth - 1)
                + "]->(children: ConversationUserFolder), "
                + "(c)-[:HAS_CONVERSATION_FOLDER]->(trashFolder:ConversationSystemFolder) "
                + "WHERE c.userId = {userId} AND c.active = {true} AND targetFolder.id = {folderId} AND trashFolder.name = {trash} "
                + "WITH c, targetFolder, trashFolder "
                + "CREATE UNIQUE (c)-[:TRASHED_CONVERSATION_FOLDER]->(targetFolder) " + "WITH targetFolder "
                + "OPTIONAL MATCH (targetParent: ConversationUserFolder)-[targetParentRel: HAS_CHILD_FOLDER]->(targetFolder) "
                + "OPTIONAL MATCH (systemParent: Conversation)-[systemParentRel: HAS_CONVERSATION_FOLDER]->(targetFolder) "
                + "SET targetParentRel.trashed = true, systemParentRel.trashed = true";

        //Trash actions on messages contained inside the folder and its children
        String trashMessages = "MATCH (c:Conversation)-[:HAS_CONVERSATION_FOLDER]->(:ConversationUserFolder)-[:HAS_CHILD_FOLDER*0.."
                + (maxFolderDepth - 1) + "]->(targetFolder: ConversationUserFolder), "
                + "(targetFolder)-[:HAS_CHILD_FOLDER*0.." + (maxFolderDepth - 1)
                + "]->(children: ConversationUserFolder), "
                + "(c)-[:HAS_CONVERSATION_FOLDER]->(trashFolder:ConversationSystemFolder), "
                + "(c)-[:HAS_CONVERSATION_FOLDER]->(f: ConversationSystemFolder)-[r:HAS_CONVERSATION_MESSAGE]->(messages: ConversationMessage)-[i: INSIDE]->(children) "
                + "WHERE c.userId = {userId} AND c.active = {true} AND targetFolder.id = {folderId} AND trashFolder.name = {trash} AND NOT HAS(i.trashed) "
                + "CREATE UNIQUE (trashFolder)-[:HAS_CONVERSATION_MESSAGE { restoreFolder: f.name, insideFolder: true, attachments: r.attachments }]->(messages) "
                + "DELETE r " + "SET i.trashed = true";

        //Trash actions on children folders already put in the bin before
        String alreadyTrashedFolders = "MATCH (c:Conversation)-[:HAS_CONVERSATION_FOLDER]->(:ConversationUserFolder)-[:HAS_CHILD_FOLDER*0.."
                + (maxFolderDepth - 1) + "]->(targetFolder: ConversationUserFolder), "
                + "(targetFolder)-[:HAS_CHILD_FOLDER*0.." + (maxFolderDepth - 1)
                + "]->(children: ConversationUserFolder), "
                + "(childrenParents: ConversationFolder)-[childrenParentRel {trashed: true}]->(children)<-[childrenTrashedRel: TRASHED_CONVERSATION_FOLDER]-(c) "
                + "WHERE targetFolder.id = {folderId} " + "REMOVE childrenParentRel.trashed "
                + "DELETE childrenTrashedRel";

        //Trash actions on children messages already put in the bin before
        String alreadyTrashedMails = "MATCH (c:Conversation)-[:HAS_CONVERSATION_FOLDER]->(:ConversationUserFolder)-[:HAS_CHILD_FOLDER*0.."
                + (maxFolderDepth - 1) + "]->(targetFolder: ConversationUserFolder), "
                + "(targetFolder)-[:HAS_CHILD_FOLDER*0.." + (maxFolderDepth - 1)
                + "]->(children: ConversationUserFolder), "
                + "(trashFolder)-[trashRel: HAS_CONVERSATION_MESSAGE]->(:ConversationMessage)-[:INSIDE {trashed: true}]->(children) "
                + "WHERE targetFolder.id = {folderId} " + "SET trashRel.insideFolder = true";

        JsonObject params = new JsonObject().put("userId", user.getUserId()).put("folderId", folderId)
                .put("true", true).put("trash", "TRASH");

        StatementsBuilder b = new StatementsBuilder();
        b.add(alreadyTrashedFolders, params);
        b.add(alreadyTrashedMails, params);
        b.add(trashMessages, params);
        b.add(trashFolders, params);
        neo.executeTransaction(b.build(), null, true, validUniqueResultHandler(result));
    }

    @Override
    public void restoreFolder(String folderId, UserInfos user, Handler<Either<String, JsonObject>> result) {
        if (validationParamsError(user, result, folderId))
            return;

        String query = "MATCH (c: Conversation)-[trashedRel: TRASHED_CONVERSATION_FOLDER]->(trashedFolder: ConversationUserFolder)"
                + "<-[targetParentRel: HAS_CHILD_FOLDER|HAS_CONVERSATION_FOLDER { trashed: true }]-(targetParent) "
                + "WHERE c.userId = {userId} AND c.active = {true} AND trashedFolder.id = {folderId} "
                + "DELETE trashedRel " + "REMOVE targetParentRel.trashed " + "WITH c, trashedFolder "
                + "MATCH (c)-[:HAS_CONVERSATION_FOLDER]->(trashFolder: ConversationSystemFolder) "
                + "WHERE trashFolder.name = {trash} " + "MATCH (trashedFolder)-[:HAS_CHILD_FOLDER*0.."
                + (maxFolderDepth - 1)
                + "]->(children: ConversationUserFolder)<-[i: INSIDE]-(messages: ConversationMessage), "
                + "(trashFolder)-[r:HAS_CONVERSATION_MESSAGE]->(messages), "
                + "(c)-[:HAS_CONVERSATION_FOLDER]->(oldFolder: ConversationSystemFolder) "
                + "WHERE oldFolder.name = r.restoreFolder "
                + "CREATE UNIQUE (oldFolder)-[:HAS_CONVERSATION_MESSAGE { insideFolder: true, attachments: r.attachments }]->(messages) "
                + "DELETE r " + "REMOVE i.trashed";

        JsonObject params = new JsonObject().put("userId", user.getUserId()).put("folderId", folderId)
                .put("true", true).put("trash", "TRASH");

        neo.execute(query, params, validEmptyHandler(result));
    }

    @Override
    public void deleteFolder(String folderId, Boolean deleteAll, UserInfos user,
            Handler<Either<String, JsonArray>> result) {
        if (validationError(user, result, folderId))
            return;

        String retrieveAttachments = "MATCH (c: Conversation)-[:TRASHED_CONVERSATION_FOLDER]->(trashedFolder:ConversationUserFolder) "
                + "WHERE c.userId = {userId} AND c.active = {true} AND trashedFolder.id = {folderId} "
                + "MATCH (c)-[:HAS_CONVERSATION_FOLDER]->(trashFolder: ConversationSystemFolder) "
                + "WHERE trashFolder.name = {trash} " + "MATCH (trashedFolder)-[childrenPath: HAS_CHILD_FOLDER*0.."
                + (maxFolderDepth - 1) + "]->(children: ConversationUserFolder) "
                + "MATCH (children)<-[i: INSIDE]-(messages: ConversationMessage)<-[r: HAS_CONVERSATION_MESSAGE]-(trashFolder) "
                + "MATCH (a: MessageAttachment)<-[:HAS_ATTACHMENT]-(messages) " + "WHERE a.id IN r.attachments "
                + "WITH CASE WHEN a IS NULL THEN [] ELSE collect({id: a.id, size: a.size}) END as attachments "
                + "RETURN attachments";

        String processMessages = "MATCH (c: Conversation)-[trashedRel: TRASHED_CONVERSATION_FOLDER]->(trashedFolder: ConversationUserFolder)"
                + "<-[targetParentRel: HAS_CHILD_FOLDER|HAS_CONVERSATION_FOLDER { trashed: true }]-(targetParent) "
                + "WHERE c.userId = {userId} AND c.active = {true} AND trashedFolder.id = {folderId} "
                + "MATCH (c)-[:HAS_CONVERSATION_FOLDER]->(trashFolder: ConversationSystemFolder) "
                + "WHERE trashFolder.name = {trash} " + "MATCH (trashedFolder)-[childrenPath: HAS_CHILD_FOLDER*0.."
                + (maxFolderDepth - 1) + "]->(children: ConversationUserFolder) "
                + "MATCH (children)<-[i: INSIDE]-(messages: ConversationMessage)<-[r: HAS_CONVERSATION_MESSAGE]-(trashFolder) "
                + "OPTIONAL MATCH (messages)-[pr: PARENT_CONVERSATION_MESSAGE]-() "
                + "CREATE (trashFolder)-[:HAD_CONVERSATION_MESSAGE { attachments: r.attachments }]->(messages) "
                + "DELETE r, i";

        String gatherAttachmentsToDelete = "MATCH (c: Conversation)-[:HAS_CONVERSATION_FOLDER]->(trashFolder: ConversationSystemFolder)-[r:HAD_CONVERSATION_MESSAGE]->(messages: ConversationMessage) "
                + "WHERE c.userId = {userId} AND c.active = {true} AND trashFolder.name = {trash} "
                + "AND NOT (messages)<-[:HAS_CONVERSATION_MESSAGE]-() "
                + "MATCH (messages)-[al: HAS_ATTACHMENT]->(a: MessageAttachment) "
                + "MATCH (a)<-[aLinks:HAS_ATTACHMENT]-(target) " + "WITH a, count(target) as links "
                + "WHERE links = 1 " + "MATCH (a)<-[l:HAS_ATTACHMENT]-(target) "
                + "WITH collect({id: a.id, size: a.size}) as attachments, l, a " + "DELETE l, a "
                + "RETURN attachments";

        String deleteMessages = "MATCH (c: Conversation)-[:HAS_CONVERSATION_FOLDER]->(trashFolder: ConversationSystemFolder)-[:HAD_CONVERSATION_MESSAGE]->(messages: ConversationMessage) "
                + "MATCH (messages)-[r:HAD_CONVERSATION_MESSAGE]-() "
                + "WHERE c.userId = {userId} AND c.active = {true} AND trashFolder.name = {trash} "
                + "AND NOT (messages)-[:HAS_CONVERSATION_MESSAGE]-() "
                + "OPTIONAL MATCH (messages)-[pr: PARENT_CONVERSATION_MESSAGE]-() "
                + "OPTIONAL MATCH (messages)-[al: HAS_ATTACHMENT]->(a: MessageAttachment) "
                + "DELETE r, pr, al, messages";

        String deleteFolders = "MATCH (c: Conversation)-[trashedRel: TRASHED_CONVERSATION_FOLDER]->(trashedFolder: ConversationUserFolder)"
                + "<-[targetParentRel: HAS_CHILD_FOLDER|HAS_CONVERSATION_FOLDER { trashed: true }]-(targetParent) "
                + "WHERE c.userId = {userId} AND c.active = {true} AND trashedFolder.id = {folderId} "
                + "MATCH (trashedFolder)-[childrenPath: HAS_CHILD_FOLDER*0.." + (maxFolderDepth - 1)
                + "]->(children: ConversationUserFolder) " + "FOREACH (rel in childrenPath | DELETE rel) "
                + "DELETE targetParentRel, trashedRel, trashedFolder, children";

        JsonObject params = new JsonObject().put("userId", user.getUserId()).put("folderId", folderId)
                .put("true", true).put("trash", "TRASH");

        StatementsBuilder b = new StatementsBuilder();
        b.add(retrieveAttachments, params);
        b.add(processMessages, params);
        b.add(gatherAttachmentsToDelete, params);
        b.add(deleteMessages, params);
        b.add(deleteFolders, params);
        neo.executeTransaction(b.build(), null, true, validResultsHandler(result));
    }

    @Override
    public void addAttachment(String messageId, UserInfos user, JsonObject uploaded,
            Handler<Either<String, JsonObject>> result) {
        if (validationParamsError(user, result, messageId))
            return;

        String query = "MATCH (m:ConversationMessage)<-[r:HAS_CONVERSATION_MESSAGE]-(f:ConversationSystemFolder)"
                + "<-[:HAS_CONVERSATION_FOLDER]-(c:Conversation) "
                + "WHERE m.id = {messageId} AND c.userId = {userId} AND c.active = {true} AND m.state = {draft} "
                + "CREATE (m)-[:HAS_ATTACHMENT]->(attachment: MessageAttachment {attachmentProps}) "
                + "SET r.attachments = coalesce(r.attachments, []) + {fileId} " + "RETURN attachment.id as id";

        JsonObject params = new JsonObject().put("userId", user.getUserId()).put("messageId", messageId)
                .put("attachmentProps",
                        new JsonObject().put("id", uploaded.getString("_id"))
                                .put("name", uploaded.getJsonObject("metadata").getString("name"))
                                .put("filename", uploaded.getJsonObject("metadata").getString("filename"))
                                .put("contentType", uploaded.getJsonObject("metadata").getString("content-type"))
                                .put("contentTransferEncoding",
                                        uploaded.getJsonObject("metadata").getString("content-transfer-encoding"))
                                .put("charset", uploaded.getJsonObject("metadata").getString("charset"))
                                .put("size", uploaded.getJsonObject("metadata").getLong("size")))
                .put("fileId", uploaded.getString("_id")).put("true", true).put("trash", "TRASH")
                .put("draft", "DRAFT");

        neo.execute(query, params, validUniqueResultHandler(result));
    }

    @Override
    public void getAttachment(String messageId, String attachmentId, UserInfos user,
            Handler<Either<String, JsonObject>> result) {
        if (validationParamsError(user, result, messageId, attachmentId))
            return;

        String query = "MATCH (attachment: MessageAttachment)<-[attachmentLink: HAS_ATTACHMENT]-(m: ConversationMessage)<-[r: HAS_CONVERSATION_MESSAGE]-(f: ConversationSystemFolder)"
                + "<-[:HAS_CONVERSATION_FOLDER]-(c:Conversation) "
                + "WHERE m.id = {messageId} AND c.userId = {userId} AND c.active = {true} AND attachment.id = {attachmentId} AND {attachmentId} IN r.attachments "
                + "WITH distinct attachment "
                + "RETURN attachment.id as id, attachment.name as name, attachment.filename as filename, attachment.contentType as contentType, attachment.contentTransferEncoding as contentTransferEncoding, attachment.charset as charset, attachment.size as size";

        JsonObject params = new JsonObject().put("userId", user.getUserId()).put("messageId", messageId)
                .put("attachmentId", attachmentId).put("true", true);

        neo.execute(query, params, validUniqueResultHandler(result));
    }

    @Override
    public void getAllAttachments(String messageId, UserInfos user, Handler<Either<String, JsonArray>> result) {
        result.handle(new Either.Left<String, JsonArray>("conversation.invalid"));
    }

    @Override
    public void removeAttachment(String messageId, String attachmentId, UserInfos user,
            final Handler<Either<String, JsonObject>> result) {
        if (validationParamsError(user, result, messageId, attachmentId))
            return;

        String get = "MATCH (attachment: MessageAttachment)<-[aLink: HAS_ATTACHMENT]-(m: ConversationMessage)<-[r: HAS_CONVERSATION_MESSAGE]-(f: ConversationSystemFolder)"
                + "<-[:HAS_CONVERSATION_FOLDER]-(c:Conversation) "
                + "WHERE m.id = {messageId} AND c.userId = {userId} AND c.active = {true} AND attachment.id = {attachmentId} AND {attachmentId} IN r.attachments ";

        String query = get
                + "SET r.attachments = filter(attachmentId IN r.attachments WHERE attachmentId <> {attachmentId}) "
                + "RETURN attachment.id as fileId, attachment.size as fileSize";

        String q3 = "MATCH (attachment:MessageAttachment)<-[attachmentLink: HAS_ATTACHMENT]-(:ConversationMessage)<-[messageLinks: HAS_CONVERSATION_MESSAGE]-(:ConversationSystemFolder) "
                + "WHERE attachment.id = {attachmentId} "
                + "WITH attachmentLink, attachment, none(item IN collect(messageLinks.attachments) WHERE attachment.id IN item) as deletionCheck "
                + "WHERE deletionCheck = true " + "DELETE attachmentLink " + "WITH attachment "
                + "WHERE NOT(attachment-[:HAS_ATTACHMENT]-()) " + "DELETE attachment "
                + "RETURN true as deletionCheck";

        JsonObject params = new JsonObject().put("userId", user.getUserId()).put("messageId", messageId)
                .put("attachmentId", attachmentId).put("true", true);

        StatementsBuilder b = new StatementsBuilder();
        b.add(query, params);
        b.add(q3, params);

        neo.executeTransaction(b.build(), null, true, validResultsHandler(new Handler<Either<String, JsonArray>>() {
            @Override
            public void handle(Either<String, JsonArray> event) {
                if (event.isLeft()) {
                    result.handle(new Either.Left<String, JsonObject>(event.left().getValue()));
                    return;
                }

                JsonArray result1 = event.right().getValue().getJsonArray(0);
                JsonArray result3 = event.right().getValue().getJsonArray(1);

                JsonObject jsonResult = result1.size() > 0 ? result1.getJsonObject(0) : new JsonObject();
                jsonResult.put("deletionCheck",
                        result3.size() > 0 ? result3.getJsonObject(0).getBoolean("deletionCheck", false) : false);

                result.handle(new Either.Right<String, JsonObject>(jsonResult));
            }
        }));
    }

    @Override
    public void forwardAttachments(String forwardId, String messageId, UserInfos user,
            Handler<Either<String, JsonObject>> result) {
        if (validationParamsError(user, result, messageId))
            return;

        JsonObject params = new JsonObject().put("userId", user.getUserId()).put("folderName", "DRAFT")
                .put("forwardId", forwardId).put("messageId", messageId).put("true", true);

        String query = "MATCH (c:Conversation)-[:HAS_CONVERSATION_FOLDER]->(f:ConversationSystemFolder)"
                + "-[r:HAS_CONVERSATION_MESSAGE]->(m:ConversationMessage {id: {messageId}}), "
                + "(c:Conversation)-[:HAS_CONVERSATION_FOLDER]->(af:ConversationSystemFolder)"
                + "-[r2:HAS_CONVERSATION_MESSAGE]->(pm:ConversationMessage) "
                + "WHERE c.userId = {userId} AND c.active = {true} AND f.name = {folderName} "
                + "AND pm.id = {forwardId} " + "OPTIONAL MATCH (pm)-[:HAS_ATTACHMENT]->(a: MessageAttachment) "
                + "SET r.attachments = r2.attachments " + "WITH distinct m, a "
                + "FOREACH (attachment IN CASE WHEN a IS NOT NULL THEN [a] ELSE [] END | "
                + "CREATE (m)-[:HAS_ATTACHMENT]->(attachment) )";

        neo.execute(query, params, validEmptyHandler(result));

    }

    private boolean validationError(UserInfos user, Handler<Either<String, JsonArray>> results, String... params) {
        if (user == null) {
            results.handle(new Either.Left<String, JsonArray>("conversation.invalid.user"));
            return true;
        }
        if (params.length > 0) {
            for (String s : params) {
                if (s == null) {
                    results.handle(new Either.Left<String, JsonArray>("conversation.invalid.parameter"));
                    return true;
                }
            }
        }
        return false;
    }

    private boolean validationParamsError(UserInfos user, Handler<Either<String, JsonObject>> result,
            String... params) {
        if (user == null) {
            result.handle(new Either.Left<String, JsonObject>("conversation.invalid.user"));
            return true;
        }
        if (params.length > 0) {
            for (String s : params) {
                if (s == null) {
                    result.handle(new Either.Left<String, JsonObject>("conversation.invalid.parameter"));
                    return true;
                }
            }
        }
        return false;
    }

    private boolean validationError(UserInfos user, JsonObject c, Handler<Either<String, JsonObject>> result,
            String... params) {
        if (c == null) {
            result.handle(new Either.Left<String, JsonObject>("conversation.invalid.fields"));
            return true;
        }
        return validationParamsError(user, result, params);
    }

    private String nodeSetPropertiesFromJson(String nodeAlias, JsonObject json) {
        StringBuilder sb = new StringBuilder();
        for (String attr : json.fieldNames()) {
            sb.append(", ").append(nodeAlias).append(".").append(attr).append(" = {").append(attr).append("}");
        }
        if (sb.length() > 2) {
            return sb.append(" ").substring(2);
        }
        return " ";
    }

}