org.entcore.directory.services.impl.DefaultUserService.java Source code

Java tutorial

Introduction

Here is the source code for org.entcore.directory.services.impl.DefaultUserService.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.directory.services.impl;

import fr.wseduc.webutils.Either;

import fr.wseduc.webutils.Utils;
import fr.wseduc.webutils.email.EmailSender;
import io.vertx.core.AsyncResult;
import io.vertx.core.eventbus.DeliveryOptions;
import org.entcore.common.neo4j.Neo4j;
import org.entcore.common.user.UserInfos;
import org.entcore.common.validation.StringValidation;
import org.entcore.directory.Directory;
import org.entcore.directory.services.UserService;
import io.vertx.core.Handler;
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.util.List;
import java.util.UUID;
import java.util.regex.Pattern;

import static fr.wseduc.webutils.Utils.getOrElse;
import static fr.wseduc.webutils.Utils.handlerToAsyncHandler;
import static org.entcore.common.neo4j.Neo4jResult.*;
import static org.entcore.common.user.DefaultFunctions.ADMIN_LOCAL;
import static org.entcore.common.user.DefaultFunctions.CLASS_ADMIN;
import static org.entcore.common.user.DefaultFunctions.SUPER_ADMIN;

public class DefaultUserService implements UserService {

    private final Neo4j neo = Neo4j.getInstance();
    private final EmailSender notification;
    private final EventBus eb;
    private Logger logger = LoggerFactory.getLogger(DefaultUserService.class);

    public DefaultUserService(EmailSender notification, EventBus eb) {
        this.notification = notification;
        this.eb = eb;
    }

    @Override
    public void createInStructure(String structureId, JsonObject user, Handler<Either<String, JsonObject>> result) {
        user.put("profiles", new fr.wseduc.webutils.collections.JsonArray().add(user.getString("type")));
        JsonObject action = new JsonObject().put("action", "manual-create-user").put("structureId", structureId)
                .put("profile", user.getString("type")).put("data", user);
        eb.send(Directory.FEEDER, action, handlerToAsyncHandler(validUniqueResultHandler(result)));
    }

    @Override
    public void createInClass(String classId, JsonObject user, Handler<Either<String, JsonObject>> result) {
        user.put("profiles", new fr.wseduc.webutils.collections.JsonArray().add(user.getString("type")));
        JsonObject action = new JsonObject().put("action", "manual-create-user").put("classId", classId)
                .put("profile", user.getString("type")).put("data", user);
        eb.send(Directory.FEEDER, action, handlerToAsyncHandler(validUniqueResultHandler(result)));
    }

    @Override
    public void update(final String id, final JsonObject user, final Handler<Either<String, JsonObject>> result) {
        JsonObject action = new JsonObject().put("action", "manual-update-user").put("userId", id).put("data",
                user);
        eb.send(Directory.FEEDER, action, handlerToAsyncHandler(validUniqueResultHandler(result)));
    }

    @Override
    public void sendUserCreatedEmail(final HttpServerRequest request, String userId,
            final Handler<Either<String, Boolean>> result) {
        String query = "MATCH (u:`User` { id : {id}}) WHERE NOT(u.email IS NULL) AND NOT(u.activationCode IS NULL) "
                + "RETURN u.login as login, u.email as email, u.activationCode as activationCode ";
        JsonObject params = new JsonObject().put("id", userId);
        neo.execute(query, params, new Handler<Message<JsonObject>>() {
            @Override
            public void handle(Message<JsonObject> m) {
                Either<String, JsonObject> r = validUniqueResult(m);
                if (r.isRight()) {
                    JsonObject j = r.right().getValue();
                    String email = j.getString("email");
                    String login = j.getString("login");
                    String activationCode = j.getString("activationCode");
                    if (email == null || login == null || activationCode == null || email.trim().isEmpty()
                            || login.trim().isEmpty() || activationCode.trim().isEmpty()) {
                        result.handle(new Either.Left<String, Boolean>("user.invalid.values"));
                        return;
                    }
                    JsonObject json = new JsonObject()
                            .put("activationUri",
                                    notification.getHost(request) + "/auth/activation?login=" + login
                                            + "&activationCode=" + activationCode)
                            .put("host", notification.getHost(request)).put("login", login);
                    logger.debug(json.encode());
                    notification.sendEmail(request, email, null, null, "email.user.created.info",
                            "email/userCreated.html", json, true, new Handler<AsyncResult<Message<JsonObject>>>() {

                                @Override
                                public void handle(AsyncResult<Message<JsonObject>> ar) {
                                    if (ar.succeeded()) {
                                        result.handle(new Either.Right<String, Boolean>(true));
                                    } else {
                                        result.handle(new Either.Left<String, Boolean>(ar.cause().getMessage()));
                                    }
                                }
                            });
                } else {
                    result.handle(new Either.Left<String, Boolean>(r.left().getValue()));
                }
            }
        });
    }

