/**
* 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.model.BoardColumn.BoardColumnLocation;
import io.lavagna.model.Card;
import io.lavagna.model.CardFull;
import io.lavagna.model.Event;
import io.lavagna.model.User;
import io.lavagna.query.CardQuery;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.jdbc.core.RowCallbackHandler;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import static org.apache.commons.lang3.StringUtils.trimToNull;
@Repository
@Transactional(readOnly = true)
public class CardRepository {
private static final Logger LOG = LogManager.getLogger();
private final NamedParameterJdbcTemplate jdbc;
private final CardQuery queries;
public CardRepository(NamedParameterJdbcTemplate jdbc, CardQuery queries) {
this.jdbc = jdbc;
this.queries = queries;
}
// prepare a {:cardOrder, :cardId, :columnId} list
private static List<SqlParameterSource> prepareOrderParameter(List<Integer> cardIds, int columnId) {
List<SqlParameterSource> params = new ArrayList<>(cardIds.size());
for (int i = 0; i < cardIds.size(); i++) {
SqlParameterSource p = new MapSqlParameterSource("cardOrder", i + 1)//
.addValue("cardId", cardIds.get(i))//
.addValue("columnId", columnId);
params.add(p);
}
return params;
}
public Integer findColumnIdById(int cardId) {
return queries.findColumnIdById(cardId);
}
public List<CardFull> findAllByBoardShortName(String boardShortName) {
return queries.findAllByBoardShortName(boardShortName);
}
public List<Card> findAllByBoardIdAndLocation(int boardId, BoardColumnLocation location) {
return queries.findAllByBoardIdAndLocation(boardId, location.toString());
}
public List<CardFull> findAllByColumnId(int columnId) {
return queries.findAllFullByColumnId(columnId);
}
public List<CardFull> findAllByIds(Collection<Integer> ids) {
return ids.isEmpty() ? Collections.<CardFull> emptyList() : queries.findAllByIds(ids);
}
public List<Card> findCards(int boardId, String criteria) {
return queries.findCards(boardId, criteria);
}
public List<Event> fetchAllActivityByCardId(int cardId) {
return queries.fetchAllActivityByCardId(cardId);
}
/**
* 10 element per page. Return 11 elements for signaling if there are more pages
*
* @param boardId
* @param location
* @param page
* @return
*/
public List<Integer> fetchPaginatedByBoardIdAndLocation(int boardId, BoardColumnLocation location, int page) {
return queries.fetchPaginatedByBoardIdAndLocation(boardId, location.toString(), 11, page * 10);
}
public Card findBy(int cardId) {
return queries.findBy(cardId);
}
public CardFull findFullBy(int cardId) {
return queries.findFullBy(cardId);
}
public List<CardFull> findFullBy(Collection<Integer> ids) {
return queries.findAllByIds(ids);
}
public CardFull findFullBy(String boardShortName, int seqNumber) {
return queries.findFullBy(boardShortName, seqNumber);
}
public Integer findCardIdByBoardNameAndSeq(String boardShortName, int seqNumber) {
return queries.findCardIdByBoardNameAndSeq(boardShortName, seqNumber);
}
public boolean existCardWith(String boardShortName, int seqNumber) {
return Integer.valueOf(1).equals(queries.countCardIdByBoardNameAndSeq(boardShortName, seqNumber));
}
public Card updateCard(int cardId, String name) {
queries.updateCard(trimToNull(name), cardId);
return findBy(cardId);
}
/**
* Returns the new Card
*
* @param name
* @param columnId
* @return
*/
@Transactional(readOnly = false)
public Card createCard(String name, int columnId, User user) {
LOG.debug("createCard: {name: {}, columnId: {}, userId: {}}", name, columnId, user.getId());
int sequence = fetchAndLockSequence(columnId);
queries.createCard(trimToNull(name), columnId, user.getId(), sequence);
incrementSequence(columnId, sequence);
return queries.findLastCreatedCard();
}
@Transactional(readOnly = false)
public Card createCardFromTop(String name, int columnId, User user) {
Card createdCard = createCard(name, columnId, user);
moveLastCardAtTop(createdCard.getId(), columnId);
return createdCard;
}
@Transactional(readOnly = false)
private void moveLastCardAtTop(int lastCardId, int columnId) {
queries.incrementCardsOrder(columnId);
SqlParameterSource updateParam = new MapSqlParameterSource("columnId", columnId).addValue("cardId", lastCardId)
.addValue("cardOrder", 0);
jdbc.update(queries.updateCardOrder(), updateParam);
}
/**
* Fetch the ticket number from the counter and lock the row.
*
* @param columnId
* @return
*/
private int fetchAndLockSequence(int columnId) {
return queries.fetchAndLockCardSequence(columnId);
}
/**
* Increment the counter
*
* @param columnId
*/
@Transactional(readOnly = false)
private void incrementSequence(int columnId, int sequence) {
int affected = queries.incrementSequence(sequence, columnId);
Validate.isTrue(affected == 1, "during the update sequence, " + affected
+ " were affected for a card inserted in the columnId " + columnId);
}
/**
* move a card and update the order of the new column. The ids are filtered.
*
* @param id
* @param prevColumnId
* @param newColumnId
* @param newOrderForNewColumn
*/
@Transactional(readOnly = false)
public void moveCardToColumnAndReorder(int id, int prevColumnId, int newColumnId, List<Integer> newOrderForNewColumn) {
moveCardToColumn(id, prevColumnId, newColumnId);
updateCardOrder(newOrderForNewColumn, newColumnId);
}
@Transactional(readOnly = false)
public void moveCardToColumn(int cardId, int previousColumnId, int columnId) {
SqlParameterSource param = new MapSqlParameterSource("cardId", cardId).addValue("columnId", columnId).addValue(
"previousColumnId", previousColumnId);
int affected = jdbc.update(queries.moveCardToColumn(), param);
Validate.isTrue(1 == affected, "moveCardToColumn: must affect exactly one row");
}
@Transactional(readOnly = false)
public List<Integer> moveCardsToColumn(List<Integer> cardIds, int previousColumnId, int columnId, int userId) {
List<Integer> filteredCardIds = Utils.filter(cardIds, queries.findCardIdsInColumnId(cardIds, previousColumnId));
List<SqlParameterSource> params = new ArrayList<>(filteredCardIds.size());
for (int cardId : filteredCardIds) {
SqlParameterSource p = new MapSqlParameterSource("cardId", cardId)//
.addValue("previousColumnId", previousColumnId)//
.addValue("columnId", columnId);
params.add(p);
}
int[] updateResult = jdbc.batchUpdate(queries.moveCardToColumn(),
params.toArray(new SqlParameterSource[params.size()]));
List<Integer> updated = new ArrayList<>();
for (int i = 0; i < updateResult.length; i++) {
if (updateResult[i] > 0) {
updated.add(filteredCardIds.get(i));
}
}
return updated;
}
/**
* Update card order in a given column id. The cardIds are filtered.
*
* @param cardIds
* @param columnId
*/
@Transactional(readOnly = false)
public void updateCardOrder(List<Integer> cardIds, int columnId) {
if (cardIds.isEmpty()) {
return;
}
List<Integer> filteredCardIds = Utils.filter(cardIds, queries.findCardIdsInColumnId(cardIds, columnId));
List<SqlParameterSource> params = prepareOrderParameter(filteredCardIds, columnId);
jdbc.batchUpdate(queries.updateCardOrder(), params.toArray(new SqlParameterSource[params.size()]));
}
public Map<String, Integer> findCardsIds(List<String> cards) {
List<Object[]> param = new ArrayList<>(cards.size());
for (String card : cards) {
String[] splitted = StringUtils.split(card, '-');
if (splitted.length > 1) {
try {
Integer cardSequenceNumber = Integer.valueOf(splitted[splitted.length - 1], 10);
String boardShortName = StringUtils
.join(ArrayUtils.subarray(splitted, 0, splitted.length - 1), '-');
param.add(new Object[] { boardShortName, cardSequenceNumber });
} catch (NumberFormatException nfe) {
// skip
}
}
}
if (param.isEmpty()) {
return Collections.emptyMap();
}
final Map<String, Integer> res = new HashMap<>();
MapSqlParameterSource paramS = new MapSqlParameterSource("projShortNameAndCardSeq", param);
jdbc.query(queries.findCardsIs(), paramS, new RowCallbackHandler() {
@Override
public void processRow(ResultSet rs) throws SQLException {
res.put(rs.getString("CARD_IDENTIFIER"), rs.getInt("CARD_ID"));
}
});
return res;
}
//TODO: not happy about the interface of this one...
public List<CardFull> findCardBy(String term, Set<Integer> projectIds) {
String maybeBoardShortName = null;
Integer maybeSequenceNumber = null;
if (term != null) {
String[] splitted = term.split("-", 2);
maybeBoardShortName = splitted[0].toUpperCase(Locale.ENGLISH);
if (splitted.length > 1) {
try {
maybeSequenceNumber = Integer.valueOf(splitted[1]);
} catch (NumberFormatException ignore) {
// ignore
}
}
}
if(projectIds == null) {
return queries.findCardBy(term, maybeBoardShortName, maybeSequenceNumber);
} else if (projectIds.isEmpty()){
return Collections.emptyList();
} else {
return queries.findCardBy(term, maybeBoardShortName, maybeSequenceNumber, projectIds);
}
}
public int updateCardOrder(int cardId, int order) {
return queries.updateCardOrder(cardId, order);
}
public List<Integer> findCardIdsByColumnId(int columnId) {
return queries.findCardIdsByColumnId(columnId);
}
}