fr.aliacom.obm.common.calendar.EventNotificationServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for fr.aliacom.obm.common.calendar.EventNotificationServiceImpl.java

Source

/* ***** BEGIN LICENSE BLOCK *****
 * 
 * Copyright (C) 2011-2014  Linagora
 *
 * This program is free software: you can redistribute it and/or 
 * modify it under the terms of the GNU Affero General Public License as 
 * published by the Free Software Foundation, either version 3 of the 
 * License, or (at your option) any later version, provided you comply 
 * with the Additional Terms applicable for OBM connector by Linagora 
 * pursuant to Section 7 of the GNU Affero General Public License, 
 * subsections (b), (c), and (e), pursuant to which you must notably (i) retain 
 * the Message sent thanks to OBM, Free Communication by Linagora? 
 * signature notice appended to any and all outbound messages 
 * (notably e-mail and meeting requests), (ii) retain all hypertext links between 
 * OBM and obm.org, as well as between Linagora and linagora.com, and (iii) refrain 
 * from infringing Linagora intellectual property rights over its trademarks 
 * and commercial brands. Other Additional Terms apply, 
 * see <http://www.linagora.com/licenses/> for more details. 
 *
 * This program 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 Affero General Public License 
 * for more details. 
 *
 * You should have received a copy of the GNU Affero General Public License 
 * and its applicable Additional Terms for OBM along with this program. If not, 
 * see <http://www.gnu.org/licenses/> for the GNU Affero General Public License version 3 
 * and <http://www.linagora.com/licenses/> for the Additional Terms applicable to 
 * OBM connectors. 
 * 
 * ***** END LICENSE BLOCK ***** */
package fr.aliacom.obm.common.calendar;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TimeZone;

import org.apache.commons.lang.StringUtils;
import org.obm.icalendar.ICalendarFactory;
import org.obm.icalendar.Ical4jHelper;
import org.obm.icalendar.Ical4jUser;
import org.obm.sync.auth.AccessToken;
import org.obm.sync.calendar.Attendee;
import org.obm.sync.calendar.Event;
import org.obm.sync.calendar.Participation;
import org.obm.sync.calendar.ResourceAttendee;
import org.obm.sync.server.mailer.AbstractMailer.NotificationException;
import org.obm.sync.server.mailer.EventChangeMailer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Collections2;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import com.google.inject.Inject;
import com.google.inject.Singleton;

import fr.aliacom.obm.common.domain.ObmDomain;
import fr.aliacom.obm.common.setting.SettingsService;
import fr.aliacom.obm.common.user.ObmUser;
import fr.aliacom.obm.common.user.UserService;
import fr.aliacom.obm.common.user.UserSettings;

@Singleton
public class EventNotificationServiceImpl implements EventNotificationService {

    private static final Logger logger = LoggerFactory.getLogger(EventChangeHandler.class);

    private final UserService userService;
    private final SettingsService settingsService;
    private final EventChangeMailer eventChangeMailer;

    private final Ical4jHelper ical4jHelper;

    private final ICalendarFactory calendarFactory;

    @Inject
    @VisibleForTesting
    EventNotificationServiceImpl(EventChangeMailer eventChangeMailer, SettingsService settingsService,
            UserService userService, Ical4jHelper ical4jHelper, ICalendarFactory calendarFactory) {

        this.userService = userService;
        this.settingsService = settingsService;
        this.eventChangeMailer = eventChangeMailer;
        this.ical4jHelper = ical4jHelper;
        this.calendarFactory = calendarFactory;
    }