    @Override
    public void get(String id, boolean getManualGroups, Handler<Either<String, JsonObject>> result) {
        get(id, getManualGroups, new JsonArray(), result);
    }

    @Override
    public void get(String id, boolean getManualGroups, JsonArray filterAttributes,
            Handler<Either<String, JsonObject>> result) {

        String getMgroups = "";
        String resultMgroups = "";
        if (getManualGroups) {
            getMgroups = "OPTIONAL MATCH u-[:IN]->(mgroup: ManualGroup) WITH COLLECT(distinct {id: mgroup.id, name: mgroup.name}) as manualGroups, admStruct, admGroups, parents, children, functions, u, structureNodes ";
            resultMgroups = "CASE WHEN manualGroups IS NULL THEN [] ELSE manualGroups END as manualGroups, ";
        }
        String query = "MATCH (u:`User` { id : {id}}) "
                + "OPTIONAL MATCH u-[:IN]->()-[:DEPENDS]->(s:Structure) WITH COLLECT(distinct s) as structureNodes, u "
                + "OPTIONAL MATCH u-[rf:HAS_FUNCTION]->fg-[:CONTAINS_FUNCTION*0..1]->(f:Function) WITH COLLECT(distinct [f.externalId, rf.scope]) as functions, u, structureNodes "
                + "OPTIONAL MATCH u<-[:RELATED]-(child: User) WITH COLLECT(distinct {id: child.id, displayName: child.displayName, externalId: child.externalId}) as children, functions, u, structureNodes "
                + "OPTIONAL MATCH u-[:RELATED]->(parent: User) WITH COLLECT(distinct {id: parent.id, displayName: parent.displayName, externalId: parent.externalId}) as parents, children, functions, u, structureNodes "
                + "OPTIONAL MATCH u-[:IN]->(fgroup: FunctionalGroup) WITH COLLECT(distinct {id: fgroup.id, name: fgroup.name}) as admGroups, parents, children, functions, u, structureNodes "
                + "OPTIONAL MATCH u-[:ADMINISTRATIVE_ATTACHMENT]->(admStruct: Structure) WITH COLLECT(distinct {id: admStruct.id}) as admStruct, admGroups, parents, children, functions, u, structureNodes "
                + getMgroups + "RETURN DISTINCT u.profiles as type, structureNodes, functions, "
                + "CASE WHEN children IS NULL THEN [] ELSE children END as children, "
                + "CASE WHEN parents IS NULL THEN [] ELSE parents END as parents, "
                + "CASE WHEN admGroups IS NULL THEN [] ELSE admGroups END as functionalGroups, "
                + "CASE WHEN admStruct IS NULL THEN [] ELSE admStruct END as administrativeStructures, "
                + resultMgroups + "u";
        final Handler<Either<String, JsonObject>> filterResultHandler = event -> {
            if (event.isRight()) {
                final JsonObject r = event.right().getValue();
                filterAttributes.add("password").add("resetCode").add("lastNameSearchField")
                        .add("firstNameSearchField").add("displayNameSearchField").add("checksum");
                for (Object o : filterAttributes) {
                    r.remove((String) o);
                }
            }
            result.handle(event);
        };
        neo.execute(query, new JsonObject().put("id", id),
                fullNodeMergeHandler("u", filterResultHandler, "structureNodes"));
    }

