/**
* 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 com.julienvey.trello.Trello;
import com.julienvey.trello.domain.*;
import com.julienvey.trello.domain.Board;
import com.julienvey.trello.domain.Card;
import com.julienvey.trello.domain.Label;
import com.julienvey.trello.impl.TrelloImpl;
import io.lavagna.model.*;
import io.lavagna.web.api.model.TrelloImportRequest;
import io.lavagna.web.api.model.TrelloImportRequest.BoardIdAndShortName;
import io.lavagna.web.api.model.TrelloRequest;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import static io.lavagna.common.Constants.SYSTEM_LABEL_ASSIGNED;
import static io.lavagna.common.Constants.SYSTEM_LABEL_DUE_DATE;
@Service
public class ImportService {
private static final Logger LOG = LogManager.getLogger();
private final EventEmitter eventEmitter;
private final ProjectService projectService;
private final BoardRepository boardRepository;
private final BoardColumnRepository boardColumnRepository;
private final CardDataService cardDataService;
private final CardService cardService;
private final LabelService labelService;
private final CardLabelRepository cardLabelRepository;
private final UserRepository userRepository;
public ImportService(EventEmitter eventEmitter, ProjectService projectService, BoardRepository boardRepository,
BoardColumnRepository boardColumnRepository, CardDataService cardDataService, CardService cardService,
LabelService labelService, CardLabelRepository cardLabelRepository, UserRepository userRepository) {
this.eventEmitter = eventEmitter;
this.projectService = projectService;
this.boardRepository = boardRepository;
this.boardColumnRepository = boardColumnRepository;
this.cardDataService = cardDataService;
this.cardService = cardService;
this.labelService = labelService;
this.cardLabelRepository = cardLabelRepository;
this.userRepository = userRepository;
}
public TrelloBoardsResponse getAvailableTrelloBoards(TrelloRequest request) {
TrelloBoardsResponse response = new TrelloBoardsResponse();
Trello trello = new TrelloImpl(request.getApiKey(), request.getSecret());
Member member = trello.getMemberInformation("me");
Map<String, TrelloOrganizationInfo> organizations = new HashMap<>();
for (String boardId : member.getIdBoards()) {
Board board = trello.getBoard(boardId);
if (board.isClosed()) {
continue;
}
String orgId = board.getIdOrganization();
if (!organizations.containsKey(orgId)) {
TrelloOrganizationInfo tOrg;
if (orgId != null) {
Organization organization = board.fetchOrganization();
tOrg = new TrelloOrganizationInfo(orgId, organization.getDisplayName());
} else {
tOrg = new TrelloOrganizationInfo(orgId, "Personal");
}
organizations.put(orgId, tOrg);
response.organizations.add(tOrg);
}
TrelloOrganizationInfo tOrg = organizations.get(orgId);
tOrg.boards.add(new TrelloBoardInfo(board.getId(), board.getName()));
}
return response;
}
public TrelloImportResponse importFromTrello(TrelloImportRequest importRequest, User user) {
Trello trello = new TrelloImpl(importRequest.getApiKey(), importRequest.getSecret());
// Cache the user mappings
Map<String, User> lavagnaUsers = new HashMap<>();
TrelloImportResponse tImport = new TrelloImportResponse();
int boardsToImport = importRequest.getBoards().size() + 1;
int currentBoard = 0;
for (BoardIdAndShortName boardIdAndShortName : importRequest.getBoards()) {
currentBoard++;
Board board = trello.getBoard(boardIdAndShortName.getId());
String boardShortName = boardIdAndShortName.getShortName().toUpperCase(Locale.ENGLISH);
if (board.isClosed() || boardRepository.existsWithShortName(boardShortName)) {
continue;
}
eventEmitter.emitImportProject(importRequest.getImportId(), currentBoard, boardsToImport, board.getName(), user);
TrelloBoard tBoard = new TrelloBoard(boardShortName, board.getName(), board.getDesc());
importBoard(trello, tImport, board, tBoard, lavagnaUsers, importRequest.isImportArchived());
tImport.boards.add(tBoard);
}
eventEmitter.emitImportProject(importRequest.getImportId(), boardsToImport, boardsToImport, "", user);
return tImport;
}
private void importBoard(Trello trello, TrelloImportResponse tImport, Board board, TrelloBoard tBoard,
Map<String, User> lavagnaUsers, boolean importArchived) {
// Cache the checklists
Map<String, CheckList> checklists = new HashMap<>();
for (CheckList checklist : trello.getBoardChecklists(board.getId())) {
checklists.put(checklist.getId(), checklist);
}
// Cache the board members
Map<String, Member> boardMembers = new HashMap<>();
for (Member member : trello.getBoardMembers(board.getId())) {
boardMembers.put(member.getId(), member);
}
// Fetch the columns with every card in it
for (TList list : board.fetchLists(new Argument("cards", importArchived ? "all" : "open"))) {
TrelloBoardColumn tColumn = new TrelloBoardColumn(list.getName());
tBoard.columns.put(list.getPos(), tColumn);
for (Card iCard : list.getCards()) {
if (StringUtils.isEmpty(iCard.getName())) {
continue;
}
TrelloCard tCard = new TrelloCard(iCard.getName(), iCard.getDesc(), iCard.isClosed(), iCard.getDue());
// Checklists
for (String checklistId : iCard.getIdChecklists()) {
CheckList checklist = checklists.get(checklistId);
TrelloChecklist tChecklist = new TrelloChecklist(checklist.getName());
for (CheckItem iItem : checklist.getCheckItems()) {
TrelloChecklistItem item = new TrelloChecklistItem(iItem.getName(),
iItem.getState().equals("complete"));
tChecklist.items.put(iItem.getPos(), item);
}
tCard.checklists.add(tChecklist);
}
// Comments
for (Action action : iCard.getActions(new Argument("filter", "commentCard"))) {
tCard.comments.add(new TrelloComment(action.getDate(),
getMemberFromId(lavagnaUsers, boardMembers, action.getIdMemberCreator()),
action.getData().getText()));
}
// Assigned users
for (String memberId : iCard.getIdMembers()) {
User user = getMemberFromId(lavagnaUsers, boardMembers, memberId);
if (user != null) {
tCard.assignedUsers.add(user);
}
}
// Labels
for (Label label : iCard.getLabels()) {
addLabelToCard(tCard, tImport, label);
}
tColumn.cards.put(iCard.getPos(), tCard);
}
}
}
private void addLabelToCard(TrelloCard card, TrelloImportResponse tImport, Label label) {
String name = label.getName();
if (StringUtils.isBlank(name)) {
name = label.getColor();
}
if (!tImport.labels.containsKey(name)) {
tImport.labels.put(name, label.getColor());
}
card.labels.add(name);
}
private User getMemberFromId(Map<String, User> lavagnaUsers, Map<String, Member> boardMembers, String memberId) {
if (lavagnaUsers.containsKey(memberId)) {
return lavagnaUsers.get(memberId);
}
Member member = boardMembers.get(memberId);
User user = matchUser(lavagnaUsers, memberId, member.getUsername());
if (user != null)
return user;
user = matchUser(lavagnaUsers, memberId, member.getEmail());
if (user != null)
return user;
return matchUser(lavagnaUsers, memberId, member.getFullName());
}
private User matchUser(Map<String, User> lavagnaUsers, String memberId, String criteria) {
if (StringUtils.isNotBlank(memberId)) {
List<User> users = userRepository.findUsers(criteria);
if (users.size() > 0) {
lavagnaUsers.put(memberId, users.get(0));
return users.get(0);
}
}
return null;
}
@Transactional(readOnly = false)
public void saveTrelloBoardsToDb(String projectShortName, TrelloImportResponse tImport, User user) {
int projectId = projectService.findIdByShortName(projectShortName);
List<BoardColumnDefinition> definitions = projectService.findColumnDefinitionsByProjectId(projectId);
BoardColumnDefinition openDefinition = null;
for (BoardColumnDefinition def : definitions) {
if (openDefinition == null || openDefinition.getValue() != ColumnDefinition.OPEN) {
openDefinition = def;
}
}
if (openDefinition == null) {
LOG.warn("no valid column definition has been found for the project {}.", projectShortName);
return;
}
// Labels
Map<String, CardLabel> lavagnaLabels = new HashMap<>();
for (CardLabel cl : cardLabelRepository.findLabelsByProject(projectId)) {
if (cl.getDomain().equals(CardLabel.LabelDomain.USER)) {
lavagnaLabels.put(cl.getName(), cl);
}
}
for (String labelName : tImport.labels.keySet()) {
if (!lavagnaLabels.containsKey(labelName)) {
CardLabel cl = cardLabelRepository.addLabel(projectId, true, CardLabel.LabelType.NULL,
CardLabel.LabelDomain.USER, labelName, getColorFromTrelloColor(tImport.labels.get(labelName)));
lavagnaLabels.put(labelName, cl);
}
}
CardLabel dueDateLabel = cardLabelRepository.findLabelByName(projectId, SYSTEM_LABEL_DUE_DATE,
CardLabel.LabelDomain.SYSTEM);
CardLabel assignedLabel = cardLabelRepository.findLabelByName(projectId, SYSTEM_LABEL_ASSIGNED,
CardLabel.LabelDomain.SYSTEM);
// Import the boards
for (TrelloBoard tBoard : tImport.boards) {
int boardId = boardRepository.createNewBoard(tBoard.getName(), tBoard.getShortName(), tBoard.getDesc(), projectId).getId();
for (Integer trelloColumnPos : tBoard.columns.keySet()) {
TrelloBoardColumn trelloColumn = tBoard.columns.get(trelloColumnPos);
int boardColumnId = boardColumnRepository.addColumnToBoard(trelloColumn.name, openDefinition.getId(),
BoardColumn.BoardColumnLocation.BOARD, boardId).getId();
for (Integer pos : trelloColumn.cards.keySet()) {
TrelloCard card = trelloColumn.cards.get(pos);
importCard(card, boardId, boardColumnId, user, lavagnaLabels, dueDateLabel, assignedLabel);
}
}
}
}
private int getColorFromTrelloColor(String color) {
switch (color) {
case "yellow":
return 16763904;
case "red":
return 15012864;
case "orange":
return 16733986;
case "green":
return 2464548;
case "pink":
return 15102392;
case "purple":
return 10233776;
case "sky":
return 48340;
case "lime":
return 13491257;
case "black":
return 0;
default: //case "blue":
return 1810914;
}
}
private void importCard(TrelloCard card, int boardId, int boardColumnId, User user,
Map<String, CardLabel> lavagnaLabels, CardLabel dueDateLabel, CardLabel assignedLabel) {
int cardId = cardService.createCard(card.getName(), boardColumnId, new Date(), user).getId();
if (StringUtils.isNotEmpty(card.getDesc())) {
cardDataService.updateDescription(cardId, card.getDesc(), new Date(), user.getId());
}
// Due date
if (card.getDueDate() != null) {
labelService.addLabelValueToCard(dueDateLabel, cardId, new CardLabelValue.LabelValue(card.getDueDate()),
user, new Date());
}
// Checklists
for (TrelloChecklist checklist : card.checklists) {
CardData data = cardDataService.createActionList(cardId, checklist.getName(), user.getId(), new Date());
for (Integer pos : checklist.items.keySet()) {
TrelloChecklistItem item = checklist.items.get(pos);
CardData itemData = cardDataService.createActionItem(cardId, data.getId(), item.name, user.getId(), new Date());
if (item.isChecked) {
cardDataService.toggleActionItem(itemData.getId(), true, user.getId(), new Date());
}
}
}
// Comments
for (TrelloComment comment : card.comments) {
cardDataService.createComment(cardId, comment.getText(), comment.getDate(),
(comment.getUser() != null ? comment.getUser() : user).getId());
}
// Archive if closed
if (card.isClosed()) {
BoardColumn destination = boardColumnRepository.findDefaultColumnFor(boardId,
BoardColumn.BoardColumnLocation.ARCHIVE);
List<Integer> idToList = new ArrayList<>();
idToList.add(cardId);
cardService.moveCardsToColumn(idToList, boardColumnId, destination.getId(), user.getId(),
Event.EventType.CARD_ARCHIVE, new Date());
}
// Assigned users
for (User assignedUser : card.assignedUsers) {
labelService.addLabelValueToCard(assignedLabel, cardId, new CardLabelValue.LabelValue(assignedUser.getId()),
user, new Date());
}
// Labels
for (String labelName : card.labels) {
labelService.addLabelValueToCard(lavagnaLabels.get(labelName), cardId, new CardLabelValue.LabelValue(),
user, new Date());
}
}
static class TrelloChecklistItem {
private final String name;
private final boolean isChecked;
@java.beans.ConstructorProperties({ "name", "isChecked" }) public TrelloChecklistItem(String name,
boolean isChecked) {
this.name = name;
this.isChecked = isChecked;
}
}
static class TrelloChecklist {
private final String name;
private final Map<Integer, TrelloChecklistItem> items = new TreeMap<>();
public TrelloChecklist(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public Map<Integer, TrelloChecklistItem> getItems() {
return this.items;
}
}
static class TrelloComment {
private final Date date;
private final User user;
private final String text;
@java.beans.ConstructorProperties({ "date", "user", "text" }) public TrelloComment(Date date, User user,
String text) {
this.date = date;
this.user = user;
this.text = text;
}
public Date getDate() {
return this.date;
}
public User getUser() {
return this.user;
}
public String getText() {
return this.text;
}
}
static class TrelloCard {
private final String name;
private final String desc;
private final boolean isClosed;
private final Date dueDate;
private final List<TrelloComment> comments = new ArrayList<>();
private final List<TrelloChecklist> checklists = new ArrayList<>();
private final List<User> assignedUsers = new ArrayList<>();
private final List<String> labels = new ArrayList<>();
public TrelloCard(String name, String desc, boolean isClosed, Date dueDate) {
this.name = name;
this.desc = desc;
this.isClosed = isClosed;
this.dueDate = dueDate;
}
public String getName() {
return this.name;
}
public String getDesc() {
return this.desc;
}
public boolean isClosed() {
return this.isClosed;
}
public Date getDueDate() {
return this.dueDate;
}
public List<TrelloComment> getComments() {
return this.comments;
}
public List<TrelloChecklist> getChecklists() {
return this.checklists;
}
public List<User> getAssignedUsers() {
return this.assignedUsers;
}
public List<String> getLabels() {
return this.labels;
}
}
static class TrelloBoardColumn {
private final String name;
private final Map<Integer, TrelloCard> cards = new TreeMap<>();
public TrelloBoardColumn(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public Map<Integer, TrelloCard> getCards() {
return this.cards;
}
}
public static class TrelloBoard {
private final String shortName;
private final String name;
private final String desc;
private final Map<Integer, TrelloBoardColumn> columns = new TreeMap<>();
public TrelloBoard(String shortName, String name, String desc) {
this.shortName = shortName;
this.name = name;
this.desc = desc;
}
public String getShortName() {
return this.shortName;
}
public String getName() {
return this.name;
}
public String getDesc() {
return this.desc;
}
public Map<Integer, TrelloBoardColumn> getColumns() {
return this.columns;
}
}
public static class TrelloImportResponse {
private final List<TrelloBoard> boards = new ArrayList<>();
private final Map<String, String> labels = new HashMap<>();
public List<TrelloBoard> getBoards() {
return this.boards;
}
public Map<String, String> getLabels() {
return this.labels;
}
}
public static class TrelloBoardsResponse {
private final List<TrelloOrganizationInfo> organizations = new ArrayList<>();
}
public static class TrelloBoardInfo {
private final String id;
private final String name;
@java.beans.ConstructorProperties({ "id", "name" }) public TrelloBoardInfo(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return this.id;
}
public String getName() {
return this.name;
}
}
public static class TrelloOrganizationInfo {
private final String id;
private final String name;
private final List<TrelloBoardInfo> boards = new ArrayList<>();
public TrelloOrganizationInfo(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return this.id;
}
public String getName() {
return this.name;
}
public List<TrelloBoardInfo> getBoards() {
return this.boards;
}
}
}