    @Override
    public void notifyCreatedEvent(Event event, AccessToken token) {
        ObmUser user = userService.getUserFromAccessToken(token);
        Ical4jUser buildIcal4jUser = calendarFactory.createIcal4jUserFromObmUser(user);
        String ics = ical4jHelper.buildIcsInvitationRequest(buildIcal4jUser, event, token);
        Attendee owner = event.findOwner();

        Collection<Attendee> attendees = filterAttendees(event, ensureAttendeeUnicity(event.getAttendees()));
        if (eventCreationInvolveNotification(event)) {
            notifyCreate(user, attendees, event, ics, token);
        }
        if (!isUserEventOwner(user, event)) {
            notifyOwnerCreate(user, event, owner, token);
        }

    }

    @Override
    public void notifyDeletedEvent(Event event, AccessToken token) {
        if (eventDeletionInvolveNotification(event)) {
            ObmUser user = userService.getUserFromAccessToken(token);
            Ical4jUser buildIcal4jUser = calendarFactory.createIcal4jUserFromObmUser(user);
            String ics = ical4jHelper.buildIcsInvitationCancel(buildIcal4jUser, event, token);

            Attendee owner = event.findOwner();

            Collection<Attendee> attendees = filterAttendees(event, ensureAttendeeUnicity(event.getAttendees()));
            Map<Participation, Set<Attendee>> attendeeGroups = computeParticipationGroups(attendees);
            Set<Attendee> notify = Sets.union(attendeeGroups.get(Participation.needsAction()),
                    attendeeGroups.get(Participation.accepted()));

            UserSettings settings = settingsService.getSettings(user);
            if (!notify.isEmpty()) {
                eventChangeMailer.notifyRemovedUsers(user, notify, event, settings.locale(), settings.timezone(),
                        ics, token);
            }
            if (owner != null && !isUserEventOwner(user, event)) {
                eventChangeMailer.notifyOwnerRemovedEvent(user, owner, event, settings.locale(),
                        settings.timezone(), token);
            }
        }
    }

    @Override
    public void notifyUpdatedParticipationAttendees(Event event, ObmUser calendarOwner, Participation participation,
            AccessToken token) {

        if (isHandledParticipation(participation)) {

            Ical4jUser replyIcal4jUser = calendarFactory.createIcal4jUserFromObmUser(calendarOwner);
            String ics = ical4jHelper.buildIcsInvitationReply(event, replyIcal4jUser, token);

            final Attendee organizer = event.findOrganizer();
            if (organizer != null) {
                if (updateParticipationNeedsNotification(calendarOwner, organizer)) {
                    UserSettings settings = settingsService.getSettings(calendarOwner);
                    eventChangeMailer.notifyUpdateParticipation(event, organizer, calendarOwner, participation,
                            settings.locale(), settings.timezone(), ics, token);
                }
            } else {
                logger.error("Can't find organizer, email won't send");
            }
        }
    }

    @Override
    public void notifyUpdatedEvent(Event previous, Event current, AccessToken token) {

        ObmUser user = userService.getUserFromAccessToken(token);
        Ical4jUser buildIcal4jUser = calendarFactory.createIcal4jUserFromObmUser(user);

        String addUserIcs = ical4jHelper.buildIcsInvitationRequest(buildIcal4jUser, current, token);
        String removedUserIcs = ical4jHelper.buildIcsInvitationCancel(buildIcal4jUser, previous, token);
        String updateUserIcs = ical4jHelper.buildIcsInvitationRequest(buildIcal4jUser, current, token);

        UserSettings settings = settingsService.getSettings(user);
        TimeZone timezone = settings.timezone();
        Locale locale = settings.locale();

        Map<AttendeeStateValue, Set<Attendee>> attendeeGroups = computeUpdateNotificationGroups(previous, current);
        notifyRemovedUsers(previous, token, user, removedUserIcs, timezone, locale, attendeeGroups);
        notifyAddedUsers(current, token, user, addUserIcs, attendeeGroups);

        boolean notifyImportantChanges = previous.getSequence() != current.getSequence();
        if (notifyImportantChanges) {
            notifyKeptUsers(previous, current, token, user, updateUserIcs, timezone, locale, attendeeGroups);
            notifyOwnerOfUpdate(previous, current, token, user, timezone, locale);
        }
    }