    @Override
    public void list(String structureId, String classId, JsonArray expectedProfiles,
            Handler<Either<String, JsonArray>> results) {
        JsonObject params = new JsonObject();
        String filterProfile = "";
        String filterStructure = "";
        String filterClass = "";
        if (expectedProfiles != null && expectedProfiles.size() > 0) {
            filterProfile = "WHERE p.name IN {expectedProfiles} ";
            params.put("expectedProfiles", expectedProfiles);
        }
        if (classId != null && !classId.trim().isEmpty()) {
            filterClass = "(g:ProfileGroup)-[:DEPENDS]->(n:Class {id : {classId}}), ";
            params.put("classId", classId);
        } else if (structureId != null && !structureId.trim().isEmpty()) {
            filterStructure = "(pg:ProfileGroup)-[:DEPENDS]->(n:Structure {id : {structureId}}), ";
            params.put("structureId", structureId);
        }
        String query = "MATCH " + filterClass + filterStructure
                + "(u:User)-[:IN]->g-[:DEPENDS*0..1]->pg-[:HAS_PROFILE]->(p:Profile) " + filterProfile
                + "RETURN DISTINCT u.id as id, p.name as type, u.externalId as externalId, u.IDPN as IDPN, "
                + "u.activationCode as code, u.login as login, u.firstName as firstName, "
                + "u.lastName as lastName, u.displayName as displayName " + "ORDER BY type DESC, displayName ASC ";
        neo.execute(query, params, validResultHandler(results));
    }

    @Override
    public void listIsolated(String structureId, List<String> profile, Handler<Either<String, JsonArray>> results) {
        JsonObject params = new JsonObject();
        String query;
        // users without class
        if (structureId != null && !structureId.trim().isEmpty()) {
            query = "MATCH  (s:Structure { id : {structureId}})<-[:DEPENDS]-(g:ProfileGroup)<-[:IN]-(u:User), "
                    + "g-[:HAS_PROFILE]->(p:Profile) "
                    + "WHERE  NOT(u-[:IN]->()-[:DEPENDS]->(:Class)-[:BELONGS]->s) ";
            params.put("structureId", structureId);
            if (profile != null && !profile.isEmpty()) {
                query += "AND p.name IN {profile} ";
                params.put("profile", new fr.wseduc.webutils.collections.JsonArray(profile));
            }
        } else { // users without structure
            query = "MATCH (u:User)" + "WHERE NOT(u-[:IN]->()-[:DEPENDS]->(:Structure)) "
                    + "OPTIONAL MATCH u-[:IN]->(dpg:DefaultProfileGroup)-[:HAS_PROFILE]->(p:Profile) ";
        }
        query += "RETURN DISTINCT u.id as id, p.name as type, "
                + "u.activationCode as code, u.firstName as firstName,"
                + "u.lastName as lastName, u.displayName as displayName " + "ORDER BY type DESC, displayName ASC ";
        neo.execute(query, params, validResultHandler(results));
    }

    @Override
    public void listAdmin(String structureId, String classId, String groupId, JsonArray expectedProfiles,
            UserInfos userInfos, io.vertx.core.Handler<fr.wseduc.webutils.Either<String, JsonArray>> results) {
        listAdmin(structureId, classId, groupId, expectedProfiles, null, null, userInfos, results);
    };

