org.entcore.feeder.timetable.AbstractTimetableImporter.java Source code

Java tutorial

Introduction

Here is the source code for org.entcore.feeder.timetable.AbstractTimetableImporter.java

Source

/*
 * Copyright  "Open Digital Education", 2016
 *
 * 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.feeder.timetable;

import fr.wseduc.mongodb.MongoDb;
import fr.wseduc.webutils.DefaultAsyncResult;
import io.vertx.core.AsyncResult;
import org.entcore.common.neo4j.Neo4jUtils;
import org.entcore.feeder.dictionary.structures.Importer;
import org.entcore.feeder.dictionary.structures.Transition;
import org.entcore.feeder.dictionary.users.PersEducNat;
import org.entcore.feeder.exceptions.TransactionException;
import org.entcore.feeder.exceptions.ValidationException;
import org.entcore.common.neo4j.Neo4j;
import org.entcore.feeder.utils.Report;
import org.entcore.feeder.utils.TransactionHelper;
import org.entcore.feeder.utils.TransactionManager;
import org.entcore.feeder.utils.Validator;
import org.joda.time.DateTime;
import io.vertx.core.Handler;
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.Logger;
import io.vertx.core.logging.LoggerFactory;

import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

import static fr.wseduc.webutils.Utils.isEmpty;
import static fr.wseduc.webutils.Utils.isNotEmpty;

public abstract class AbstractTimetableImporter implements TimetableImporter {

    protected static final Logger log = LoggerFactory.getLogger(AbstractTimetableImporter.class);
    private static final String CREATE_SUBJECT = "MATCH (s:Structure {externalId : {structureExternalId}}) "
            + "MERGE (sub:Subject {externalId : {externalId}}) "
            + "ON CREATE SET sub.code = {Code}, sub.label = {Libelle}, sub.id = {id} "
            + "SET sub.lastUpdated = {now}, sub.source = {source} " + "MERGE (sub)-[:SUBJECT]->(s) ";
    private static final String LINK_SUBJECT = "MATCH (s:Subject {id : {subjectId}}), (u:User) "
            + "WHERE u.id IN {teacherIds} " + "MERGE u-[r:TEACHES]->s "
            + "SET r.classes = FILTER(c IN coalesce(r.classes, []) where NOT(c IN r.classes)) + {classes}, "
            + "r.groups = FILTER(g IN coalesce(r.groups, []) where NOT(g IN r.groups)) + {groups}, "
            + "r.lastUpdated = {now}, r.source = {source} ";
    private static final String DELETE_SUBJECT = "MATCH (s:Structure {externalId : {structureExternalId}})<-[:SUBJECT]-(sub:Subject {source: {source}}) "
            + "WHERE NOT(sub.id IN {subjects}) " + "DETACH DELETE sub";
    private static final String UNLINK_SUBJECT = "MATCH (s:Structure {externalId : {structureExternalId}})<-[:SUBJECT]-(:Subject)<-[r:TEACHES {source: {source}}]-(:User) "
            + "WHERE r.lastUpdated <> {now} " + "DELETE r";
    protected static final String UNKNOWN_CLASSES = "MATCH (s:Structure {UAI : {UAI}})<-[:BELONGS]-(c:Class) "
            + "WHERE c.name = {className} " + "WITH count(*) AS exists " + "MATCH (s:Structure {UAI : {UAI}}) "
            + "WHERE exists = 0 " + "MERGE (cm:ClassesMapping { UAI : {UAI}}) "
            + "SET cm.unknownClasses = coalesce(FILTER(cn IN cm.unknownClasses WHERE cn <> {className}), []) + {className} "
            + "MERGE (s)<-[:MAPPING]-(cm) ";
    protected static final String CREATE_GROUPS = "MATCH (s:Structure {externalId : {structureExternalId}}) "
            + "MERGE (fg:FunctionalGroup:Group {externalId:{externalId}}) "
            + "ON CREATE SET fg.name = {name}, fg.id = {id}, fg.source = {source}, fg.displayNameSearchField = {displayNameSearchField} "
            + "MERGE (fg)-[:DEPENDS]->(s) ";
    private static final String PERSEDUCNAT_TO_GROUPS = "MATCH (u:User {id : {id}}), (fg:FunctionalGroup) "
            + "WHERE fg.externalId IN {groups} " + "MERGE u-[r:IN]->fg "
            + "SET r.lastUpdated = {now}, r.source = {source}, r.outDate = {outDate} ";
    private static final String PERSEDUCNAT_TO_CLASSES = "MATCH (u:User {id : {id}}), (:Structure {externalId : {structureExternalId}})<-[:BELONGS]-(c:Class)"
            + "<-[:DEPENDS]-(pg:ProfileGroup {name : c.name + '-' + {profile}}) "
            + "WHERE u.source = {source} AND c.name IN {classes} " + "MERGE u-[r:IN]->pg "
            + "SET r.lastUpdated = {now}, r.source = {source}, r.outDate = {outDate} ";
    private static final String UNLINK_GROUP = "MATCH (s:Structure {externalId : {structureExternalId}})<-[:DEPENDS]-(fg:FunctionalGroup)<-[r:IN]-(u:User) "
            + "WHERE r.source = {source} AND (r.outDate < {now} OR r.lastUpdated < {now}) "
            + "OPTIONAL MATCH fg-[rc:COMMUNIQUE]-u " + "DELETE r, rc";
    private static final String DELETE_GROUPS = "MATCH (:Structure {externalId : {structureExternalId}})<-[:DEPENDS]-(g:FunctionalGroup {source:{source}})<-[:IN]-(:User) "
            + "WITH COLLECT(distinct g.id) as usedFunctionalGroup "
            + "MATCH (:Structure {externalId : {structureExternalId}})<-[:DEPENDS]-(g:FunctionalGroup {source:{source}}) "
            + "WHERE NOT(g.id IN usedFunctionalGroup) " + "DETACH DELETE g ";
    // prevent difference between relationships and properties
    private static final String UNSET_OLD_GROUPS = "MATCH (:Structure {externalId : {structureExternalId}})<-[:DEPENDS]-(:ProfileGroup)<-[:IN]-(u:User) "
            + "WHERE NOT(HAS(u.deleteDate)) AND has(u.groups) AND LENGTH(u.groups) > 0 "
            + "AND NOT(u-[:IN]->(:FunctionalGroup)) " + "SET u.groups = [];";
    private static final String SET_GROUPS = "MATCH (:Structure {externalId : {structureExternalId}})<-[:DEPENDS]-(:ProfileGroup)<-[:IN]-(u:User) "
            + "WITH u " + "MATCH u-[:IN]->(g:FunctionalGroup) " + "WHERE has(u.groups) "
            + "WITH u, collect(g.externalId) as groups " + "SET u.groups = groups";
    public static final String COURSES = "courses";
    protected long importTimestamp;
    protected final String UAI;
    protected final Report report;
    protected final JsonArray structure = new fr.wseduc.webutils.collections.JsonArray();
    protected String structureExternalId;
    protected String structureId;
    protected JsonObject classesMapping;
    protected DateTime startDateWeek1;
    protected int slotDuration; // seconds
    protected Map<String, Slot> slots = new HashMap<>();
    protected final Map<String, String> rooms = new HashMap<>();
    protected final Map<String, String[]> teachersMapping = new HashMap<>();
    protected final Map<String, String[]> teachersCleanNameMapping = new HashMap<>();
    protected final Map<String, String> teachers = new HashMap<>();
    protected final Map<String, String> subjectsMapping = new HashMap<>();
    protected final Map<String, String> subjects = new HashMap<>();
    protected final Map<String, JsonObject> classes = new HashMap<>();
    protected final Map<String, JsonObject> groups = new HashMap<>();

    protected PersEducNat persEducNat;
    protected TransactionHelper txXDT;
    private final MongoDb mongoDb = MongoDb.getInstance();
    private final AtomicInteger countMongoQueries = new AtomicInteger(0);
    private Handler<AsyncResult<Report>> endHandler;
    protected final String basePath;
    private boolean txSuccess = false;
    protected Set<String> userImportedExternalId = new HashSet<>();
    private volatile JsonArray coursesBuffer = new fr.wseduc.webutils.collections.JsonArray();

    protected AbstractTimetableImporter(String uai, String path, String acceptLanguage) {
        UAI = uai;
        this.basePath = path;
        this.report = new Report(acceptLanguage);
    }

    protected void init(final Handler<AsyncResult<Void>> handler) throws TransactionException {
        importTimestamp = System.currentTimeMillis();
        final String externalIdFromUAI = "MATCH (s:Structure {UAI : {UAI}}) "
                + "return s.externalId as externalId, s.id as id, s.timetable as timetable ";
        final String tma = getTeacherMappingAttribute();
        final String getUsersByProfile = "MATCH (:Structure {UAI : {UAI}})<-[:DEPENDS]-(:ProfileGroup)<-[:IN]-(u:User) "
                + "WHERE head(u.profiles) = {profile} AND NOT(u." + tma + " IS NULL) "
                + "RETURN DISTINCT u.id as id, u." + tma
                + " as tma, head(u.profiles) as profile, u.source as source, "
                + "u.lastName as lastName, u.firstName as firstName";
        final String classesMappingQuery = "MATCH (s:Structure {UAI : {UAI}})<-[:MAPPING]-(cm:ClassesMapping) "
                + "return cm.mapping as mapping ";
        final String subjectsMappingQuery = "MATCH (s:Structure {UAI : {UAI}})<-[:SUBJECT]-(sub:Subject) return sub.code as code, sub.id as id";
        final TransactionHelper tx = TransactionManager.getTransaction();
        tx.add(getUsersByProfile, new JsonObject().put("UAI", UAI).put("profile", "Teacher"));
        tx.add(externalIdFromUAI, new JsonObject().put("UAI", UAI));
        tx.add(classesMappingQuery, new JsonObject().put("UAI", UAI));
        tx.add(subjectsMappingQuery, new JsonObject().put("UAI", UAI));
        tx.commit(new Handler<Message<JsonObject>>() {
            @Override
            public void handle(Message<JsonObject> event) {
                final JsonArray res = event.body().getJsonArray("results");
                if ("ok".equals(event.body().getString("status")) && res != null && res.size() == 4) {
                    try {
                        for (Object o : res.getJsonArray(0)) {
                            if (o instanceof JsonObject) {
                                final JsonObject j = (JsonObject) o;
                                teachersMapping.put(j.getString("tma"),
                                        new String[] { j.getString("id"), j.getString("source") });
                                teachersCleanNameMapping.put(
                                        Validator.sanitize(j.getString("firstName") + j.getString("lastName")),
                                        new String[] { j.getString("id"), j.getString("source") });
                            }
                        }
                        JsonArray a = res.getJsonArray(1);
                        if (a != null && a.size() == 1) {
                            structureExternalId = a.getJsonObject(0).getString("externalId");
                            structure.add(structureExternalId);
                            structureId = a.getJsonObject(0).getString("id");
                            if (!getSource().equals(a.getJsonObject(0).getString("timetable"))) {
                                handler.handle(new DefaultAsyncResult<Void>(
                                        new TransactionException("different.timetable.type")));
                                return;
                            }
                        } else {
                            handler.handle(new DefaultAsyncResult<Void>(new ValidationException("invalid.uai")));
                            return;
                        }
                        JsonArray cm = res.getJsonArray(2);
                        if (cm != null && cm.size() == 1) {
                            try {
                                final JsonObject cmn = cm.getJsonObject(0);
                                log.info(cmn.encode());
                                if (isNotEmpty(cmn.getString("mapping"))) {
                                    classesMapping = new JsonObject(cmn.getString("mapping"));
                                    log.info("classMapping : " + classesMapping.encodePrettily());
                                } else {
                                    classesMapping = new JsonObject();
                                }
                            } catch (Exception ecm) {
                                classesMapping = new JsonObject();
                                log.error(ecm.getMessage(), ecm);
                            }
                        }
                        JsonArray subjects = res.getJsonArray(3);
                        if (subjects != null && subjects.size() > 0) {
                            for (Object o : subjects) {
                                if (o instanceof JsonObject) {
                                    final JsonObject s = (JsonObject) o;
                                    subjectsMapping.put(s.getString("code"), s.getString("id"));
                                }
                            }
                        }
                        txXDT = TransactionManager.getTransaction();
                        persEducNat = new PersEducNat(txXDT, report, getSource());
                        persEducNat.setMapping(
                                "dictionary/mapping/" + getSource().toLowerCase() + "/PersEducNat.json");
                        handler.handle(new DefaultAsyncResult<>((Void) null));
                    } catch (Exception e) {
                        handler.handle(new DefaultAsyncResult<Void>(e));
                    }
                } else {
                    handler.handle(new DefaultAsyncResult<Void>(
                            new TransactionException(event.body().getString("message"))));
                }
            }
        });
    }

    protected void addSubject(String id, JsonObject currentEntity) {
        String subjectId = subjectsMapping.get(currentEntity.getString("Code"));
        if (isEmpty(subjectId)) {
            final String externalId = structureExternalId + "$" + currentEntity.getString("Code");
            subjectId = UUID.randomUUID().toString();
            txXDT.add(CREATE_SUBJECT,
                    currentEntity.put("structureExternalId", structureExternalId).put("externalId", externalId)
                            .put("id", subjectId).put("source", getSource()).put("now", importTimestamp));
        }
        subjects.put(id, subjectId);
    }

    protected void persistCourse(JsonObject object) {
        if (object == null) {
            return;
        }
        persEducNatToClasses(object);
        persEducNatToGroups(object);
        persEducNatToSubjects(object);
        object.put("pending", importTimestamp);
        final int currentCount = countMongoQueries.incrementAndGet();
        JsonObject m = new JsonObject().put("$set", object).put("$setOnInsert",
                new JsonObject().put("created", importTimestamp));
        coursesBuffer.add(new JsonObject().put("operation", "upsert").put("document", m).put("criteria",
                new JsonObject().put("_id", object.getString("_id"))));

        if (currentCount % 1000 == 0) {
            persistBulKCourses();
        }
    }

    private void persistBulKCourses() {
        final JsonArray cf = coursesBuffer;
        coursesBuffer = new fr.wseduc.webutils.collections.JsonArray();
        final int countCoursesBuffer = cf.size();
        if (countCoursesBuffer > 0) {
            mongoDb.bulk(COURSES, cf, new Handler<Message<JsonObject>>() {
                @Override
                public void handle(Message<JsonObject> event) {
                    if (!"ok".equals(event.body().getString("status"))) {
                        if (event.body().getString("message") == null
                                || !event.body().getString("message").contains("duplicate key error")) {
                            report.addError("error.persist.course");
                        } else {
                            log.warn("Duplicate courses keys.");
                        }
                    }
                    if (countMongoQueries.addAndGet(-countCoursesBuffer) == 0) {
                        end();
                    }
                }
            });
        }
    }

    private void persEducNatToClasses(JsonObject object) {
        final JsonArray classes = object.getJsonArray("classes");
        if (classes != null) {
            final JsonObject params = new JsonObject().put("structureExternalId", structureExternalId)
                    .put("classes", classes).put("source", getSource())
                    .put("outDate", DateTime.now().plusDays(1).getMillis()).put("now", importTimestamp);
            final JsonArray teacherIds = object.getJsonArray("teacherIds");
            if (teacherIds != null && teacherIds.size() > 0) {
                params.put("profile", "Teacher");
                for (Object id : teacherIds) {
                    if (id != null) {
                        txXDT.add(PERSEDUCNAT_TO_CLASSES, params.copy().put("id", id.toString()));
                    }
                }
            }
            final JsonArray personnelIds = object.getJsonArray("personnelIds");
            if (personnelIds != null && personnelIds.size() > 0) {
                params.put("profile", "Personnel");
                for (Object id : personnelIds) {
                    if (id != null) {
                        txXDT.add(PERSEDUCNAT_TO_CLASSES, params.copy().put("id", id.toString()));
                    }
                }
            }
        }
    }

    private void persEducNatToSubjects(JsonObject object) {
        final String subjectId = object.getString("subjectId");
        final JsonArray teacherIds = object.getJsonArray("teacherIds");
        if (isNotEmpty(subjectId) && teacherIds != null && teacherIds.size() > 0) {
            final JsonObject params = new JsonObject().put("subjectId", subjectId).put("teacherIds", teacherIds)
                    .put("classes", object.getJsonArray("classes", new fr.wseduc.webutils.collections.JsonArray()))
                    .put("groups", object.getJsonArray("groups", new fr.wseduc.webutils.collections.JsonArray()))
                    .put("source", getSource()).put("now", importTimestamp);
            txXDT.add(LINK_SUBJECT, params);
        }
    }

    private void persEducNatToGroups(JsonObject object) {
        final JsonArray groups = object.getJsonArray("groups");
        if (groups != null) {
            final JsonArray teacherIds = object.getJsonArray("teacherIds");
            final List<String> ids = new ArrayList<>();
            if (teacherIds != null) {
                ids.addAll(teacherIds.getList());
            }
            final JsonArray personnelIds = object.getJsonArray("personnelIds");
            if (personnelIds != null) {
                ids.addAll(personnelIds.getList());
            }
            if (!ids.isEmpty()) {
                final JsonArray g = new fr.wseduc.webutils.collections.JsonArray();
                for (Object o : groups) {
                    g.add(structureExternalId + "$" + o.toString());
                }
                for (String id : ids) {
                    txXDT.add(PERSEDUCNAT_TO_GROUPS,
                            new JsonObject().put("groups", g).put("id", id).put("source", getSource())
                                    .put("outDate", DateTime.now().plusDays(1).getMillis())
                                    .put("now", importTimestamp));
                }
            }
        }
    }

    protected void updateUser(JsonObject user) {
        user.remove("Ident");
        user.remove("epj");
        final String attrs = Neo4jUtils.nodeSetPropertiesFromJson("u", user, "id", "externalId", "login",
                "activationCode", "displayName", "email");
        if (isNotEmpty(attrs.trim())) {
            final String updateUser = "MATCH (u:User {" + getTeacherMappingAttribute() + ": {"
                    + getTeacherMappingAttribute() + "}}) " + "SET " + attrs;
            txXDT.add(updateUser, user);
        }
    }

    private void end() {
        if (endHandler != null && countMongoQueries.get() == 0) {
            final JsonObject baseQuery = new JsonObject().put("structureId", structureId);
            if (txSuccess) {
                mongoDb.update(COURSES, baseQuery.copy().put("pending", importTimestamp),
                        new JsonObject().put("$rename", new JsonObject().put("pending", "modified")), false, true,
                        new Handler<Message<JsonObject>>() {
                            @Override
                            public void handle(Message<JsonObject> event) {
                                if ("ok".equals(event.body().getString("status"))) {
                                    mongoDb.update(COURSES,
                                            baseQuery.copy().put("deleted", new JsonObject().put("$exists", false))
                                                    .put("modified", new JsonObject().put("$ne", importTimestamp)),
                                            new JsonObject().put("$set",
                                                    new JsonObject().put("deleted", importTimestamp)),
                                            false, true, new Handler<Message<JsonObject>>() {
                                                @Override
                                                public void handle(Message<JsonObject> event) {
                                                    if (!"ok".equals(event.body().getString("status"))) {
                                                        report.addError("error.set.deleted.courses");
                                                    }
                                                    endHandler.handle(new DefaultAsyncResult<>(report));
                                                }
                                            });
                                } else {
                                    report.addError("error.renaming.pending");
                                    endHandler.handle(new DefaultAsyncResult<>(report));
                                }
                            }
                        });
            } else {
                mongoDb.delete(COURSES, baseQuery.copy().put("pending", importTimestamp).put("modified",
                        new JsonObject().put("$exists", false)), new Handler<Message<JsonObject>>() {
                            @Override
                            public void handle(Message<JsonObject> event) {
                                if ("ok".equals(event.body().getString("status"))) {
                                    mongoDb.update(COURSES, baseQuery.copy().put("pending", importTimestamp),
                                            new JsonObject().put("$unset", new JsonObject().put("pending", "")),
                                            false, true, new Handler<Message<JsonObject>>() {
                                                @Override
                                                public void handle(Message<JsonObject> event) {
                                                    if (!"ok".equals(event.body().getString("status"))) {
                                                        report.addError("error.unset.pending");
                                                    }
                                                    endHandler.handle(new DefaultAsyncResult<>(report));
                                                }
                                            });
                                } else {
                                    report.addError("error.removing.inconsistencies.courses");
                                    endHandler.handle(new DefaultAsyncResult<>(report));
                                }
                            }
                        });
            }
        }
    }

    protected void commit(final Handler<AsyncResult<Report>> handler) {
        final JsonObject params = new JsonObject().put("structureExternalId", structureExternalId)
                .put("source", getSource()).put("now", importTimestamp);
        persistBulKCourses();
        txXDT.add(DELETE_SUBJECT, params.copy().put("subjects",
                new fr.wseduc.webutils.collections.JsonArray(new ArrayList<>(subjects.values()))));
        txXDT.add(UNLINK_SUBJECT, params);
        txXDT.add(UNLINK_GROUP, params);
        txXDT.add(DELETE_GROUPS, params);
        txXDT.add(UNSET_OLD_GROUPS, params);
        txXDT.add(SET_GROUPS, params);
        Importer.markMissingUsers(structureExternalId, getSource(), userImportedExternalId, txXDT,
                new Handler<Void>() {
                    @Override
                    public void handle(Void event) {
                        Importer.restorePreDeletedUsers(getSource(), txXDT);
                        txXDT.commit(new Handler<Message<JsonObject>>() {
                            @Override
                            public void handle(Message<JsonObject> event) {
                                if (!"ok".equals(event.body().getString("status"))) {
                                    report.addError("error.commit.timetable.transaction");
                                } else {
                                    txSuccess = true;
                                }
                                endHandler = handler;
                                end();
                            }
                        });
                    }
                });
    }

    protected abstract String getSource();

    protected abstract String getTeacherMappingAttribute();

    public static void updateMergedUsers(JsonArray mergedUsers) {
        if (mergedUsers == null)
            return;
        long now = System.currentTimeMillis();
        for (int i = 1; i < mergedUsers.size(); i += 2) {
            final JsonArray a = mergedUsers.getJsonArray(i);
            if (a.size() > 0) {
                updateMergedUsers(a.getJsonObject(0), now);
            }
        }
    }

    private static void updateMergedUsers(JsonObject j, long now) {
        final String oldId = j.getString("oldId");
        final String id = j.getString("id");
        final String profile = j.getString("profile");
        if (isEmpty(oldId) || isEmpty(id) || (!"Teacher".equals(profile) && !"Personnel".equals(profile))) {
            return;
        }
        final JsonObject query = new JsonObject();
        final JsonObject modifier = new JsonObject();
        final String pl = profile.toLowerCase();
        query.put(pl + "Ids", oldId);
        modifier.put("$set", new JsonObject().put(pl + "Ids.$", id).put("modified", now));
        MongoDb.getInstance().update(COURSES, query, modifier, false, true);
    }

    public static void transition(final String structureExternalId) {
        if (isNotEmpty(structureExternalId)) {
            final String query = "MATCH (s:Structure {externalId: {externalId}}) RETURN s.id as structureId";
            final JsonObject params = new JsonObject().put("externalId", structureExternalId);
            TransactionManager.getNeo4jHelper().execute(query, params, new Handler<Message<JsonObject>>() {
                @Override
                public void handle(Message<JsonObject> event) {
                    final JsonArray res = event.body().getJsonArray("result");
                    if ("ok".equals(event.body().getString("status")) && res != null && res.size() > 0) {
                        transitionDeleteCourses(res.getJsonObject(0));
                        transitionDeleteSubjectsAndMapping(structureExternalId);
                    }
                }
            });
        } else {
            transitionDeleteCourses(new JsonObject());
            transitionDeleteSubjectsAndMapping(structureExternalId);
        }
    }

    private static void transitionDeleteCourses(final JsonObject query) {
        MongoDb.getInstance().delete(COURSES, query, new Handler<Message<JsonObject>>() {
            @Override
            public void handle(Message<JsonObject> event) {
                if (!"ok".equals(event.body().getString("status"))) {
                    log.error("Courses timetable transition error on structure " + query.encode() + " - message : "
                            + event.body().getString("message"));
                }
            }
        });
    }

    private static void transitionDeleteSubjectsAndMapping(final String structureExternalId) {
        final JsonObject params = new JsonObject();
        String filter = "";
        if (isNotEmpty(structureExternalId)) {
            filter = " {externalId : {structureExternalId}}";
            params.put("structureExternalId", structureExternalId);
        }
        try {
            final TransactionHelper tx = TransactionManager.getTransaction();
            tx.add("MATCH (s:Structure" + filter + ") SET s.timetable = 'NOP'", params);
            tx.add("MATCH (:Structure" + filter + ")<-[:SUBJECT]-(sub:Subject) DETACH DELETE sub", params);
            tx.add("MATCH (:Structure" + filter + ")<-[:MAPPING]-(cm:ClassesMapping) DETACH DELETE cm", params);
            tx.commit(new Handler<Message<JsonObject>>() {
                @Override
                public void handle(Message<JsonObject> event) {
                    if (!"ok".equals(event.body().getString("status"))) {
                        log.error("Subjects timetable transition error on structure " + params.encode()
                                + " - message : " + event.body().getString("message"));
                    }
                }
            });
        } catch (TransactionException e) {
            log.error("Unable to acquire transaction for timetable transition", e);
        }
    }

    public static void initStructure(final EventBus eb, final Message<JsonObject> message) {
        final JsonObject conf = message.body().getJsonObject("conf");
        if (conf == null) {
            message.reply(new JsonObject().put("status", "error").put("message", "invalid.conf"));
            return;
        }
        final String query = "MATCH (s:Structure {id:{structureId}}) "
                + "RETURN (NOT(HAS(s.timetable)) OR s.timetable <> {type}) as update ";
        TransactionManager.getNeo4jHelper().execute(query, conf, new Handler<Message<JsonObject>>() {
            @Override
            public void handle(final Message<JsonObject> event) {
                final JsonArray j = event.body().getJsonArray("result");
                if ("ok".equals(event.body().getString("status")) && j != null && j.size() == 1
                        && j.getJsonObject(0).getBoolean("update", false)) {
                    try {
                        TransactionHelper tx = TransactionManager.getTransaction();
                        final String q1 = "MATCH (s:Structure {id : {structureId}})<-[:DEPENDS]-(fg:FunctionalGroup) "
                                + "WHERE NOT(HAS(s.timetable)) OR s.timetable <> {type} "
                                + "OPTIONAL MATCH fg<-[:IN]-(u:User) "
                                + "RETURN fg.id as group, fg.name as groupName, collect(u.id) as users ";
                        final String q2 = "MATCH (s:Structure {id: {structureId}}) "
                                + "WHERE NOT(HAS(s.timetable)) OR s.timetable <> {type} "
                                + "SET s.timetable = {typeUpdate} " + "WITH s "
                                + "MATCH s<-[:DEPENDS]-(fg:FunctionalGroup), s<-[:SUBJECT]-(sub:Subject) "
                                + "DETACH DELETE fg, sub ";
                        final String q3 = "MATCH (s:Structure {id: {structureId}})<-[:MAPPING]-(cm:ClassesMapping) "
                                + "DETACH DELETE cm";
                        tx.add(q1, conf);
                        tx.add(q2, (isEmpty(conf.getString("type")) ? conf.putNull("typeUpdate")
                                : conf.put("typeUpdate", conf.getString("type"))));
                        tx.add(q3, conf);
                        tx.commit(new Handler<Message<JsonObject>>() {
                            @Override
                            public void handle(Message<JsonObject> res) {
                                if ("ok".equals(res.body().getString("status"))) {
                                    final JsonArray r = res.body().getJsonArray("results");
                                    if (r != null && r.size() == 2) {
                                        Transition.publishDeleteGroups(eb, log, r.getJsonArray(0));
                                    }
                                    final JsonObject matcher = new JsonObject().put("structureId",
                                            conf.getString("structureId"));
                                    MongoDb.getInstance().delete(COURSES, matcher,
                                            new Handler<Message<JsonObject>>() {
                                                @Override
                                                public void handle(Message<JsonObject> mongoResult) {
                                                    if (!"ok".equals(mongoResult.body().getString("status"))) {
                                                        log.error("Error deleting courses : "
                                                                + mongoResult.body().getString("message"));
                                                    }
                                                    message.reply(event.body());
                                                }
                                            });
                                } else {
                                    message.reply(res.body());
                                }
                            }
                        });
                    } catch (TransactionException e) {
                        log.error("Transaction error when init timetable structure", e);
                        message.reply(new JsonObject().put("status", "error").put("message", e.getMessage()));
                    }
                } else {
                    message.reply(event.body());
                }
            }
        });
    }

}