    private void notifyOwnerOfUpdate(Event previous, Event current, AccessToken token, ObmUser user,
            TimeZone timezone, Locale locale) {
        Attendee owner = current.findOwner();
        if (owner != null && !isUserEventOwner(user, current)) {
            notifyOwnerUpdate(user, owner, previous, current, locale, timezone, token);
        }
    }

    private void notifyKeptUsers(Event previous, Event current, AccessToken token, ObmUser user,
            String updateUserIcs, TimeZone timezone, Locale locale,
            Map<AttendeeStateValue, Set<Attendee>> attendeeGroups) {
        Set<Attendee> keptAttendees = attendeeGroups.get(AttendeeStateValue.KEPT);
        if (!keptAttendees.isEmpty()) {
            Map<Participation, Set<Attendee>> atts = computeParticipationGroups(keptAttendees);
            notifyAcceptedUpdateUsers(user, previous, current, locale, atts, timezone, updateUserIcs, token);
            notifyNeedActionUpdateUsers(user, previous, current, locale, atts, timezone, updateUserIcs, token);
        }
    }

    private void notifyAddedUsers(Event current, AccessToken token, ObmUser user, String addUserIcs,
            Map<AttendeeStateValue, Set<Attendee>> attendeeGroups) {
        Set<Attendee> addedAttendees = attendeeGroups.get(AttendeeStateValue.ADDED);
        if (!addedAttendees.isEmpty()) {
            notifyCreate(user, addedAttendees, current, addUserIcs, token);
        }
    }

    private void notifyRemovedUsers(Event previous, AccessToken token, ObmUser user, String removedUserIcs,
            TimeZone timezone, Locale locale, Map<AttendeeStateValue, Set<Attendee>> attendeeGroups) {
        final Set<Attendee> removedAttendees = attendeeGroups.get(AttendeeStateValue.REMOVED);
        if (!removedAttendees.isEmpty()) {
            eventChangeMailer.notifyRemovedUsers(user, removedAttendees, previous, locale, timezone, removedUserIcs,
                    token);
        }
    }

    private boolean updateParticipationNeedsNotification(final ObmUser calendarOwner, final Attendee organizer) {

        return organizerHasEmailAddress(organizer) && organizerMayAttend(organizer)
                && organizerExpectParticipationEmails(organizer, calendarOwner.getDomain())
                && !organizer.getEmail().equalsIgnoreCase(calendarOwner.getEmailAtDomain());
    }

    private boolean organizerExpectParticipationEmails(Attendee organizer, ObmDomain domain) {
        ObmUser user = userService.getUserFromLogin(organizer.getEmail(), domain.getName());
        if (user == null) {
            return true;
        }
        UserSettings settings = settingsService.getSettings(user);
        return settings.expectParticipationEmailNotification();
    }

    private boolean organizerMayAttend(final Attendee organizer) {
        return !Participation.declined().equals(organizer.getParticipation());
    }

    private boolean organizerHasEmailAddress(final Attendee organizer) {
        return !StringUtils.isEmpty(organizer.getEmail());
    }

    private boolean isHandledParticipation(final Participation participation) {
        if (participation == null) {
            return false;
        }
        switch (participation.getState()) {
        case ACCEPTED:
        case DECLINED:
            return true;
        default:
            return false;
        }
    }

    private boolean eventDeletionInvolveNotification(final Event event) {
        return !event.isEventInThePast();
    }

    private boolean eventCreationInvolveNotification(final Event event) {
        return !event.isEventInThePast();
    }

    private Collection<Attendee> ensureAttendeeUnicity(List<Attendee> attendees) {
        return ImmutableSet.copyOf(attendees);
    }

