Java tutorial
/* 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.common.share.impl; import fr.wseduc.webutils.I18n; import io.vertx.core.eventbus.Message; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import org.entcore.common.neo4j.Neo4j; import org.entcore.common.neo4j.Neo4jResult; import org.entcore.common.neo4j.StatementsBuilder; import org.entcore.common.share.ShareService; import org.entcore.common.user.UserUtils; import fr.wseduc.webutils.Either; import fr.wseduc.webutils.security.ActionType; import fr.wseduc.webutils.security.SecuredAction; import org.entcore.common.validation.StringValidation; import io.vertx.core.Handler; import io.vertx.core.eventbus.EventBus; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; import static fr.wseduc.webutils.Utils.getOrElse; import static org.entcore.common.neo4j.Neo4jResult.validResultHandler; import static org.entcore.common.user.UserUtils.findVisibleProfilsGroups; import static org.entcore.common.user.UserUtils.findVisibleUsers; import static org.entcore.common.validation.StringValidation.cleanId; public abstract class GenericShareService implements ShareService { protected static final Logger log = LoggerFactory.getLogger(GenericShareService.class); private static final String GROUP_SHARED = "MATCH (g:Group) WHERE g.id in {groupIds} " + "RETURN distinct g.id as id, g.name as name, g.groupDisplayName as groupDisplayName, g.structureName as structureName " + "ORDER BY name "; private static final String USER_SHARED = "MATCH (u:User) WHERE u.id in {userIds} " + "RETURN distinct u.id as id, u.login as login, u.displayName as username, " + "u.lastName as lastName, u.firstName as firstName, u.profiles[0] as profile " + "ORDER BY username "; protected final EventBus eb; protected final Map<String, SecuredAction> securedActions; protected final Map<String, List<String>> groupedActions; protected static final I18n i18n = I18n.getInstance(); private JsonArray resourceActions; public GenericShareService(EventBus eb, Map<String, SecuredAction> securedActions, Map<String, List<String>> groupedActions) { this.eb = eb; this.securedActions = securedActions; this.groupedActions = groupedActions; } protected JsonArray getResoureActions(Map<String, SecuredAction> securedActions) { if (resourceActions != null) { return resourceActions; } JsonObject resourceActions = new JsonObject(); for (SecuredAction action : securedActions.values()) { if (ActionType.RESOURCE.name().equals(action.getType()) && !action.getDisplayName().isEmpty()) { JsonObject a = resourceActions.getJsonObject(action.getDisplayName()); if (a == null) { a = new JsonObject() .put("name", new fr.wseduc.webutils.collections.JsonArray() .add(action.getName().replaceAll("\\.", "-"))) .put("displayName", action.getDisplayName()).put("type", action.getType()); resourceActions.put(action.getDisplayName(), a); } else { a.getJsonArray("name").add(action.getName().replaceAll("\\.", "-")); } } } this.resourceActions = new fr.wseduc.webutils.collections.JsonArray( new ArrayList<>(resourceActions.getMap().values())); return this.resourceActions; } protected void getShareInfos(final String userId, final JsonArray actions, final JsonObject groupCheckedActions, final JsonObject userCheckedActions, final String acceptLanguage, String search, final Handler<JsonObject> handler) { final JsonObject params = new JsonObject().put("groupIds", new fr.wseduc.webutils.collections.JsonArray(new ArrayList<>(groupCheckedActions.fieldNames()))); final JsonObject params2 = new JsonObject().put("userIds", new fr.wseduc.webutils.collections.JsonArray(new ArrayList<>(userCheckedActions.fieldNames()))); if (search != null && search.trim().isEmpty()) { final Neo4j neo4j = Neo4j.getInstance(); neo4j.execute(GROUP_SHARED, params, validResultHandler(new Handler<Either<String, JsonArray>>() { @Override public void handle(Either<String, JsonArray> sg) { JsonArray visibleGroups; if (sg.isRight()) { visibleGroups = sg.right().getValue(); } else { visibleGroups = new fr.wseduc.webutils.collections.JsonArray(); } final JsonObject groups = new JsonObject(); groups.put("visibles", visibleGroups); groups.put("checked", groupCheckedActions); for (Object u : visibleGroups) { if (!(u instanceof JsonObject)) continue; JsonObject group = (JsonObject) u; UserUtils.groupDisplayName(group, acceptLanguage); } neo4j.execute(USER_SHARED, params2, validResultHandler(new Handler<Either<String, JsonArray>>() { @Override public void handle(Either<String, JsonArray> event) { JsonArray visibleUsers; if (event.isRight()) { visibleUsers = event.right().getValue(); } else { visibleUsers = new fr.wseduc.webutils.collections.JsonArray(); } JsonObject users = new JsonObject(); users.put("visibles", visibleUsers); users.put("checked", userCheckedActions); JsonObject share = new JsonObject().put("actions", actions) .put("groups", groups).put("users", users); handler.handle(share); } })); } })); } else { final String preFilter; if (search != null) { preFilter = "AND m.displayNameSearchField CONTAINS {search} "; String sanitizedSearch = StringValidation.sanitize(search); params.put("search", sanitizedSearch); params2.put("search", sanitizedSearch); } else { preFilter = null; } final String q = "RETURN distinct profileGroup.id as id, profileGroup.name as name, " + "profileGroup.groupDisplayName as groupDisplayName, profileGroup.structureName as structureName " + "ORDER BY name " + "UNION " + GROUP_SHARED; final String q2 = "RETURN distinct visibles.id as id, visibles.login as login, visibles.displayName as username, " + "visibles.lastName as lastName, visibles.firstName as firstName, visibles.profiles[0] as profile " + "ORDER BY username " + "UNION " + USER_SHARED; UserUtils.findVisibleProfilsGroups(eb, userId, q, params, new Handler<JsonArray>() { @Override public void handle(JsonArray visibleGroups) { final JsonObject groups = new JsonObject(); groups.put("visibles", visibleGroups); groups.put("checked", groupCheckedActions); for (Object u : visibleGroups) { if (!(u instanceof JsonObject)) continue; JsonObject group = (JsonObject) u; UserUtils.groupDisplayName(group, acceptLanguage); } findVisibleUsers(eb, userId, false, preFilter, q2, params2, new Handler<JsonArray>() { @Override public void handle(JsonArray visibleUsers) { JsonObject users = new JsonObject(); users.put("visibles", visibleUsers); users.put("checked", userCheckedActions); JsonObject share = new JsonObject().put("actions", actions).put("groups", groups) .put("users", users); handler.handle(share); } }); } }); } } // TODO improve query protected void profilGroupIsVisible(String userId, final String groupId, final Handler<Boolean> handler) { if (userId == null || groupId == null) { handler.handle(false); return; } findVisibleProfilsGroups(eb, userId, new Handler<JsonArray>() { @Override public void handle(JsonArray visibleGroups) { final List<String> visibleGroupsIds = new ArrayList<>(); for (int i = 0; i < visibleGroups.size(); i++) { JsonObject j = visibleGroups.getJsonObject(i); if (j != null && j.getString("id") != null) { visibleGroupsIds.add(j.getString("id")); } } handler.handle(visibleGroupsIds.contains(groupId)); } }); } protected void userIsVisible(String userId, final String userShareId, final Handler<Boolean> handler) { if (userId == null || userShareId == null) { handler.handle(false); return; } findVisibleUsers(eb, userId, false, new Handler<JsonArray>() { @Override public void handle(JsonArray visibleUsers) { final List<String> visibleUsersIds = new ArrayList<>(); for (int i = 0; i < visibleUsers.size(); i++) { JsonObject j = visibleUsers.getJsonObject(i); if (j != null && j.getString("id") != null) { visibleUsersIds.add(j.getString("id")); } } handler.handle(visibleUsersIds.contains(userShareId)); } }); } protected boolean actionsExists(List<String> actions) { if (securedActions != null) { List<String> a = new ArrayList<>(); for (String action : securedActions.keySet()) { a.add(action.replaceAll("\\.", "-")); } return a.containsAll(actions); } return false; } protected void groupShareValidation(String userId, String groupShareId, final List<String> actions, final Handler<Either<String, JsonObject>> handler) { profilGroupIsVisible(userId, groupShareId, new Handler<Boolean>() { @Override public void handle(Boolean visible) { if (Boolean.TRUE.equals(visible)) { if (actionsExists(actions)) { handler.handle(new Either.Right<String, JsonObject>(new JsonObject())); } else { handler.handle(new Either.Left<String, JsonObject>("Invalid actions.")); } } else { handler.handle(new Either.Left<String, JsonObject>("Profil group not found.")); } } }); } protected void userShareValidation(String userId, String userShareId, final List<String> actions, final Handler<Either<String, JsonObject>> handler) { userIsVisible(userId, userShareId, new Handler<Boolean>() { @Override public void handle(Boolean visible) { if (Boolean.TRUE.equals(visible)) { if (actionsExists(actions)) { handler.handle(new Either.Right<String, JsonObject>(new JsonObject())); } else { handler.handle(new Either.Left<String, JsonObject>("Invalid actions.")); } } else { handler.handle(new Either.Left<String, JsonObject>("User not found.")); } } }); } protected void shareValidation(String resourceId, String userId, JsonObject share, Handler<Either<String, JsonObject>> handler) { final JsonObject groups = share.getJsonObject("groups"); final JsonObject users = share.getJsonObject("users"); final JsonObject shareBookmark = share.getJsonObject("bookmarks"); final HashMap<String, Set<String>> membersActions = new HashMap<>(); if (groups != null && groups.size() > 0) { for (String attr : groups.fieldNames()) { JsonArray actions = groups.getJsonArray(attr); if (actionsExists(actions.getList())) { membersActions.put(attr, new HashSet<>(actions.getList())); } } } if (users != null && users.size() > 0) { for (String attr : users.fieldNames()) { JsonArray actions = users.getJsonArray(attr); if (actionsExists(actions.getList())) { membersActions.put(attr, new HashSet<>(actions.getList())); } } } if (shareBookmark != null && shareBookmark.size() > 0) { final JsonObject p = new JsonObject().put("userId", userId); StatementsBuilder statements = new StatementsBuilder(); for (String sbId : shareBookmark.fieldNames()) { final String csbId = cleanId(sbId); final String query = "MATCH (:User {id:{userId}})-[:HAS_SB]->(sb:ShareBookmark) " + "RETURN DISTINCT '" + csbId + "' as id, TAIL(sb." + csbId + ") as members "; statements.add(query, p); } Neo4j.getInstance().executeTransaction(statements.build(), null, true, Neo4jResult.validResultsHandler(sbRes -> { if (sbRes.isRight()) { JsonArray a = sbRes.right().getValue(); for (Object o : a) { JsonObject r = ((JsonArray) o).getJsonObject(0); JsonArray actions = shareBookmark.getJsonArray(r.getString("id")); JsonArray mIds = r.getJsonArray("members"); if (actions != null && mIds != null && mIds.size() > 0 && actionsExists(actions.getList())) { for (Object mId : mIds) { Set<String> actionsShare = membersActions.get(mId.toString()); if (actionsShare == null) { actionsShare = new HashSet<>(new HashSet<>(actions.getList())); membersActions.put(mId.toString(), actionsShare); // } else { // actionsShare.addAll(new HashSet<>(actions.getList())); } } } } shareValidationVisible(userId, resourceId, handler, membersActions, shareBookmark.fieldNames()); } else { handler.handle(new Either.Left<>(sbRes.left().getValue())); } })); } else { shareValidationVisible(userId, resourceId, handler, membersActions, null); } } private void shareValidationVisible(String userId, String resourceId, Handler<Either<String, JsonObject>> handler, HashMap<String, Set<String>> membersActions, Set<String> shareBookmarkIds) { // final String preFilter = "AND m.id IN {members} "; final Set<String> members = membersActions.keySet(); final JsonObject params = new JsonObject().put("members", new JsonArray(new ArrayList<>(members))); // final String customReturn = "RETURN DISTINCT visibles.id as id, has(visibles.login) as isUser"; // UserUtils.findVisibles(eb, userId, customReturn, params, true, true, false, null, preFilter, res -> { checkMembers(params, res -> { if (res != null) { final JsonArray users = new JsonArray(); final JsonArray groups = new JsonArray(); final JsonArray shared = new JsonArray(); final JsonArray notifyMembers = new JsonArray(); for (Object o : res) { JsonObject j = (JsonObject) o; final String attr = j.getString("id"); if (Boolean.TRUE.equals(j.getBoolean("isUser"))) { users.add(attr); notifyMembers.add(new JsonObject().put("userId", attr)); prepareSharedArray(resourceId, "userId", shared, attr, membersActions.get(attr)); } else { groups.add(attr); notifyMembers.add(new JsonObject().put("groupId", attr)); prepareSharedArray(resourceId, "groupId", shared, attr, membersActions.get(attr)); } } handler.handle(new Either.Right<>(params.put("shared", shared).put("groups", groups) .put("users", users).put("notify-members", notifyMembers))); if (shareBookmarkIds != null && res.size() < members.size()) { members.removeAll(groups.getList()); members.removeAll(users.getList()); resyncShareBookmark(userId, members, shareBookmarkIds); } } else { handler.handle(new Either.Left<>("Invalid members count.")); } }); } private void checkMembers(JsonObject params, Handler<JsonArray> handler) { final String query = "MATCH (v:Visible) " + "WHERE v.id IN {members} AND NOT(HAS(v.deleteDate)) AND (NOT(HAS(v.blocked)) OR v.blocked = false) " + "RETURN DISTINCT v.id as id, has(v.login) as isUser "; Neo4j.getInstance().execute(query, params, event -> { if ("ok".equals(event.body().getString("status"))) { handler.handle(event.body().getJsonArray("result")); } else { handler.handle(null); } }); } private void resyncShareBookmark(String userId, Set<String> members, Set<String> shareBookmarkIds) { final StringBuilder query = new StringBuilder( "MATCH (:User {id:{userId}})-[:HAS_SB]->(sb:ShareBookmark) SET "); for (String sb : shareBookmarkIds) { query.append("sb.").append(sb).append(" = FILTER(mId IN sb.").append(sb) .append(" WHERE NOT(mId IN {members})), "); } JsonObject params = new JsonObject().put("userId", userId).put("members", new JsonArray(new ArrayList<>(members))); Neo4j.getInstance().execute(query.substring(0, query.length() - 2), params, res -> { if ("ok".equals(res.body().getString("status"))) { log.info("Resync share bookmark for user " + userId); } else { log.error("Error when resync share bookmark for user " + userId + " : " + res.body().getString("message")); } }); } protected void getNotifyMembers(Handler<Either<String, JsonObject>> handler, JsonArray oldShared, JsonArray members, Function<Object, String> f) { JsonArray notifyMembers; if (oldShared != null && oldShared.size() > 0 && members != null && members.size() > 0) { final Set<String> oldMembersIds = oldShared.stream().map(f).collect(Collectors.toSet()); notifyMembers = new JsonArray(); for (Object o : members) { final JsonObject j = (JsonObject) o; final String memberId = getOrElse(j.getString("groupId"), j.getString("userId")); if (!oldMembersIds.contains(memberId)) { notifyMembers.add(j); } } } else { notifyMembers = members; } handler.handle(new Either.Right<>(new JsonObject().put("notify-timeline-array", notifyMembers))); } protected abstract void prepareSharedArray(String resourceId, String type, JsonArray shared, String attr, Set<String> actions); protected List<String> findRemoveActions(List<String> removeActions) { if (removeActions == null || removeActions.isEmpty()) { return null; } List<String> rmActions = new ArrayList<>(removeActions); if (groupedActions != null) { for (Map.Entry<String, List<String>> ga : groupedActions.entrySet()) { for (String a : removeActions) { if (ga.getValue().contains(a)) { rmActions.add(ga.getKey()); break; } } } } return rmActions; } }