/** * This file is part of lavagna. * * lavagna is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * lavagna is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with lavagna. If not, see <http://www.gnu.org/licenses/>. */ package io.lavagna.service; import io.lavagna.common.Json; import io.lavagna.model.*; import io.lavagna.model.CardLabelValue.LabelValue; import io.lavagna.model.apihook.Column; import io.lavagna.model.apihook.From; import io.lavagna.model.apihook.Label; import io.lavagna.query.ApiHookQuery; import io.lavagna.service.EventEmitter.LavagnaEvent; import org.apache.commons.lang3.tuple.Triple; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; import javax.script.*; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @Service public class ApiHooksService { private static final Logger LOG = LogManager.getLogger(); private final Compilable engine; private final Executor executor; private final Map<String, Triple<ApiHook, Map<String, String>, CompiledScript>> compiledScriptCache = new ConcurrentHashMap<>(); private final ProjectService projectService; private final CardService cardService; private final ApiHookQuery apiHookQuery; private final LabelService labelService; private final UserService userService; private final ConfigurationRepository configurationRepository; public ApiHooksService(ProjectService projectService, CardService cardService, ApiHookQuery apiHookQuery, LabelService labelService, UserService userService, ConfigurationRepository configurationRepository) { this.projectService = projectService; this.cardService = cardService; this.apiHookQuery = apiHookQuery; this.labelService = labelService; this.userService = userService; this.configurationRepository = configurationRepository; engine = (Compilable) new ScriptEngineManager().getEngineByName("javascript"); executor = Executors.newFixedThreadPool(4); } private static void executeScript(String name, CompiledScript script, Map<String, Object> scope) { try { ScriptContext newContext = new SimpleScriptContext(); Bindings engineScope = newContext.getBindings(ScriptContext.ENGINE_SCOPE); engineScope.putAll(scope); engineScope.put("log", LOG); engineScope.put("GSON", Json.GSON); engineScope.put("restTemplate", new RestTemplate()); script.eval(newContext); } catch (ScriptException ex) { LOG.warn("Error while executing script " + name, ex); } } public ApiHook findByName(String name) { return apiHookQuery.findByNames(Collections.singletonList(name)).get(0); } private static class EventToRun implements Runnable { private final ApiHooksService apiHooksService; private final LavagnaEvent eventName; private final String projectName; private final Map<String, Object> env; private final io.lavagna.model.apihook.User user; EventToRun(ApiHooksService apiHooksService, LavagnaEvent eventName, String projectName, User user, Map<String, Object> env) { this.apiHooksService = apiHooksService; this.eventName = eventName; this.projectName = projectName; this.env = env; this.user = From.from(user); } @Override public void run() { List<ApiHookNameAndVersion> nameAndVersions = apiHooksService.apiHookQuery.findAllEnabled(ApiHook.Type.EVENT_EMITTER_HOOK); List<String> names = new ArrayList<>(nameAndVersions.size()); for (ApiHookNameAndVersion nv : nameAndVersions) { names.add(nv.getName()); } //remove all disabled scripts apiHooksService.compiledScriptCache.keySet().retainAll(names); List<String> toAddOrUpdate = new ArrayList<>(0); for (ApiHookNameAndVersion hook : nameAndVersions) { if (!apiHooksService.compiledScriptCache.containsKey(hook.getName()) || apiHooksService.compiledScriptCache.get(hook.getName()).getLeft().getVersion() < hook.getVersion()) { toAddOrUpdate.add(hook.getName()); } } if (!toAddOrUpdate.isEmpty()) { for (ApiHook apiHook : apiHooksService.apiHookQuery.findByNames(toAddOrUpdate)) { try { CompiledScript cs = apiHooksService.engine.compile(apiHook.getScript()); Map<String, String> configuration = apiHook.getConfiguration() != null ? apiHook.getConfiguration() : Collections.<String, String>emptyMap(); apiHooksService.compiledScriptCache.put(apiHook.getName(), Triple.of(apiHook, configuration, cs)); } catch (ScriptException ex) { LOG.warn("Error while compiling script " + apiHook.getName(), ex); } } } for (Triple<ApiHook, Map<String, String>, CompiledScript> val : apiHooksService.compiledScriptCache.values()) { List<String> projectsFilter = val.getLeft().getProjects(); if(projectsFilter != null && !projectsFilter.contains(projectName)) { continue; } Map<String, Object> scope = new HashMap<>(env); scope.put("eventName", eventName.name()); scope.put("project", projectName); scope.put("user", user); scope.put("data", env); scope.put("configuration", val.getMiddle()); executeScript(val.getLeft().getName(), val.getRight(), scope); } } } @Transactional(readOnly = true) public List<ApiHook> findAllPlugins() { return apiHookQuery.findAll(); } @Transactional public void deleteHook(String name) { apiHookQuery.delete(name); } @Transactional public void enable(String name, boolean enabled) { apiHookQuery.enable(name, enabled); } @Transactional public void createApiHook(String name, String code, Map<String, String> properties, List<String> projects, Map<String, Object> metadata) { String propAsJson = properties == null ? null : Json.GSON.toJson(properties, Map.class); String projectsAsJson = projects == null ? null : Json.GSON.toJson(projects, List.class); String metadataAsJson = metadata == null ? null : Json.GSON.toJson(metadata, Map.class); apiHookQuery.insert(name, code, propAsJson, true, ApiHook.Type.EVENT_EMITTER_HOOK, projectsAsJson, metadataAsJson); } @Transactional public void updateApiHook(String name, String code, Map<String, String> properties, List<String> projects) { String propAsJson = properties == null ? null : Json.GSON.toJson(properties, Map.class); String projectsAsJson = projects == null ? null : Json.GSON.toJson(projects, List.class); apiHookQuery.update(name, code, propAsJson, apiHookQuery.findStatusByName(name), ApiHook.Type.EVENT_EMITTER_HOOK, projectsAsJson); } private Map<String, Object> getBaseDataFor(int cardId) { CardFull cf = cardService.findFullBy(cardId); String baseUrl = configurationRepository.getValue(Key.BASE_APPLICATION_URL); return getBaseDataFor(cf, baseUrl); } private static Map<String, Object> getBaseDataFor(CardFull cf, String baseUrl) { Map<String, Object> res = new HashMap<>(); res.put("card", From.from(cf, baseUrl)); res.put("board", cf.getBoardShortName()); return res; } private Map<String, Object> updateForObj(int cardId, Object previous, Object updated) { Map<String, Object> payload = new HashMap<>(); payload.put("previous", previous); payload.put("updated", updated); payload.putAll(getBaseDataFor(cardId)); return payload; } private Map<String, Object> updateFor(int cardId, String previous, String updated) { return updateForObj(cardId, previous, updated); } private Map<String, Object> updateFor(int cardId, CardData previous, String updated) { return updateForObj(cardId, From.from(previous), updated); } private Map<String, Object> updateFor(int cardId, CardType type, CardDataHistory previous, CardDataHistory updated) { return updateForObj(cardId, From.from(type, previous), From.from(type, updated)); } private Map<String, Object> payloadForObj(int cardId, String name, Object object) { Map<String, Object> r = getBaseDataFor(cardId); r.put(name, object); return r; } private Map<String, Object> payloadFor(int cardId, String name, CardData cardData) { return payloadForObj(cardId, name, From.from(cardData)); } private Map<String, Object> payloadFor(int cardId, String name, String value) { return payloadForObj(cardId, name, value); } private Map<String, Object> payloadFor(int cardId, String name, Collection<?> value) { return payloadForObj(cardId, name, value); } public void createdProject(String projectShortName, User user, LavagnaEvent event) { executor.execute(new EventToRun(this, event, projectShortName, user, Collections.<String, Object>emptyMap())); } public void updatedProject(String projectShortName, User user, LavagnaEvent event) { executor.execute(new EventToRun(this, event, projectShortName, user, Collections.<String, Object>emptyMap())); } public void createdBoard(String boardShortName, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByBoardShortname(boardShortName); executor.execute(new EventToRun(this, event, projectShortName, user, Collections.<String, Object>singletonMap("board", boardShortName))); } public void updatedBoard(String boardShortName, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByBoardShortname(boardShortName); executor.execute(new EventToRun(this, event, projectShortName, user, Collections.<String, Object>singletonMap("board", boardShortName))); } public void createdColumn(String boardShortName, String columnName, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByBoardShortname(boardShortName); Map<String, Object> payload = new HashMap<>(); payload.put("board", boardShortName); payload.put("columnName", columnName); executor.execute(new EventToRun(this, event, projectShortName, user, payload)); } public void updateColumn(String boardShortName, BoardColumn oldColumn, BoardColumn updatedColumn, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByBoardShortname(boardShortName); Map<String, Object> payload = new HashMap<>(); payload.put("board", boardShortName); payload.put("previous", From.from(oldColumn)); payload.put("updated", From.from(updatedColumn)); executor.execute(new EventToRun(this, event, projectShortName, user, payload)); } public void createdCard(String boardShortName, Card card, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByBoardShortname(boardShortName); executor.execute(new EventToRun(this, event, projectShortName, user, getBaseDataFor(card.getId()))); } public void updatedCardName(String boardShortName, Card beforeUpdate, Card newCard, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByBoardShortname(boardShortName); executor.execute(new EventToRun(this, event, projectShortName, user, updateForObj(beforeUpdate.getId(), beforeUpdate.getName(), newCard.getName()))); } public void updateCardDescription(int cardId, CardDataHistory previousDescription, CardDataHistory newDescription, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByCardId(cardId); executor.execute(new EventToRun(this, event, projectShortName, user, updateFor(cardId, CardType.DESCRIPTION, previousDescription, newDescription))); } public void createdComment(int cardId, CardData comment, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByCardId(cardId); executor.execute(new EventToRun(this, event, projectShortName, user, payloadFor(cardId, "comment", comment))); } public void updatedComment(int cardId, CardData previousComment, String newComment, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByCardId(cardId); executor.execute(new EventToRun(this, event, projectShortName, user, updateFor(cardId, previousComment, newComment))); } public void deletedComment(int cardId, CardData deletedComment, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByCardId(cardId); executor.execute(new EventToRun(this, event, projectShortName, user, payloadFor(cardId, "comment", deletedComment))); } public void undeletedComment(int cardId, CardData undeletedComment, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByCardId(cardId); executor.execute(new EventToRun(this, event, projectShortName, user, payloadFor(cardId, "comment", undeletedComment))); } public void uploadedFile(int cardId, List<String> fileNames, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByCardId(cardId); Map<String, Object> payload = payloadFor(cardId, "files", fileNames); executor.execute(new EventToRun(this, event, projectShortName, user, payload)); } public void deletedFile(int cardId, String fileName, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByCardId(cardId); Map<String, Object> payload = payloadFor(cardId, "file", fileName); executor.execute(new EventToRun(this, event, projectShortName, user, payload)); } public void undoDeletedFile(int cardId, String fileName, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByCardId(cardId); Map<String, Object> payload = payloadFor(cardId, "file", fileName); executor.execute(new EventToRun(this, event, projectShortName, user, payload)); } public void removedLabelValueToCards(List<CardFull> affectedCards, int labelId, LabelValue labelValue, User user, LavagnaEvent event) { handleLabelValue(affectedCards, labelId, labelValue, user, event); } public void addLabelValueToCards(List<CardFull> affectedCards, int labelId, LabelValue labelValue, User user, LavagnaEvent event) { handleLabelValue(affectedCards, labelId, labelValue, user, event); } public void updateLabelValueToCards(List<CardFull> updated, int labelId, LabelValue labelValue, User user, LavagnaEvent event) { handleLabelValue(updated, labelId, labelValue, user, event); } private void handleLabelValue(List<CardFull> affectedCards, int labelId, LabelValue labelValue, User user, LavagnaEvent event) { if (affectedCards.isEmpty()) { return; } String projectShortName = projectService.findRelatedProjectShortNameByLabelId(labelId); Map<String, Object> payload = new HashMap<>(); Label label = from(labelService.findLabelById(labelId), labelValue); payload.put("affectedCards", toList(affectedCards)); payload.put("label", label); executor.execute(new EventToRun(this, event, projectShortName, user, payload)); } private Label from(CardLabel cardLabel, LabelValue labelValue) { Object value = null; switch (cardLabel.getType()) { case CARD: value = From.from(cardService.findFullBy(labelValue.getValueCard()), baseUrl()); break; case INT: value = labelValue.getValueInt(); break; case LIST: value = labelService.findLabelListValueById(labelValue.getValueList()).getValue(); break; case STRING: value = labelValue.getValueString(); break; case TIMESTAMP: value = Json.formatDate(labelValue.getValueTimestamp()); break; case USER: value = From.from(userService.findUserWithPermission(labelValue.getValueUser())); break; } return new Label(cardLabel.getType().toString(), cardLabel.getDomain().toString(), cardLabel.getName(), value); } private String baseUrl() { return configurationRepository.getValue(Key.BASE_APPLICATION_URL); } private List<io.lavagna.model.apihook.Card> toList(List<CardFull> cards) { List<io.lavagna.model.apihook.Card> res = new ArrayList<>(cards.size()); String baseUrl = baseUrl(); for(CardFull cf : cards) { res.add(From.from(cf, baseUrl)); } return res; } private void handleActionList(int cardId, String name, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByCardId(cardId); Map<String, Object> payload = payloadFor(cardId, "actionList", name); executor.execute(new EventToRun(this, event, projectShortName, user, payload)); } public void createActionList(int cardId, String name, User user, LavagnaEvent event) { handleActionList(cardId, name, user, event); } public void deleteActionList(int cardId, String name, User user, LavagnaEvent event) { handleActionList(cardId, name, user, event); } public void updatedNameActionList(int cardId, String oldName, String newName, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByCardId(cardId); executor.execute(new EventToRun(this, event, projectShortName, user, updateFor(cardId, oldName, newName))); } public void undeletedActionList(int cardId, String name, User user, LavagnaEvent event) { handleActionList(cardId, name, user, event); } public void createActionItem(int cardId, String actionItemListName, String actionItem, User user, LavagnaEvent event) { handleActionItem(cardId, actionItemListName, actionItem, user, event); } public void deletedActionItem(int cardId, String actionItemListName, String actionItem, User user, LavagnaEvent event) { handleActionItem(cardId, actionItemListName, actionItem, user, event); } public void toggledActionItem(int cardId, String actionItemListName, String actionItem, boolean toggle, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByCardId(cardId); Map<String, Object> payload = payloadFor(cardId, "actionList", actionItemListName); payload.put("actionItem", actionItem); payload.put("toggled", toggle); executor.execute(new EventToRun(this, event, projectShortName, user, payload)); } public void updatedActionItem(int cardId, String actionItemListName, String oldActionItem, String newActionItem, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByCardId(cardId); Map<String, Object> payload = updateFor(cardId, oldActionItem, newActionItem); payload.put("actionList", actionItemListName); executor.execute(new EventToRun(this, event, projectShortName, user, payload)); } public void undoDeleteActionItem(int cardId, String actionItemListName, String actionItem, User user, LavagnaEvent event) { handleActionItem(cardId, actionItemListName, actionItem, user, event); } private void handleActionItem(int cardId, String actionItemListName, String actionItem, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByCardId(cardId); Map<String, Object> payload = payloadFor(cardId, "actionList", actionItemListName); payload.put("actionItem", actionItem); executor.execute(new EventToRun(this, event, projectShortName, user, payload)); } public void movedActionItem(int cardId, String fromActionItemListName, String toActionItemListName, String actionItem, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByCardId(cardId); Map<String, Object> payload = payloadFor(cardId, "actionItem", actionItem); payload.put("from", fromActionItemListName); payload.put("to", toActionItemListName); executor.execute(new EventToRun(this, event, projectShortName, user, payload)); } public void moveCards(BoardColumn from, BoardColumn to, Collection<Integer> cardIds, User user, LavagnaEvent event) { String projectShortName = projectService.findRelatedProjectShortNameByCardId(cardIds.iterator().next()); Map<String, Object> payload = new HashMap<>(); payload.put("affectedCards", toList(cardService.findFullBy(cardIds))); payload.put("from", From.from(from)); payload.put("to", From.from(to)); executor.execute(new EventToRun(this, event, projectShortName, user, payload)); } }