    private void notifyOwnerCreate(ObmUser user, Event event, Attendee owner, AccessToken token) {
        UserSettings settings = settingsService.getSettings(user);
        Locale locale = settings.locale();
        TimeZone timezone = settings.timezone();

        Collection<Attendee> ownerAsCollection = new ArrayList<Attendee>(1);
        ownerAsCollection.add(owner);
        eventChangeMailer.notifyAcceptedNewUsers(user, ownerAsCollection, event, locale, timezone, token);
    }

    private void notifyCreate(final ObmUser user, final Collection<Attendee> attendees, final Event event,
            String ics, AccessToken token) throws NotificationException {

        UserSettings settings = settingsService.getSettings(user);
        Locale locale = settings.locale();
        TimeZone timezone = settings.timezone();

        Map<Participation, Set<Attendee>> attendeeGroups = computeParticipationGroups(attendees);
        Set<Attendee> accepted = attendeeGroups.get(Participation.accepted());
        if (accepted != null && !accepted.isEmpty()) {
            eventChangeMailer.notifyAcceptedNewUsers(user, accepted, event, locale, timezone, token);
        }

        Set<Attendee> notAccepted = attendeeGroups.get(Participation.needsAction());
        if (notAccepted != null && !notAccepted.isEmpty()) {
            eventChangeMailer.notifyNeedActionNewUsers(user, notAccepted, event, locale, timezone, ics, token);
        }
    }

    private boolean isUserEventOwner(ObmUser user, Event event) {
        return user.getEmailAtDomain().equals(event.getOwnerEmail());
    }

    private void notifyOwnerUpdate(ObmUser user, Attendee owner, Event previous, Event current, Locale locale,
            TimeZone timezone, AccessToken token) {
        eventChangeMailer.notifyOwnerUpdate(user, owner, previous, current, locale, timezone, token);
    }

    private void notifyNeedActionUpdateUsers(ObmUser user, Event previous, Event current, Locale locale,
            Map<Participation, Set<Attendee>> atts, TimeZone timezone, String ics, AccessToken token) {

        logger.info("Listing all event attendees for event with name=[" + current.getTitle() + "]");
        for (Entry<Participation, Set<Attendee>> attendeesByState : atts.entrySet()) {
            logger.info("Attendees in state=[" + attendeesByState.getKey().getState().name() + "]");
            for (Attendee attendee : attendeesByState.getValue()) {
                logger.info("<" + attendee.getEmail() + "> is in [" + attendee.getParticipation().getState().name()
                        + "]");
            }
        }

        final Set<Attendee> notAccepted = atts.get(Participation.needsAction());

        if (notAccepted != null && !notAccepted.isEmpty()) {
            eventChangeMailer.notifyNeedActionUpdateUsers(user, notAccepted, previous, current, locale, timezone,
                    ics, token);
        }
    }

    private Collection<Attendee> filterCanWriteOnCalendar(Set<Attendee> attendees) {
        return Collections2.filter(attendees, new Predicate<Attendee>() {
            @Override
            public boolean apply(Attendee attendee) {
                return attendee.isCanWriteOnCalendar();
            }
        });
    }

    private void notifyAcceptedUpdateUsers(ObmUser user, Event previous, Event current, Locale locale,
            Map<Participation, ? extends Set<Attendee>> atts, TimeZone timezone, String ics, AccessToken token) {

        Set<Attendee> attendeesAccepted = atts.get(Participation.accepted());
        if (attendeesAccepted != null) {
            Collection<Attendee> attendeesCanWriteOnCalendar = filterCanWriteOnCalendar(attendeesAccepted);
            if (attendeesCanWriteOnCalendar != null && !attendeesCanWriteOnCalendar.isEmpty()) {
                eventChangeMailer.notifyAcceptedUpdateUsersCanWriteOnCalendar(user, attendeesCanWriteOnCalendar,
                        previous, current, locale, timezone, token);
            }

            attendeesAccepted.removeAll(attendeesCanWriteOnCalendar);
            if (!attendeesAccepted.isEmpty()) {
                eventChangeMailer.notifyAcceptedUpdateUsers(user, attendeesAccepted, previous, current, locale,
                        timezone, ics, token);
            }
        }
    }

