Java tutorial
/* * 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.timeline.controllers; import fr.wseduc.bus.BusAddress; import fr.wseduc.rs.Delete; import fr.wseduc.rs.Get; import fr.wseduc.rs.Post; import fr.wseduc.rs.Put; import fr.wseduc.security.ActionType; import fr.wseduc.security.SecuredAction; import fr.wseduc.webutils.Either; import fr.wseduc.webutils.I18n; import fr.wseduc.webutils.Utils; import fr.wseduc.webutils.collections.TTLSet; import fr.wseduc.webutils.http.BaseController; import fr.wseduc.webutils.request.RequestUtils; import io.vertx.core.shareddata.LocalMap; import org.entcore.common.http.filter.AdminFilter; import org.entcore.common.http.filter.AdmlOfStructures; import org.entcore.common.http.filter.ResourceFilter; import org.entcore.common.http.filter.SuperAdminFilter; import org.entcore.common.http.request.JsonHttpServerRequest; import org.entcore.common.notification.TimelineHelper; import org.entcore.common.notification.TimelineNotificationsLoader; import org.entcore.common.notification.NotificationUtils; import org.entcore.common.user.UserInfos; import org.entcore.common.user.UserUtils; import org.entcore.timeline.controllers.helper.NotificationHelper; import org.entcore.timeline.events.DefaultTimelineEventStore; import org.entcore.timeline.events.TimelineEventStore; import org.entcore.timeline.events.TimelineEventStore.AdminAction; import org.entcore.timeline.services.TimelineConfigService; import org.entcore.timeline.services.TimelineMailerService; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.eventbus.Message; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import org.entcore.timeline.services.TimelinePushNotifService; import org.vertx.java.core.http.RouteMatcher; import java.io.StringReader; import java.io.Writer; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import static org.entcore.common.http.response.DefaultResponseHandler.arrayResponseHandler; import static org.entcore.common.http.response.DefaultResponseHandler.defaultResponseHandler; public class TimelineController extends BaseController { private TimelineEventStore store; private TimelineConfigService configService; private TimelineMailerService mailerService; private TimelinePushNotifService pushNotifService; private Map<String, String> registeredNotifications; private LocalMap<String, String> eventsI18n; private HashMap<String, JsonObject> lazyEventsI18n; private Set<String> antiFlood; //Declaring a TimelineHelper ensures the loading of the i18n/timeline folder. private TimelineHelper timelineHelper; private JsonArray eventTypes; // cache to improve perfs private boolean refreshTypesCache; private NotificationHelper notificationHelper; public void init(Vertx vertx, JsonObject config, RouteMatcher rm, Map<String, fr.wseduc.webutils.security.SecuredAction> securedActions) { super.init(vertx, config, rm, securedActions); store = new DefaultTimelineEventStore(); timelineHelper = new TimelineHelper(vertx, eb, config); antiFlood = new TTLSet<>(config.getLong("antiFloodDelay", 3000l), vertx, config.getLong("antiFloodClear", 3600 * 1000l)); refreshTypesCache = config.getBoolean("refreshTypesCache", false); } /* Override i18n to use additional timeline translations and nested templates */ @Override protected void setLambdaTemplateRequest(final HttpServerRequest request, final Map<String, Object> ctx) { super.setLambdaTemplateRequest(request, ctx); TimelineLambda.setLambdaTemplateRequest(request, ctx, eventsI18n, lazyEventsI18n); } @Get("/timeline") @SecuredAction(value = "timeline.view", type = ActionType.AUTHENTICATED) public void view(HttpServerRequest request) { renderView(request); } @Get("/preferencesView") @SecuredAction(value = "timeline.preferencesView", type = ActionType.AUTHENTICATED) public void preferencesView(HttpServerRequest request) { renderView(request); } @Get("/historyView") @SecuredAction(value = "timeline.historyView") public void historyView(HttpServerRequest request) { renderView(request); } @Get("/i18nNotifications") @SecuredAction(value = "timeline.i18n", type = ActionType.AUTHENTICATED) public void i18n(HttpServerRequest request) { String language = Utils.getOrElse(I18n.acceptLanguage(request), "fr", false); String i18n = eventsI18n.get(language.split(",")[0].split("-")[0]); if (i18n == null) { i18n = eventsI18n.get("fr"); } renderJson(request, new JsonObject("{" + i18n.substring(0, i18n.length() - 1) + "}")); } @Get("/registeredNotifications") @SecuredAction(value = "", type = ActionType.AUTHENTICATED) public void registeredNotifications(HttpServerRequest request) { JsonArray reply = new fr.wseduc.webutils.collections.JsonArray(); for (String key : registeredNotifications.keySet()) { JsonObject notif = new JsonObject(registeredNotifications.get(key)).put("key", key); notif.remove("template"); reply.add(notif); } renderJson(request, reply); } @Get("/calendar") @SecuredAction(value = "timeline.calendar", type = ActionType.AUTHENTICATED) public void calendar(HttpServerRequest request) { renderView(request); } @Get("/lastNotifications") @SecuredAction(value = "timeline.events", type = ActionType.AUTHENTICATED) public void lastEvents(final HttpServerRequest request) { final boolean mine = request.params().contains("mine"); UserUtils.getUserInfos(eb, request, new Handler<UserInfos>() { @Override public void handle(final UserInfos user) { if (user != null) { getExternalNotifications(new Handler<Either<String, JsonObject>>() { public void handle(Either<String, JsonObject> notifs) { if (notifs.isLeft()) { badRequest(request, notifs.left().getValue()); return; } String page = request.params().get("page"); List<String> types = request.params().getAll("type"); int offset = 0; try { offset = 25 * Integer.parseInt(page); } catch (NumberFormatException e) { } store.get(user, types, offset, 25, notifs.right().getValue(), mine, new Handler<JsonObject>() { public void handle(final JsonObject res) { if (res != null && "ok".equals(res.getString("status"))) { JsonArray results = res.getJsonArray("results", new fr.wseduc.webutils.collections.JsonArray()); final JsonArray compiledResults = new fr.wseduc.webutils.collections.JsonArray(); final AtomicInteger countdown = new AtomicInteger(results.size()); final Handler<Void> endHandler = new Handler<Void>() { public void handle(Void v) { if (countdown.decrementAndGet() <= 0) { res.put("results", compiledResults); renderJson(request, res); } } }; if (results.size() == 0) endHandler.handle(null); for (Object notifObj : results) { final JsonObject notif = (JsonObject) notifObj; if (!notif.getString("message", "").isEmpty()) { compiledResults.add(notif); endHandler.handle(null); continue; } String key = notif.getString("type", "").toLowerCase() + "." + notif.getString("event-type", "").toLowerCase(); String stringifiedRegisteredNotif = registeredNotifications .get(key); if (stringifiedRegisteredNotif == null) { log.error( "Failed to retrieve registered from the shared map notification with key : " + key); endHandler.handle(null); continue; } JsonObject registeredNotif = new JsonObject( stringifiedRegisteredNotif); StringReader reader = new StringReader( registeredNotif.getString("template", "")); processTemplate(request, notif.getJsonObject("params", new JsonObject()), key, reader, new Handler<Writer>() { public void handle(Writer writer) { notif.put("message", writer.toString()); compiledResults.add(notif); endHandler.handle(null); } }); } } else { renderError(request, res); } } }); } }); } else { unauthorized(request); } } }); } @Get("/types") @SecuredAction(value = "timeline.auth", type = ActionType.AUTHENTICATED) public void listTypes(final HttpServerRequest request) { if (eventTypes != null) { renderJson(request, eventTypes); } else { store.listTypes(new Handler<JsonArray>() { @Override public void handle(JsonArray res) { renderJson(request, res); eventTypes = res; } }); } } @Post("/publish") @SecuredAction("timeline.publish") public void publish(final HttpServerRequest request) { RequestUtils.bodyToJson(request, pathPrefix + "publish", new Handler<JsonObject>() { @Override public void handle(JsonObject json) { store.add(json, new Handler<JsonObject>() { @Override public void handle(JsonObject res) { if ("ok".equals(res.getString("status"))) { created(request); } else { badRequest(request, res.getString("message")); } } }); } }); } @Get("/admin-console") @SecuredAction(value = "", type = ActionType.RESOURCE) @ResourceFilter(SuperAdminFilter.class) public void adminPage(final HttpServerRequest request) { renderView(request); } @Get("/admin-history") @SecuredAction(value = "", type = ActionType.RESOURCE) @ResourceFilter(AdminFilter.class) public void adminHistory(final HttpServerRequest request) { renderView(request); } @Get("/config") @SecuredAction(type = ActionType.AUTHENTICATED, value = "") public void getConfig(final HttpServerRequest request) { configService.list(arrayResponseHandler(request)); } @Put("/config") @SecuredAction(type = ActionType.RESOURCE, value = "") @ResourceFilter(SuperAdminFilter.class) public void updateConfig(final HttpServerRequest request) { RequestUtils.bodyToJson(request, new Handler<JsonObject>() { public void handle(JsonObject data) { configService.upsert(data, defaultResponseHandler(request)); } }); } @Get("/notifications-defaults") @SecuredAction("timeline.external.notifications") public void mixinConfig(final HttpServerRequest request) { configService.list(new Handler<Either<String, JsonArray>>() { public void handle(Either<String, JsonArray> event) { if (event.isLeft()) { badRequest(request); return; } JsonArray admcDefaults = event.right().getValue(); JsonArray reply = new fr.wseduc.webutils.collections.JsonArray(); for (String key : registeredNotifications.keySet()) { JsonObject notif = new JsonObject(registeredNotifications.get(key)).put("key", key); notif.remove("template"); for (Object admcDefaultObj : admcDefaults) { JsonObject admcDefault = (JsonObject) admcDefaultObj; if (admcDefault.getString("key", "").equals(key)) { notif.mergeIn(admcDefault); notif.remove("_id"); break; } } reply.add(notif); } renderJson(request, reply); } }); } @Get("/performDailyMailing") @SecuredAction(type = ActionType.RESOURCE, value = "") @ResourceFilter(SuperAdminFilter.class) public void performDailyMailing(final HttpServerRequest request) { mailerService.sendDailyMails(0, defaultResponseHandler(request)); } @Get("/performWeeklyMailing") @SecuredAction(type = ActionType.RESOURCE, value = "") @ResourceFilter(SuperAdminFilter.class) public void performWeeklyMailing(final HttpServerRequest request) { mailerService.sendWeeklyMails(0, defaultResponseHandler(request)); } @Get("/allowLanguages") @SecuredAction("timeline.allowLanguages") public void allowLanguages(final HttpServerRequest request) { // This route is used to create allowLanguages Workflow right, nothing to do return; } @Delete("/:id") @SecuredAction("timeline.delete.own.notification") public void deleteNotification(final HttpServerRequest request) { final String id = request.params().get("id"); UserUtils.getUserInfos(eb, request, new Handler<UserInfos>() { public void handle(final UserInfos user) { if (user == null) { unauthorized(request); return; } store.delete(id, user.getUserId(), defaultResponseHandler(request)); } }); } @Put("/:id") @SecuredAction("timeline.discard.notification") public void discardNotification(final HttpServerRequest request) { final String id = request.params().get("id"); UserUtils.getUserInfos(eb, request, new Handler<UserInfos>() { public void handle(final UserInfos user) { if (user == null) { unauthorized(request); return; } store.discard(id, user.getUserId(), defaultResponseHandler(request)); } }); } @Put("/:id/report") @SecuredAction("timeline.report.notification") public void reportNotification(final HttpServerRequest request) { final String id = request.params().get("id"); UserUtils.getUserInfos(eb, request, new Handler<UserInfos>() { public void handle(final UserInfos user) { if (user == null) { unauthorized(request); return; } store.report(id, user, new Handler<Either<String, JsonObject>>() { public void handle(Either<String, JsonObject> event) { defaultResponseHandler(request).handle(event); if (event.isLeft() || event.right().getValue().getInteger("number", 0) == 0) { return; } final List<String> structureIds = user.getStructures(); final JsonObject params = new JsonObject().put("username", user.getUsername()).put("uri", "/userbook/annuaire#" + user.getUserId() + "#" + user.getType()); final AtomicInteger countdown = new AtomicInteger(structureIds.size()); final Set<String> recipientsSet = new HashSet<>(); final Handler<Void> finalHandler = new Handler<Void>() { public void handle(Void v) { if (countdown.decrementAndGet() == 0) { ArrayList<String> recipients = new ArrayList<>(); recipients.addAll(recipientsSet); timelineHelper.notifyTimeline(request, "timeline.notify-report", null, recipients, id, params); } } }; for (final String structureId : structureIds) { JsonObject message = new JsonObject().put("action", "list-adml").put("structureId", structureId); eb.send("directory", message, result -> { if (result.succeeded()) { JsonArray users = (JsonArray) result.result().body(); for (Object userObj : users) { JsonObject user = (JsonObject) userObj; recipientsSet.add(user.getString("id")); } } else { log.error("Error list adml", result.cause()); } finalHandler.handle(null); }); } } }); } }); } final int PAGELIMIT = 25; @Get("/reported") @SecuredAction(type = ActionType.RESOURCE, value = "") @ResourceFilter(AdmlOfStructures.class) public void listReportedNotifications(final HttpServerRequest request) { final String structure = request.params().get("structure"); final boolean pending = Boolean.parseBoolean(request.params().get("pending")); int page = 0; if (request.params().contains("page")) { try { page = Integer.parseInt(request.params().get("page")); } catch (NumberFormatException e) { //silent } } store.listReported(structure, pending, PAGELIMIT * page, PAGELIMIT, new Handler<Either<String, JsonArray>>() { public void handle(Either<String, JsonArray> event) { if (event.isLeft()) { renderError(request); return; } final JsonArray results = event.right().getValue(); final JsonArray compiledResults = new fr.wseduc.webutils.collections.JsonArray(); final AtomicInteger countdown = new AtomicInteger(results.size()); final Handler<Void> endHandler = new Handler<Void>() { public void handle(Void v) { if (countdown.decrementAndGet() <= 0) { renderJson(request, compiledResults); } } }; if (results.size() == 0) endHandler.handle(null); for (Object notifObj : results) { final JsonObject notif = (JsonObject) notifObj; if (!notif.getString("message", "").isEmpty()) { compiledResults.add(notif); endHandler.handle(null); continue; } String key = notif.getString("type", "").toLowerCase() + "." + notif.getString("event-type", "").toLowerCase(); String stringifiedRegisteredNotif = registeredNotifications.get(key); if (stringifiedRegisteredNotif == null) { log.error( "Failed to retrieve registered from the shared map notification with key : " + key); endHandler.handle(null); continue; } JsonObject registeredNotif = new JsonObject(stringifiedRegisteredNotif); StringReader reader = new StringReader(registeredNotif.getString("template", "")); processTemplate(request, notif.getJsonObject("params", new JsonObject()), key, reader, new Handler<Writer>() { public void handle(Writer writer) { notif.put("message", writer.toString()); compiledResults.add(notif); endHandler.handle(null); } }); } } }); } @Put("/:id/action/keep") @SecuredAction(type = ActionType.RESOURCE, value = "") @ResourceFilter(AdmlOfStructures.class) public void adminKeepAction(final HttpServerRequest request) { final String id = request.params().get("id"); final String structureId = request.params().get("structure"); UserUtils.getUserInfos(eb, request, new Handler<UserInfos>() { public void handle(UserInfos user) { if (user == null) { unauthorized(request); return; } store.performAdminAction(id, structureId, user, AdminAction.KEEP, new Handler<Either<String, JsonObject>>() { public void handle(Either<String, JsonObject> event) { if (event.isRight()) { store.deleteReportNotification(id, new Handler<Either<String, JsonObject>>() { public void handle(Either<String, JsonObject> event) { if (event.isLeft()) { log.error(event.left().getValue()); } } }); } defaultResponseHandler(request).handle(event); } }); } }); } @Put("/:id/action/delete") @SecuredAction(type = ActionType.RESOURCE, value = "") @ResourceFilter(AdmlOfStructures.class) public void adminDeleteAction(final HttpServerRequest request) { final String id = request.params().get("id"); final String structureId = request.params().get("structure"); UserUtils.getUserInfos(eb, request, new Handler<UserInfos>() { public void handle(UserInfos user) { if (user == null) { unauthorized(request); return; } store.performAdminAction(id, structureId, user, AdminAction.DELETE, new Handler<Either<String, JsonObject>>() { public void handle(Either<String, JsonObject> event) { if (event.isRight()) { store.deleteReportNotification(id, new Handler<Either<String, JsonObject>>() { public void handle(Either<String, JsonObject> event) { if (event.isLeft()) { log.error(event.left().getValue()); } } }); } defaultResponseHandler(request).handle(event); } }); } }); } @Get("/pushNotif/fcmTokens") @SecuredAction(value = "timeline.api", type = ActionType.AUTHENTICATED) public void getFcmTokens(final HttpServerRequest request) { UserUtils.getUserInfos(eb, request, new Handler<UserInfos>() { @Override public void handle(final UserInfos user) { if (user != null) { NotificationUtils.getFcmTokensByUser(user.getUserId(), new Handler<Either<String, JsonArray>>() { @Override public void handle(Either<String, JsonArray> result) { if (result.isRight()) { renderJson(request, result.right().getValue()); } else { JsonObject error = new JsonObject().put("error", result.left().getValue()); renderJson(request, error, 400); } } }); } else { unauthorized(request); } } }); } @Put("/pushNotif/fcmToken") @SecuredAction(value = "timeline.api", type = ActionType.AUTHENTICATED) public void putFcmToken(final HttpServerRequest request) { final String token = request.params().get("fcmToken"); if (token != null && !token.trim().isEmpty()) { UserUtils.getUserInfos(eb, request, new Handler<UserInfos>() { @Override public void handle(final UserInfos user) { if (user != null) { NotificationUtils.putFcmToken(user.getUserId(), token, new Handler<Either<String, JsonObject>>() { @Override public void handle(Either<String, JsonObject> result) { if (result.isRight()) { renderJson(request, result.right().getValue()); } else { JsonObject error = new JsonObject().put("error", result.left().getValue()); renderJson(request, error, 400); } } }); } else { unauthorized(request); } } }); } else { badRequest(request); } } @Delete("/pushNotif/fcmToken") @SecuredAction(value = "timeline.api", type = ActionType.AUTHENTICATED) public void deleteFcmToken(final HttpServerRequest request) { final String token = request.params().get("fcmToken"); if (token != null && !token.trim().isEmpty()) { UserUtils.getUserInfos(eb, request, new Handler<UserInfos>() { @Override public void handle(final UserInfos user) { if (user != null) { NotificationUtils.deleteFcmToken(user.getUserId(), token, new Handler<Either<String, JsonObject>>() { @Override public void handle(Either<String, JsonObject> result) { if (result.isRight()) { renderJson(request, result.right().getValue()); } else { JsonObject error = new JsonObject().put("error", result.left().getValue()); renderJson(request, error, 400); } } }); } else { unauthorized(request); } } }); } else { badRequest(request); } } @BusAddress("wse.timeline") public void busApi(final Message<JsonObject> message) { if (message == null) { return; } final JsonObject json = message.body(); if (json == null) { message.reply(new JsonObject().put("status", "error").put("message", "Invalid body.")); return; } final Handler<JsonObject> handler = new Handler<JsonObject>() { public void handle(JsonObject event) { message.reply(event); } }; String action = json.getString("action"); if (action == null) { log.warn("Invalid action."); message.reply(new JsonObject().put("status", "error").put("message", "Invalid action.")); return; } switch (action) { case "add": final String sender = json.getString("sender"); if (sender == null || sender.startsWith("no-reply") || json.getBoolean("disableAntiFlood", false) || antiFlood.add(sender)) { store.add(json, new Handler<JsonObject>() { public void handle(JsonObject result) { notificationHelper.sendImmediateNotifications( new JsonHttpServerRequest(json.getJsonObject("request")), json); handler.handle(result); } }); if (refreshTypesCache && eventTypes != null && !eventTypes.contains(json.getString("type"))) { eventTypes = null; } } else { message.reply(new JsonObject().put("status", "error").put("message", "flood")); } break; case "get": UserInfos u = new UserInfos(); u.setUserId(json.getString("recipient")); u.setExternalId(json.getString("externalId")); store.get(u, null, json.getInteger("offset", 0), json.getInteger("limit", 25), null, false, handler); break; case "delete": store.delete(json.getString("resource"), handler); break; case "deleteSubResource": store.deleteSubResource(json.getString("sub-resource"), handler); case "list-types": store.listTypes(new Handler<JsonArray>() { @Override public void handle(JsonArray types) { message.reply(new JsonObject().put("status", "ok").put("types", types)); } }); break; default: message.reply(new JsonObject().put("status", "error").put("message", "Invalid action.")); } } private void getExternalNotifications(final Handler<Either<String, JsonObject>> handler) { configService.list(new Handler<Either<String, JsonArray>>() { public void handle(Either<String, JsonArray> event) { if (event.isLeft()) { handler.handle(new Either.Left<String, JsonObject>(event.left().getValue())); return; } final JsonObject restricted = new JsonObject(); for (String key : registeredNotifications.keySet()) { JsonObject notif = new JsonObject(registeredNotifications.get(key)); String restriction = notif.getString("restriction", TimelineNotificationsLoader.Restrictions.NONE.name()); for (Object notifConfigObj : event.right().getValue()) { JsonObject notifConfig = (JsonObject) notifConfigObj; if (notifConfig.getString("key", "").equals(key)) { restriction = notifConfig.getString("restriction", restriction); break; } } if (restriction.equals(TimelineNotificationsLoader.Restrictions.EXTERNAL.name()) || restriction.equals(TimelineNotificationsLoader.Restrictions.HIDDEN.name())) { String notifType = notif.getString("type"); if (!restricted.containsKey(notifType)) { restricted.put(notifType, new fr.wseduc.webutils.collections.JsonArray()); } restricted.getJsonArray(notifType).add(notif.getString("event-type")); } } handler.handle(new Either.Right<String, JsonObject>(restricted)); } }); } public void setConfigService(TimelineConfigService configService) { this.configService = configService; } public void setMailerService(TimelineMailerService mailerService) { this.mailerService = mailerService; } public void setRegisteredNotifications(Map<String, String> registeredNotifications) { this.registeredNotifications = registeredNotifications; } public void setPushNotifService(TimelinePushNotifService pushNotifService) { this.pushNotifService = pushNotifService; } public void setNotificationHelper(NotificationHelper notificationHelper) { this.notificationHelper = notificationHelper; } public void setEventsI18n(LocalMap<String, String> eventsI18n) { this.eventsI18n = eventsI18n; } public void setLazyEventsI18n(HashMap<String, JsonObject> lazyEventsI18n) { this.lazyEventsI18n = lazyEventsI18n; } }