/** * 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.samskivert.mustache.Escapers; import com.samskivert.mustache.Mustache; import com.samskivert.mustache.MustacheException; import com.samskivert.mustache.Template; import com.samskivert.mustache.Template.Fragment; import io.lavagna.model.*; import io.lavagna.query.NotificationQuery; import org.apache.commons.lang3.EnumUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.ImmutableTriple; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.context.MessageSource; import org.springframework.core.io.ClassPathResource; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.mail.MailException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.io.IOException; import java.io.InputStreamReader; import java.io.Writer; import java.nio.charset.StandardCharsets; import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; import java.util.Map.Entry; /** * Handle the whole email notification process. */ @Service @Transactional(readOnly = false) public class NotificationService { private static final Logger LOG = LogManager.getLogger(); private final ConfigurationRepository configurationRepository; private final BoardColumnRepository boardColumnRepository; private final CardDataRepository cardDataRepository; private final CardRepository cardRepository; private final UserRepository userRepository; private final MessageSource messageSource; private final NamedParameterJdbcTemplate jdbc; private final NotificationQuery queries; private final Template emailTextTemplate; private final Template emailHtmlTemplate; public NotificationService(ConfigurationRepository configurationRepository, UserRepository userRepository, CardDataRepository cardDataRepository, CardRepository cardRepository, BoardColumnRepository boardColumnRepository, MessageSource messageSource, NamedParameterJdbcTemplate jdbc, NotificationQuery queries) { this.configurationRepository = configurationRepository; this.userRepository = userRepository; this.cardDataRepository = cardDataRepository; this.cardRepository = cardRepository; this.boardColumnRepository = boardColumnRepository; this.messageSource = messageSource; this.jdbc = jdbc; this.queries = queries; com.samskivert.mustache.Mustache.Compiler compiler = Mustache.compiler().escapeHTML(false).defaultValue(""); try { emailTextTemplate = compiler.compile(new InputStreamReader( new ClassPathResource("/io/lavagna/notification/email.txt") .getInputStream(), StandardCharsets.UTF_8)); emailHtmlTemplate = compiler .compile(new InputStreamReader(new ClassPathResource( "/io/lavagna/notification/email.html") .getInputStream(), StandardCharsets.UTF_8)); } catch (IOException e) { throw new IllegalStateException(e); } } /** * Return a list of user id to notify. * * @param upTo * @return */ public Set<Integer> check(Date upTo) { final List<Integer> userWithChanges = new ArrayList<>(); List<SqlParameterSource> res = jdbc.query(queries.countNewForUsersId(), new RowMapper<SqlParameterSource>() { @Override public SqlParameterSource mapRow(ResultSet rs, int rowNum) throws SQLException { int userId = rs.getInt("USER_ID"); userWithChanges.add(userId); return new MapSqlParameterSource("count", rs.getInt("COUNT_EVENT_ID")).addValue("userId", userId); } }); if (!res.isEmpty()) { jdbc.batchUpdate(queries.updateCount(), res.toArray(new SqlParameterSource[res.size()])); } queries.updateCheckDate(upTo); // select users that have pending notifications that were not present in this check round MapSqlParameterSource userWithChangesParam = new MapSqlParameterSource("userWithChanges", userWithChanges); // List<Integer> usersToNotify = jdbc.queryForList(queries.usersToNotify() + " " + (userWithChanges.isEmpty() ? "" : queries.notIn()), userWithChangesParam, Integer.class); // jdbc.update(queries.reset() + " " + (userWithChanges.isEmpty() ? "" : queries.notIn()), userWithChangesParam); // return new TreeSet<>(usersToNotify); } private List<String> composeCardSection(List<Event> events, EventsContext context) { // List<String> res = new ArrayList<>(); for (Event e : events) { if (EnumUtils.isValidEnum(SupportedEventType.class, e.getEvent().toString())) { ImmutablePair<String, String[]> message = SupportedEventType.valueOf(e.getEvent().toString()) .toKeyAndParam(e, context, cardDataRepository); res.add(messageSource.getMessage(message.getKey(), message.getValue(), Locale.ENGLISH)); } } return res; } private ImmutableTriple<String, String, String> composeEmailForUser(EventsContext context) throws MustacheException, IOException { List<Map<String, Object>> cardsModel = new ArrayList<>(); StringBuilder subject = new StringBuilder(); for (Entry<Integer, List<Event>> kv : context.events.entrySet()) { Map<String, Object> cardModel = new HashMap<>(); CardFull cf = context.cards.get(kv.getKey()); StringBuilder cardName = new StringBuilder(cf.getBoardShortName()).append("-").append(cf.getSequence()) .append(" ").append(cf.getName()); cardModel.put("cardFull", cf); cardModel.put("cardName", cardName.toString()); cardModel.put("cardEvents", composeCardSection(kv.getValue(), context)); subject.append(cf.getBoardShortName()).append("-").append(cf.getSequence()).append(", "); cardsModel.add(cardModel); } Map<String, Object> tmplModel = new HashMap<>(); String baseApplicationUrl = StringUtils .appendIfMissing(configurationRepository.getValue(Key.BASE_APPLICATION_URL), "/"); tmplModel.put("cards", cardsModel); tmplModel.put("baseApplicationUrl", baseApplicationUrl); tmplModel.put("htmlEscape", new Mustache.Lambda() { @Override public void execute(Fragment frag, Writer out) throws IOException { out.write(Escapers.HTML.escape(frag.execute())); } }); String text = emailTextTemplate.execute(tmplModel); String html = emailHtmlTemplate.execute(tmplModel); return ImmutableTriple.of(subject.substring(0, subject.length() - ", ".length()), text, html); } /** * Send email (if all the conditions are met) to the user. * * @param userId * @param upTo * @param emailEnabled * @param mailConfig */ public void notifyUser(int userId, Date upTo, boolean emailEnabled, MailConfig mailConfig) { Date lastSent = queries.lastEmailSent(userId); User user = userRepository.findById(userId); Date fromDate = ObjectUtils.firstNonNull(lastSent, DateUtils.addDays(upTo, -1)); List<Event> events = user.getSkipOwnNotifications() ? queries.eventsForUserWithoutHisOwns(userId, fromDate, upTo) : queries.eventsForUser(userId, fromDate, upTo); if (!events.isEmpty() && mailConfig != null && mailConfig.getMinimalConfigurationPresent() && emailEnabled && user.canSendEmail()) { try { sendEmailToUser(user, events, mailConfig); } catch (MustacheException | IOException | MailException e) { LOG.warn("Error while sending an email to user with id " + user.getId(), e); } } // queries.updateSentEmailDate(upTo, userId); } private void sendEmailToUser(User user, List<Event> events, MailConfig mailConfig) throws MustacheException, IOException { Set<Integer> userIds = new HashSet<>(); userIds.add(user.getId()); Set<Integer> cardIds = new HashSet<>(); Set<Integer> cardDataIds = new HashSet<>(); Set<Integer> columnIds = new HashSet<>(); for (Event e : events) { cardIds.add(e.getCardId()); userIds.add(e.getUserId()); addIfNotNull(userIds, e.getValueUser()); addIfNotNull(cardIds, e.getValueCard()); addIfNotNull(cardDataIds, e.getDataId()); addIfNotNull(cardDataIds, e.getPreviousDataId()); addIfNotNull(columnIds, e.getColumnId()); addIfNotNull(columnIds, e.getPreviousColumnId()); } final ImmutableTriple<String, String, String> subjectAndText = composeEmailForUser(new EventsContext(events, userRepository.findByIds(userIds), cardRepository.findAllByIds(cardIds), cardDataRepository.findDataByIds(cardDataIds), boardColumnRepository.findByIds(columnIds))); mailConfig.send(user.getEmail(),"Lavagna: " + subjectAndText.getLeft(), subjectAndText.getMiddle(), subjectAndText.getRight()); } private static <T> void addIfNotNull(Set<T> s, T v) { if (v != null) { s.add(v); } } }