    @Override
    public void listAdmin(String structureId, String classId, String groupId, JsonArray expectedProfiles,
            String filterActivated, String nameFilter, UserInfos userInfos,
            Handler<Either<String, JsonArray>> results) {
        JsonObject params = new JsonObject();
        String filter = "";
        String filterProfile = "WHERE 1=1 ";
        String optionalMatch = "OPTIONAL MATCH u-[:IN]->(:ProfileGroup)-[:DEPENDS]->(class:Class)-[:BELONGS]->(s) "
                + "OPTIONAL MATCH u-[:RELATED]->(parent: User) " + "OPTIONAL MATCH (child: User)-[:RELATED]->u "
                + "OPTIONAL MATCH u-[rf:HAS_FUNCTION]->fg-[:CONTAINS_FUNCTION*0..1]->(f:Function) ";
        if (expectedProfiles != null && expectedProfiles.size() > 0) {
            filterProfile += "AND p.name IN {expectedProfiles} ";
            params.put("expectedProfiles", expectedProfiles);
        }
        if (classId != null && !classId.trim().isEmpty()) {
            filter = "(n:Class {id : {classId}})<-[:DEPENDS]-(g:ProfileGroup)<-[:IN]-";
            params.put("classId", classId);
        } else if (structureId != null && !structureId.trim().isEmpty()) {
            filter = "(n:Structure {id : {structureId}})<-[:DEPENDS]-(g:ProfileGroup)<-[:IN]-";
            params.put("structureId", structureId);
        } else if (groupId != null && !groupId.trim().isEmpty()) {
            filter = "(n:Group {id : {groupId}})<-[:IN]-";
            params.put("groupId", groupId);
        }
        String condition = "";
        String functionMatch = "WITH u MATCH (s:Structure)<-[:DEPENDS]-(pg:ProfileGroup)-[:HAS_PROFILE]->(p:Profile), u-[:IN]->pg ";
        if (!userInfos.getFunctions().containsKey(SUPER_ADMIN) && !userInfos.getFunctions().containsKey(ADMIN_LOCAL)
                && !userInfos.getFunctions().containsKey(CLASS_ADMIN)) {
            results.handle(new Either.Left<String, JsonArray>("forbidden"));
            return;
        } else if (userInfos.getFunctions().containsKey(ADMIN_LOCAL)) {
            UserInfos.Function f = userInfos.getFunctions().get(ADMIN_LOCAL);
            List<String> scope = f.getScope();
            if (scope != null && !scope.isEmpty()) {
                condition = "AND s.id IN {scope} ";
                params.put("scope", new fr.wseduc.webutils.collections.JsonArray(scope));
            }
        } else if (userInfos.getFunctions().containsKey(CLASS_ADMIN)) {
            UserInfos.Function f = userInfos.getFunctions().get(CLASS_ADMIN);
            List<String> scope = f.getScope();
            if (scope != null && !scope.isEmpty()) {
                functionMatch = "WITH u MATCH (c:Class)<-[:DEPENDS]-(cpg:ProfileGroup)-[:DEPENDS]->(pg:ProfileGroup)-[:HAS_PROFILE]->(p:Profile), u-[:IN]->pg ";
                condition = "AND c.id IN {scope} ";
                params.put("scope", new fr.wseduc.webutils.collections.JsonArray(scope));
            }
        }
        if (nameFilter != null && !nameFilter.trim().isEmpty()) {
            condition += "AND u.displayName =~ {regex}  ";
            params.put("regex", "(?i)^.*?" + Pattern.quote(nameFilter.trim()) + ".*?$");
        }
        if (filterActivated != null) {
            if ("inactive".equals(filterActivated)) {
                condition += "AND NOT(u.activationCode IS NULL)  ";
            } else if ("active".equals(filterActivated)) {
                condition += "AND u.activationCode IS NULL ";
            }
        }

        String query = "MATCH " + filter + "(u:User) " + functionMatch + filterProfile + condition + optionalMatch
                + "RETURN DISTINCT u.id as id, p.name as type, u.externalId as externalId, "
                + "u.activationCode as code, "
                + "CASE WHEN u.loginAlias IS NOT NULL THEN u.loginAlias ELSE u.login END as login, "
                + "u.firstName as firstName, "
                + "u.lastName as lastName, u.displayName as displayName, u.source as source, u.attachmentId as attachmentId, "
                + "u.birthDate as birthDate, u.blocked as blocked, "
                + "extract(function IN u.functions | last(split(function, \"$\"))) as aafFunctions, "
                + "collect(distinct {id: s.id, name: s.name}) as structures, "
                + "collect(distinct {id: class.id, name: class.name}) as allClasses, "
                + "collect(distinct [f.externalId, rf.scope]) as functions, "
                + "CASE WHEN parent IS NULL THEN [] ELSE collect(distinct {id: parent.id, firstName: parent.firstName, lastName: parent.lastName}) END as parents, "
                + "CASE WHEN child IS NULL THEN [] ELSE collect(distinct {id: child.id, firstName: child.firstName, lastName: child.lastName, attachmentId : child.attachmentId }) END as children, "
                + "HEAD(COLLECT(distinct parent.externalId)) as parent1ExternalId, " + // Hack for GEPI export
                "HEAD(TAIL(COLLECT(distinct parent.externalId))) as parent2ExternalId, " + // Hack for GEPI export
                "COUNT(distinct class.id) > 0 as hasClass " + // Hack for Esidoc export
                "ORDER BY type DESC, displayName ASC ";
        neo.execute(query, params, validResultHandler(results));
    }

