package de.flower.rmt.service; import com.google.common.base.Predicate; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.mysema.query.types.Path; import com.mysema.query.types.expr.BooleanExpression; import de.flower.common.util.Check; import de.flower.rmt.model.db.entity.CalItem; import de.flower.rmt.model.db.entity.Comment; import de.flower.rmt.model.db.entity.Invitation; import de.flower.rmt.model.db.entity.Invitation_; import de.flower.rmt.model.db.entity.Player; import de.flower.rmt.model.db.entity.QInvitation; import de.flower.rmt.model.db.entity.User; import de.flower.rmt.model.db.entity.event.Event; import de.flower.rmt.model.db.type.RSVPStatus; import de.flower.rmt.repository.IInvitationRepo; import de.flower.rmt.service.mail.INotificationService; import de.flower.rmt.util.Dates; import org.joda.time.DateTime; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import javax.mail.internet.InternetAddress; import javax.persistence.metamodel.Attribute; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; import static de.flower.rmt.repository.Specs.*; import static org.springframework.data.jpa.domain.Specifications.where; /** * @author flowerrrr */ @Service @Transactional(readOnly = true, propagation = Propagation.REQUIRED) public class InvitationManager extends AbstractService implements IInvitationManager { @Autowired private IInvitationRepo invitationRepo; @Autowired private IPlayerManager playerManager; @Autowired private IUserManager userManager; @Autowired private IActivityManager activityManager; @Autowired private INotificationService notificationService; @Autowired private ICommentManager commentManager; @Autowired private ICalendarManager calendarManager; @Autowired private ILineupManager lineupManager; @Autowired private IEventTeamManager eventTeamManager; @Autowired private MessageSourceAccessor messageSource; @Override public Invitation newInstance(final Event event, User user) { Check.notNull(event); Check.notNull(user); return new Invitation(event, user); } @Override public Invitation newInstance(final Event event, String guestName) { Check.notNull(event); Check.notBlank(guestName); return new Invitation(event, guestName); } @Override public Invitation loadById(Long id, final Attribute... attributes) { Check.notNull(id); Specification fetch = fetch(attributes); Invitation entity = invitationRepo.findOne(where(eq(Invitation_.id, id)).and(fetch)); Check.notNull(entity, "No invitation found for id [" + id + "]"); return entity; } @Override public List<Invitation> findAllByEvent(final Event event, final Attribute... attributes) { Specification fetch = fetch(attributes); return invitationRepo.findAll(where(eq(Invitation_.event, event)).and(fetch)); } @Override public List<Invitation> findAllByEventSortedByName(final Event event, Attribute... attributes) { List<Invitation> list = findAllByEvent(event, attributes); // use in-memory sorting cause field username is derived and would required complicated sql-query to sort after. return sortByName(list); } @Override public List<Invitation> findAllByEventAndStatusSortedByName(final Event event, final RSVPStatus status, final Attribute... attributes) { List<Invitation> list = findAllByEventAndStatus(event, status, attributes); // list is sorted by date -> resort return sortByName(list); } // TODO (flowerrrr - 20.04.12) not a nice method, neither name nor implementation. refactor! @Override public List<Invitation> findAllForNotificationByEventSortedByName(final Event event) { List<Invitation> list = findAllByEventSortedByName(event); // filter out those that do not want to receive email notifications Iterable<Invitation> filtered = Iterables.filter(list, new Predicate<Invitation>() { private List<Player> players; { players = playerManager.findAllByTeam(event.getTeam()); } @Override public boolean apply(final Invitation invitation) { if (!invitation.hasEmail()) { return false; } else { // check if player has opted out of email notifications for (Player player : players) { if (player.getUser().equals(invitation.getUser())) { return player.isNotification(); } } // this happens for invitees who are not team-members (invitees that were later added). return true; } } }); return ImmutableList.copyOf(filtered); } @Override public List<InternetAddress> getAddressesForfAllInvitees(final Event event) { List<Invitation> list = findAllByEventSortedByName(event); // convert to list of internet addresses List<InternetAddress[]> internetAddresses = de.flower.common.util.Collections.convert(list, new de.flower.common.util.Collections.IElementConverter<Invitation, InternetAddress[]>() { @Override public InternetAddress[] convert(final Invitation element) { if (element.hasEmail()) { return element.getInternetAddresses(); } else { return null; } } }); return de.flower.common.util.Collections.flattenArray(internetAddresses); } @Override public List<Invitation> findAllByEventAndStatus(Event event, RSVPStatus rsvpStatus, final Attribute... attributes) { List<Invitation> list = invitationRepo.findAll(where(eq(Invitation_.event, event)) .and(eq(Invitation_.status, rsvpStatus)) .and(asc(Invitation_.date)) .and(fetch(attributes))); if (rsvpStatus == RSVPStatus.NORESPONSE) { // no response means there is no date set yet. so sort by name instead sortByName(list); } return list; } @Override public Long numByEventAndStatus(final Event event, final RSVPStatus rsvpStatus) { return invitationRepo.numByEventAndStatus(event, rsvpStatus); } @Override public List<Invitation> findAllForNoResponseReminder(final Event event, final int hoursAfterInvitationSent) { DateTime now = new DateTime(); BooleanExpression isEvent = QInvitation.invitation.event.eq(event); // no invitation sent yet -> cannot blame user for not having responded. BooleanExpression isInvitationSent = QInvitation.invitation.invitationSent.eq(true); BooleanExpression isNoResponse = QInvitation.invitation.status.eq(RSVPStatus.NORESPONSE); BooleanExpression isHoursAfterInvitationSent = QInvitation.invitation.invitationSentDate.before(now.minusHours(hoursAfterInvitationSent).toDate()); BooleanExpression isNotReminderSent = QInvitation.invitation.noResponseReminderSent.eq(false); return invitationRepo.findAll(isEvent.and(isInvitationSent).and(isNoResponse).and(isHoursAfterInvitationSent).and(isNotReminderSent), QInvitation.invitation.user); } @Override public List<Invitation> findAllForUnsureReminder(final Event event) { BooleanExpression isEvent = QInvitation.invitation.event.eq(event); BooleanExpression isUnsure = QInvitation.invitation.status.eq(RSVPStatus.UNSURE); BooleanExpression isNotReminderSent = QInvitation.invitation.unsureReminderSent.eq(false); return invitationRepo.findAll(isEvent.and(isUnsure).and(isNotReminderSent), QInvitation.invitation.user); } @Override public Invitation loadByEventAndUser(Event event, User user) { Invitation invitation = findByEventAndUser(event, user); Check.notNull(invitation, "No invitation found"); return invitation; } @Override public Invitation findByEventAndUser(Event event, User user, Path<?>... attributes) { BooleanExpression isEvent = QInvitation.invitation.event.eq(event); BooleanExpression isUser = QInvitation.invitation.user.eq(user); Invitation invitation = invitationRepo.findOne(isEvent.and(isUser), attributes); return invitation; } @Override @Transactional(readOnly = false) public void save(final Invitation invitation) { _save(invitation, null); } @Override @Transactional(readOnly = false) public void save(final Invitation invitation, String comment) { _save(invitation, (comment == null) ? "" : comment); } /** * @param invitation * @param comment if not-null comment will be updated */ private void _save(final Invitation invitation, String comment) { validate(invitation); boolean isNew = invitation.isNew(); boolean isNotifyManager = false; Invitation origInvitation; if (!isNew) { // depending on the caller of this method the invitation might be detached or attached (when called // by ResponseManager. If object is attached it is not possible to check for modifications against the // saved state in database as em.findOne() will return the attached version from session cache. invitationRepo.detach(invitation); // in case the status changes update the date of response. // used for early maybe-responder who later switch their status. // after status update the rank of an invitation is reset as if he has // just responded the first time. origInvitation = invitationRepo.findOne(invitation.getId()); Check.isTrue(invitation != origInvitation); if (origInvitation.getStatus() != invitation.getStatus()) { invitation.setDate(new Date()); if (origInvitation.getStatus() == RSVPStatus.ACCEPTED) { // if status changes from accepted to any other state -> notify manager // but only if it's not the manager himself who changes the status. // but only if event is not cancelled -> all users will switch from ACCEPTED to DECLINED. if (!securityService.getUser().isManager() && !origInvitation.getEvent().isCanceled()) { isNotifyManager = true; } } } if (invitation.getDate() == null) { invitation.setDate(new Date()); } // invitations are created when event is created. that's not interesting to track. we'd only // like to know when invitation is updated. Comment origComment = commentManager.findByInvitationAndAuthor(origInvitation, securityService.getUser(), 0); // must be called before invitation is persisted. otherwise origInvitation would contain the new values. activityManager.onInvitationUpdated(invitation, origInvitation, comment, (origComment == null) ? null : origComment.getText()); } invitationRepo.save(invitation); // update comment if (comment != null) { commentManager.updateOrRemoveComment(invitation, comment, securityService.getUser()); } if (isNotifyManager) { try { notificationService.sendStatusChangedMessage(invitation); } catch (Exception e) { log.error("Could not send notification.", e); } } } @Override @Transactional(readOnly = false) public void delete(final Long id) { // delete from lineup lineupManager.removeLineupItem(id); eventTeamManager.removeInvitation(id); invitationRepo.delete(id); } @Override @Transactional(readOnly = false) public void markInvitationSent(final Event event, final List<String> addressList, Date date) { invitationRepo.markInvitationSent(event, addressList, date == null ? new Date() : date); } @Override @Transactional(readOnly = false) public void markNoResponseReminderSent(final List<Invitation> invitations) { invitationRepo.markNoResponseReminderSent(invitations, new Date()); } @Override public void markUnsureReminderSent(final List<Invitation> invitations) { invitationRepo.markUnsureReminderSent(invitations, new Date()); } @Override @Transactional(readOnly = false) public void addUsers(final Event entity, final Collection<Long> userIds) { for (Long userId : userIds) { User user = userManager.loadById(userId); Invitation invitation = newInstance(entity, user); save(invitation); checkForAutoDecline(invitation); } } /** * Searches for user-cal-items that match the event date. if autoDecline is true * the invitation will be declined. * * @return true if invitation is auto declined. */ private boolean checkForAutoDecline(Invitation invitation) { DateTime eventDate = invitation.getEvent().getDateTime(); List<CalItem> list = calendarManager.findAllByUserAndRange(invitation.getUser(), eventDate, eventDate); for (CalItem calItem : list) { if (calItem.isAutoDecline()) { autoDecline(invitation, calItem); return true; } } return false; } private void autoDecline(final Invitation invitation, final CalItem calItem) { log.info("Auto declining user [{}] for event [{}] due to calendar item [{}]", new Object[]{invitation.getUser().getEmail(), invitation.getEvent(), calItem}); invitation.setStatus(RSVPStatus.DECLINED); invitation.setDate(new Date()); // no validation, no activity log, just plain save invitationRepo.save(invitation); // set comment String comment; if (calItem.getType() == CalItem.Type.OTHER) { comment = calItem.getSummary(); } else { comment = messageSource.getMessage(CalItem.Type.getResourceKey(calItem.getType())); } if (!calItem.isSingleDay() && calItem.getType() == CalItem.Type.HOLIDAY) { comment += String.format(" (%s - %s)", Dates.formatDateShort(calItem.getStartDateTime().toDate()), Dates.formatDateShort(calItem.getEndDateTime().toDate())); } commentManager.updateOrRemoveComment(invitation, comment, invitation.getUser()); } @Override @Transactional(readOnly = false) public void addGuestPlayer(final Event entity, final String guestName) { Invitation invitation = newInstance(entity, guestName); save(invitation); } private List<Invitation> sortByName(List<Invitation> list) { Collections.sort(list, new Comparator<Invitation>() { @Override public int compare(final Invitation o1, final Invitation o2) { return o1.getName().compareToIgnoreCase(o2.getName()); } }); return list; } @Override @Transactional(readOnly = false) public void onAutoDeclineCalItem(final CalItem calItem) { // find all invitations belonging to user and inside calendar event range. Check.isTrue(calItem.isAutoDecline()); List<Invitation> invitations = findAllByUserAndRange(calItem.getUser(), calItem.getStartDateTime(), calItem.getEndDateTime()); for (Invitation invitation : invitations) { if (invitation.getStatus() == RSVPStatus.NORESPONSE) { autoDecline(invitation, calItem); } } } private List<Invitation> findAllByUserAndRange(final User user, final DateTime dateStart, final DateTime dateEnd) { BooleanExpression isUser = QInvitation.invitation.user.eq(user); BooleanExpression isEventInRange = QInvitation.invitation.event.dateTime.between(dateStart, dateEnd); return invitationRepo.findAll(isUser.and(isEventInRange)); } }