    @VisibleForTesting
    Map<AttendeeStateValue, Set<Attendee>> computeUpdateNotificationGroups(Event previous, Event current) {

        if (previous.isEventInThePast() && current.isEventInThePast()) {
            Set<Attendee> emptyAttendeesSet = ImmutableSet.of();
            return ImmutableMap.of(AttendeeStateValue.REMOVED, emptyAttendeesSet, AttendeeStateValue.KEPT,
                    emptyAttendeesSet, AttendeeStateValue.ADDED, emptyAttendeesSet);
        }

        ImmutableSet<Attendee> previousAttendees = ImmutableSet
                .copyOf(filterAttendees(previous, previous.getAttendees()));
        ImmutableSet<Attendee> currentAttendees = ImmutableSet
                .copyOf(filterAttendees(current, current.getAttendees()));
        SetView<Attendee> removedAttendees = Sets.difference(previousAttendees, currentAttendees);
        SetView<Attendee> addedAttendees = Sets.difference(currentAttendees, previousAttendees);
        Set<Attendee> keptAttendees = Sets.difference(currentAttendees, addedAttendees);

        return ImmutableMap.of(AttendeeStateValue.REMOVED, removedAttendees, AttendeeStateValue.KEPT, keptAttendees,
                AttendeeStateValue.ADDED, addedAttendees);
    }

    private Collection<Attendee> filterAttendees(final Event event, Collection<Attendee> attendees) {
        return FluentIterable.from(attendees).filter(new Predicate<Attendee>() {
            @Override
            public boolean apply(Attendee attendee) {
                return !attendee.getEmail().equalsIgnoreCase(event.getOwnerEmail());
            }
        }).filter(Predicates.not(Predicates.instanceOf(ResourceAttendee.class))).toSet();
    }

    private Map<Participation, Set<Attendee>> computeParticipationGroups(Collection<Attendee> attendees) {
        Set<Attendee> acceptedAttendees = Sets.newLinkedHashSet();
        Set<Attendee> needActionAttendees = Sets.newLinkedHashSet();
        Set<Attendee> declinedAttendees = Sets.newLinkedHashSet();

        Set<Attendee> tentativeAttendees = Sets.newLinkedHashSet();
        Set<Attendee> delegatedAttendees = Sets.newLinkedHashSet();

        Set<Attendee> completedAttendees = Sets.newLinkedHashSet();
        Set<Attendee> inprogressAttendees = Sets.newLinkedHashSet();

        for (Attendee att : attendees) {
            switch (att.getParticipation().getState()) {
            case ACCEPTED:
                acceptedAttendees.add(att);
                break;
            case NEEDSACTION:
                needActionAttendees.add(att);
                break;
            case DECLINED:
                declinedAttendees.add(att);
                break;
            case TENTATIVE:
                tentativeAttendees.add(att);
                break;
            case DELEGATED:
                delegatedAttendees.add(att);
                break;
            case COMPLETED:
                completedAttendees.add(att);
                break;
            case INPROGRESS:
                inprogressAttendees.add(att);
                break;
            }
        }
        Builder<Participation, Set<Attendee>> ret = ImmutableMap.builder();
        ret.put(Participation.accepted(), acceptedAttendees);
        ret.put(Participation.needsAction(), needActionAttendees);
        ret.put(Participation.declined(), declinedAttendees);
        ret.put(Participation.tentative(), tentativeAttendees);
        ret.put(Participation.delegated(), delegatedAttendees);
        ret.put(Participation.completed(), completedAttendees);
        ret.put(Participation.inProgress(), inprogressAttendees);
        return ret.build();
    }

    /* package */ static enum AttendeeStateValue {
        ADDED, KEPT, REMOVED;
    }

}