    @Override
    public void delete(List<String> users, Handler<Either<String, JsonObject>> result) {
        JsonObject action = new JsonObject().put("action", "manual-delete-user").put("users",
                new fr.wseduc.webutils.collections.JsonArray(users));
        eb.send(Directory.FEEDER, action, handlerToAsyncHandler(validEmptyHandler(result)));
    }

    @Override
    public void restore(List<String> users, Handler<Either<String, JsonObject>> result) {
        JsonObject action = new JsonObject().put("action", "manual-restore-user").put("users",
                new fr.wseduc.webutils.collections.JsonArray(users));
        eb.send(Directory.FEEDER, action, handlerToAsyncHandler(validEmptyHandler(result)));
    }

    @Override
    public void addFunction(String id, String functionCode, JsonArray scope, String inherit,
            Handler<Either<String, JsonObject>> result) {
        JsonObject action = new JsonObject().put("action", "manual-add-user-function").put("userId", id)
                .put("function", functionCode).put("inherit", inherit).put("scope", scope);
        eb.send(Directory.FEEDER, action, ar -> {
            if (ar.succeeded()) {
                JsonArray res = ((JsonObject) ar.result().body()).getJsonArray("results");
                JsonObject json = new JsonObject();
                if (res.size() == 2) {
                    JsonArray r = res.getJsonArray(1);
                    if (r.size() == 1) {
                        json = r.getJsonObject(0);
                    }
                }
                result.handle(new Either.Right<>(json));
            } else {
                result.handle(new Either.Left<>(ar.cause().getMessage()));
            }
        });
    }

    @Override
    public void addHeadTeacherManual(String id, String structureExternalId, String classExternalId,
            Handler<Either<String, JsonObject>> result) {
        JsonObject action = new JsonObject().put("action", "manual-add-head-teacher").put("userId", id)
                .put("classExternalId", classExternalId).put("structureExternalId", structureExternalId);
        eb.send(Directory.FEEDER, action, handlerToAsyncHandler(validEmptyHandler(result)));
    }

    @Override
    public void updateHeadTeacherManual(String id, String structureExternalId, String classExternalId,
            Handler<Either<String, JsonObject>> result) {
        JsonObject action = new JsonObject().put("action", "manual-update-head-teacher").put("userId", id)
                .put("classExternalId", classExternalId).put("structureExternalId", structureExternalId);
        eb.send(Directory.FEEDER, action, handlerToAsyncHandler(validEmptyHandler(result)));
    }

    @Override
    public void removeFunction(String id, String functionCode, Handler<Either<String, JsonObject>> result) {
        JsonObject action = new JsonObject().put("action", "manual-remove-user-function").put("userId", id)
                .put("function", functionCode);
        eb.send(Directory.FEEDER, action, handlerToAsyncHandler(validEmptyHandler(result)));
    }

    public void listFunctions(String userId, Handler<Either<String, JsonArray>> result) {
        String query = "MATCH (u:User{id: {userId}})-[rf:HAS_FUNCTION]->fg-[:CONTAINS_FUNCTION*0..1]->(f:Function) "
                + "RETURN COLLECT(distinct [f.externalId, rf.scope]) as functions";
        JsonObject params = new JsonObject();
        params.put("userId", userId);
        neo.execute(query, params, validResultHandler(result));
    }

    @Override
    public void addGroup(String id, String groupId, Handler<Either<String, JsonObject>> result) {
        JsonObject action = new JsonObject().put("action", "manual-add-user-group").put("userId", id).put("groupId",
                groupId);
        eb.send(Directory.FEEDER, action, handlerToAsyncHandler(validEmptyHandler(result)));
    }

    @Override
    public void removeGroup(String id, String groupId, Handler<Either<String, JsonObject>> result) {
        JsonObject action = new JsonObject().put("action", "manual-remove-user-group").put("userId", id)
                .put("groupId", groupId);
        eb.send(Directory.FEEDER, action, handlerToAsyncHandler(validEmptyHandler(result)));
    }

    @Override
    public void listAdml(String scopeId, Handler<Either<String, JsonArray>> result) {
        String query = "MATCH (n)<-[:DEPENDS]-(g:FunctionGroup)<-[:IN]-(u:User) "
                + "WHERE (n:Structure OR n:Class) AND n.id = {scopeId} AND g.name =~ '^.*-AdminLocal$' "
                + "OPTIONAL MATCH u-[:IN]->(pg:ProfileGroup)-[:HAS_PROFILE]->(profile:Profile) "
                + "RETURN distinct u.id as id, u.login as login,"
                + " u.displayName as username, profile.name as type " + "ORDER BY username ";
        JsonObject params = new JsonObject();
        params.put("scopeId", scopeId);
        neo.execute(query, params, validResultHandler(result));
    }

    @Override
    public void getInfos(String userId, Handler<Either<String, JsonObject>> result) {
        String query = "MATCH (n:User {id : {id}}) " + "OPTIONAL MATCH n-[:IN]->(gp:Group) "
                + "OPTIONAL MATCH n-[:IN]->()-[:DEPENDS]->(s:Structure) "
                + "OPTIONAL MATCH n-[:IN]->()-[:DEPENDS]->(c:Class) "
                + "OPTIONAL MATCH n-[rf:HAS_FUNCTION]->fg-[:CONTAINS_FUNCTION*0..1]->(f:Function) "
                + "OPTIONAL MATCH n-[:IN]->()-[:HAS_PROFILE]->(p:Profile) "
                + "OPTIONAL MATCH n-[:ADMINISTRATIVE_ATTACHMENT]->(sa:Structure) " + "RETURN distinct "
                + "n, COLLECT(distinct c) as classes, HEAD(COLLECT(distinct p.name)) as type, "
                + "COLLECT(distinct s) as structures, COLLECT(distinct [f.externalId, rf.scope]) as functions, "
                + "COLLECT(distinct gp) as groups, COLLECT(distinct sa) as administratives";
        neo.execute(query, new JsonObject().put("id", userId),
                fullNodeMergeHandler("n", result, "structures", "classes", "groups", "administratives"));
    }

    @Override
    public void relativeStudent(String relativeId, String studentId,
            Handler<Either<String, JsonObject>> eitherHandler) {
        JsonObject action = new JsonObject().put("action", "manual-relative-student").put("relativeId", relativeId)
                .put("studentId", studentId);
        eb.send(Directory.FEEDER, action, handlerToAsyncHandler(validUniqueResultHandler(0, eitherHandler)));
    }

    @Override
    public void unlinkRelativeStudent(String relativeId, String studentId,
            Handler<Either<String, JsonObject>> eitherHandler) {
        JsonObject action = new JsonObject().put("action", "manual-unlink-relative-student")
                .put("relativeId", relativeId).put("studentId", studentId);
        eb.send(Directory.FEEDER, action, handlerToAsyncHandler(validEmptyHandler(eitherHandler)));
    }

    @Override
    public void ignoreDuplicate(String userId1, String userId2, Handler<Either<String, JsonObject>> result) {
        JsonObject action = new JsonObject().put("action", "ignore-duplicate").put("userId1", userId1)
                .put("userId2", userId2);
        eb.send(Directory.FEEDER, action, handlerToAsyncHandler(validEmptyHandler(result)));
    }

    @Override
    public void listDuplicates(JsonArray structures, boolean inherit, Handler<Either<String, JsonArray>> results) {
        JsonObject action = new JsonObject().put("action", "list-duplicate").put("structures", structures)
                .put("inherit", inherit);
        eb.send(Directory.FEEDER, action, new DeliveryOptions().setSendTimeout(600000l),
                handlerToAsyncHandler(validResultHandler(results)));
    }

    @Override
    public void mergeDuplicate(String userId1, String userId2, Handler<Either<String, JsonObject>> handler) {
        JsonObject action = new JsonObject().put("action", "merge-duplicate").put("userId1", userId1).put("userId2",
                userId2);
        eb.send(Directory.FEEDER, action, handlerToAsyncHandler(validEmptyHandler(handler)));
    }

    @Override
    public void listByUAI(List<String> UAI, JsonArray expectedTypes, boolean isExportFull, JsonArray fields,
            Handler<Either<String, JsonArray>> results) {
        if (UAI == null || UAI.isEmpty()) {
            results.handle(new Either.Left<String, JsonArray>("missing.uai"));
            return;
        } else {
            for (String uaiCode : UAI) {
                if (!StringValidation.isUAI(uaiCode)) {
                    results.handle(new Either.Left<String, JsonArray>("invalid.uai"));
                    return;
                }
            }
        }

        if (fields == null || fields.size() == 0) {
            fields = new fr.wseduc.webutils.collections.JsonArray().add("id").add("externalId").add("lastName")
                    .add("firstName").add("login");
        }

        //user's fields for Full Export
        if (isExportFull) {
            fields.add("email");
            fields.add("emailAcademy");
            fields.add("mobile");
            fields.add("deleteDate");
            fields.add("functions");
            fields.add("displayName");
        }

        // Init params and filter for all type of queries
        String filter = "WHERE s.UAI IN {uai} ";

        JsonObject params = new JsonObject().put("uai", new fr.wseduc.webutils.collections.JsonArray(UAI));

        StringBuilder query = new StringBuilder();
        query.append("MATCH (s:Structure)<-[:DEPENDS]-(cpg:ProfileGroup)");

        // filter by types if needed OR full export
        if (isExportFull || (expectedTypes != null && expectedTypes.size() > 0)) {
            query.append("-[:HAS_PROFILE]->(p:Profile)");
        }
        // filter by types if needed
        if (expectedTypes != null && expectedTypes.size() > 0) {

            filter += "AND p.name IN {expectedTypes} ";
            params.put("expectedTypes", expectedTypes);
        }

        query.append(", cpg<-[:IN]-(u:User) ").append(filter);

        if (fields.contains("administrativeStructure")) {
            query.append("OPTIONAL MATCH u-[:ADMINISTRATIVE_ATTACHMENT]->sa ");
        }

        query.append("RETURN DISTINCT ");

        for (Object field : fields) {
            if ("type".equals(field) || "profile".equals(field)) {
                query.append(" HEAD(u.profiles)");
            } else if ("administrativeStructure".equals(field)) {
                query.append(" sa.externalId ");
            } else {
                query.append(" u.").append(field);
            }
            query.append(" as ").append(field).append(",");
        }
        query.deleteCharAt(query.length() - 1);

        //Full Export : profiles and Structure
        if (isExportFull) {
            query.append(", p.name as profiles");
            query.append(", s.externalId as structures")
                    .append(" , CASE WHEN size(u.classes) > 0  THEN  last(collect(u.classes)) END as classes");
        }

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

    @Override
    public void generateMergeKey(String userId, Handler<Either<String, JsonObject>> handler) {
        if (Utils.defaultValidationParamsError(handler, userId))
            return;
        final String query = "MATCH (u:User {id: {id}}) SET u.mergeKey = {mergeKey} return u.mergeKey as mergeKey";
        final JsonObject params = new JsonObject().put("id", userId).put("mergeKey", UUID.randomUUID().toString());
        neo.execute(query, params, validUniqueResultHandler(handler));
    }

    @Override
    public void mergeByKey(String userId, JsonObject body, Handler<Either<String, JsonObject>> handler) {
        if (Utils.defaultValidationParamsNull(handler, userId, body))
            return;
        JsonObject action = new JsonObject().put("action", "merge-by-keys").put("originalUserId", userId)
                .put("mergeKeys", body.getJsonArray("mergeKeys"));
        eb.send(Directory.FEEDER, action, handlerToAsyncHandler(validUniqueResultHandler(5, handler)));
    }

    @Override
    public void listChildren(String userId, Handler<Either<String, JsonArray>> handler) {
        final String query = "MATCH (n:User {id : {id}})<-[:RELATED]-(child:User)-[:IN]->(:ProfileGroup)-[:DEPENDS]->(s:Structure) "
                + "OPTIONAL MATCH (child)-[:IN]->(:ProfileGroup)-[:DEPENDS]->(c:Class) "
                + "WITH COLLECT(distinct c.name) as classesNames, s, child "
                + "RETURN s.name as structureName, COLLECT(distinct {id: child.id, displayName: child.displayName, externalId: child.externalId, classesNames : classesNames}) as children ";
        final JsonObject params = new JsonObject().put("id", userId);
        neo.execute(query, params, validResultHandler(handler));
    }

    @Override
    public void list(String groupId, boolean itSelf, String userId,
            final Handler<Either<String, JsonArray>> handler) {
        String condition = (itSelf || userId == null) ? "" : "AND u.id <> {userId} ";
        String query = "MATCH (n:Group)<-[:IN]-(u:User) " + "WHERE n.id = {groupId} " + condition
                + "OPTIONAL MATCH (n)-[:DEPENDS*0..1]->(:ProfileGroup)-[:HAS_PROFILE]->(profile:Profile) "
                + "OPTIONAL MATCH (u)-[:IN]->(pg:ProfileGroup)-[:DEPENDS]->(s:Structure) "
                + "OPTIONAL MATCH (pg)-[:HAS_PROFILE]->(pro:Profile) "
                + "RETURN distinct u.id as id, u.login as login,"
                + "u.displayName as username, u.firstName as firstName, u.lastName as lastName, profile.name as type,"
                + "CASE WHEN s IS NULL THEN [] ELSE COLLECT(DISTINCT {id: s.id, name: s.name}) END as structures,"
                + "CASE WHEN pro IS NULL THEN NULL ELSE HEAD(COLLECT(DISTINCT pro.name)) END as profile "
                + "ORDER BY username ";
        JsonObject params = new JsonObject();
        params.put("groupId", groupId);
        if (!itSelf && userId != null) {
            params.put("userId", userId);
        }
        neo.execute(query, params, validResultHandler(handler));
    }

    @Override
    public void list(JsonArray groupIds, JsonArray userIds, boolean itSelf, String userId,
            final Handler<Either<String, JsonArray>> handler) {
        String condition = (itSelf || userId == null) ? "" : "AND u.id <> {userId} ";
        String query = "MATCH (n:Group)<-[:IN]-(u:User) " + "WHERE n.id IN {groupIds} " + condition
                + "OPTIONAL MATCH n-[:DEPENDS*0..1]->(pg:ProfileGroup)-[:HAS_PROFILE]->(profile:Profile) "
                + "RETURN distinct u.id as id, u.login as login,"
                + " u.displayName as username, profile.name as type " + "ORDER BY username " + "UNION "
                + "MATCH (u:User) " + "WHERE u.id IN {userIds} " + condition
                + "OPTIONAL MATCH u-[:IN]->(pg:ProfileGroup)-[:HAS_PROFILE]->(profile:Profile) "
                + "RETURN distinct u.id as id, u.login as login,"
                + " u.displayName as username, profile.name as type " + "ORDER BY username ";
        JsonObject params = new JsonObject();
        params.put("groupIds", groupIds);
        params.put("userIds", userIds);
        if (!itSelf && userId != null) {
            params.put("userId", userId);
        }
        neo.execute(query, params, validResultHandler(handler));
    }

}