package com.integralblue.availability.service.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import com.integralblue.availability.model.Availability;
import com.integralblue.availability.model.CalendarEvent;
import com.integralblue.availability.model.FreeBusyStatus;
import com.integralblue.availability.model.Room;
import com.integralblue.availability.model.RoomList;
import com.integralblue.availability.properties.ExchangeConnectionProperties;
import com.integralblue.availability.service.AvailabilityService;
import lombok.NonNull;
import lombok.SneakyThrows;
import microsoft.exchange.webservices.data.core.ExchangeService;
import microsoft.exchange.webservices.data.core.enumeration.availability.AvailabilityData;
import microsoft.exchange.webservices.data.core.enumeration.misc.error.ServiceError;
import microsoft.exchange.webservices.data.core.enumeration.property.LegacyFreeBusyStatus;
import microsoft.exchange.webservices.data.core.exception.service.remote.ServiceResponseException;
import microsoft.exchange.webservices.data.core.response.AttendeeAvailability;
import microsoft.exchange.webservices.data.credential.WebCredentials;
import microsoft.exchange.webservices.data.misc.availability.AttendeeInfo;
import microsoft.exchange.webservices.data.misc.availability.AvailabilityOptions;
import microsoft.exchange.webservices.data.misc.availability.GetUserAvailabilityResults;
import microsoft.exchange.webservices.data.misc.availability.TimeWindow;
import microsoft.exchange.webservices.data.property.complex.EmailAddress;
import microsoft.exchange.webservices.data.property.complex.availability.Suggestion;
import microsoft.exchange.webservices.data.property.complex.availability.TimeSuggestion;
@Service
@CacheConfig(cacheNames= {"exchange"})
public class ExchangeAvailabilityService implements AvailabilityService {
@Autowired
private ExchangeConnectionProperties exchangeConnectionProperties;
@Override
@SneakyThrows
public Map<String, Optional<Availability>> getAvailability(List<String> emailAddresses, Date start,
Date end) {
Assert.notEmpty(emailAddresses, "emailAddresses cannot be empty");
Assert.isTrue(! start.after(end), "start must not be after end");
final Map<String, Optional<Availability>> ret = new HashMap<>();
final AvailabilityOptions availabilityOptions = new AvailabilityOptions();
availabilityOptions.setMeetingDuration(30);
GetUserAvailabilityResults results;
try(ExchangeService exchangeService = getExchangeService()){
// minimum time frame allowed by API is 24 hours
results = exchangeService.getUserAvailability(emailAddresses.stream().map(AttendeeInfo::new).collect(Collectors.toList()),
new TimeWindow(start, end.before(DateUtils.addDays(start, 1))?DateUtils.addDays(start, 1):end), AvailabilityData.FreeBusyAndSuggestions,availabilityOptions);
}
Assert.isTrue(results.getAttendeesAvailability().getCount() == emailAddresses.size());
for(int attendeesAvailabilityIndex=0;attendeesAvailabilityIndex<results.getAttendeesAvailability().getCount();attendeesAvailabilityIndex++){
AttendeeAvailability attendeeAvailability;
try {
attendeeAvailability = results.getAttendeesAvailability().getResponseAtIndex(attendeesAvailabilityIndex);
attendeeAvailability.throwIfNecessary();
} catch (ServiceResponseException e) {
if (e.getErrorCode() == ServiceError.ErrorMailRecipientNotFound) {
ret.put(emailAddresses.get(attendeesAvailabilityIndex), Optional.empty());
continue;
} else {
throw e;
}
}
FreeBusyStatus statusAtStart = FreeBusyStatus.FREE;
final List<CalendarEvent> calendarEvents = new ArrayList<>();
for (final microsoft.exchange.webservices.data.property.complex.availability.CalendarEvent calendarEvent : attendeeAvailability.getCalendarEvents()) {
if(start.compareTo(calendarEvent.getEndTime()) < 0 && calendarEvent.getStartTime().compareTo(start) <= 0){
switch (calendarEvent.getFreeBusyStatus()) {
case Busy:
statusAtStart = FreeBusyStatus.BUSY;
break;
case Free:
// do nothing
break;
case NoData:
// do nothing
break;
case OOF:
// do nothing
break;
case Tentative:
if(statusAtStart == FreeBusyStatus.FREE){
statusAtStart = FreeBusyStatus.TENTATIVE;
}
break;
}
}
if(start.compareTo(calendarEvent.getEndTime()) < 0 && calendarEvent.getStartTime().compareTo(end) < 0){
calendarEvents.add(
CalendarEvent.builder()
.start(calendarEvent.getStartTime())
.end(calendarEvent.getEndTime())
.status(legacyFreeBusyStatusToFreeBusyStatus(calendarEvent.getFreeBusyStatus()))
.location(calendarEvent.getDetails()==null?null:calendarEvent.getDetails().getLocation())
.subject(calendarEvent.getDetails()==null?null:calendarEvent.getDetails().getSubject())
.id(calendarEvent.getDetails()==null?null:calendarEvent.getDetails().getStoreId())
.build());
}
}
Date nextFree = null;
for(final Suggestion suggestion : results.getSuggestions()){
for(final TimeSuggestion timeSuggestion : suggestion.getTimeSuggestions()){
if(nextFree==null || nextFree.after(timeSuggestion.getMeetingTime())){
nextFree = timeSuggestion.getMeetingTime();
}
}
}
ret.put(emailAddresses.get(attendeesAvailabilityIndex), Optional.of(Availability.builder().statusAtStart(statusAtStart).nextFree(nextFree).calendarEvents(Collections.unmodifiableList(calendarEvents)).build()));
}
return Collections.unmodifiableMap(ret);
}
@Override
public Optional<Availability> getAvailability(@NonNull String emailAddress, @NonNull Date start, @NonNull Date end) {
Map<String, Optional<Availability>> ret = getAvailability(Collections.singletonList(emailAddress), start, end);
Assert.isTrue(ret.keySet().size()==1);
Assert.notNull(ret.get(emailAddress));
return ret.get(emailAddress);
}
@PostConstruct
private ExchangeService getExchangeService() {
final ExchangeService exchangeService = new ExchangeService();
exchangeService.setCredentials(new WebCredentials(exchangeConnectionProperties.getCredentials().getUsername(), exchangeConnectionProperties.getCredentials().getPassword(),exchangeConnectionProperties.getCredentials().getDomain()));
exchangeService.setUrl(exchangeConnectionProperties.getUri());
return exchangeService;
}
@SneakyThrows
@Override
@Cacheable(cacheNames="roomLists")
public Set<RoomList> getRoomLists() {
final Set<RoomList> roomLists = new HashSet<>();
final Set<String> addressesFromExchange = new HashSet<>();
try(ExchangeService exchangeService = getExchangeService()){
for(EmailAddress emailAddress : exchangeService.getRoomLists()){
roomLists.add(RoomList.builder().emailAddress(emailAddress.getAddress()).name(emailAddress.getName()).build());
addressesFromExchange.add(emailAddress.getAddress());
}
}
for(String emailAddress : exchangeConnectionProperties.getRoomLists().keySet()){
if(!addressesFromExchange.contains(emailAddress))
roomLists.add(RoomList.builder().emailAddress(emailAddress).name(emailAddress).build());
}
return Collections.unmodifiableSet(roomLists);
}
@SneakyThrows
@Override
@Cacheable(cacheNames="rooms")
public Optional<Set<Room>> getRooms(@NonNull String roomListEmailAddress) {
if(exchangeConnectionProperties.getRoomListAlias().containsKey(roomListEmailAddress)){
return getRooms(exchangeConnectionProperties.getRoomListAlias().get(roomListEmailAddress));
}
if(exchangeConnectionProperties.getRoomLists().containsKey(roomListEmailAddress)){
return Optional.of(
Collections.unmodifiableSet(
exchangeConnectionProperties.getRoomLists().get(roomListEmailAddress).stream().map(emailAddress ->
Room.builder().emailAddress(emailAddress).name(emailAddress).build())
.collect(Collectors.toSet())));
}else{
final Set<Room> roomLists = new HashSet<>();
Collection<EmailAddress> rooms;
try(ExchangeService exchangeService = getExchangeService()){
rooms=exchangeService.getRooms(new EmailAddress(roomListEmailAddress));
} catch (ServiceResponseException e) {
if (e.getErrorCode() == ServiceError.ErrorNameResolutionNoResults) {
return Optional.empty();
} else {
throw e;
}
}
for(EmailAddress emailAddress : rooms){
roomLists.add(Room.builder().emailAddress(emailAddress.getAddress()).name(emailAddress.getName()).build());
}
return Optional.of(Collections.unmodifiableSet(roomLists));
}
}
private static FreeBusyStatus legacyFreeBusyStatusToFreeBusyStatus(@NonNull LegacyFreeBusyStatus legacyFreeBusyStatus){
switch(legacyFreeBusyStatus){
case Busy:
return FreeBusyStatus.BUSY;
case Free:
return FreeBusyStatus.FREE;
case Tentative:
return FreeBusyStatus.TENTATIVE;
default:
return FreeBusyStatus.FREE;
}
}
@Override
@Cacheable(cacheNames="roomListAvailability")
public Optional<Map<Room, Optional<Availability>>> getRoomListAvailability(String roomListEmailAddress) {
Optional<Set<Room>> optionalRooms = getRooms(roomListEmailAddress);
Optional<Map<String, Room>> emailAddressToRoom = optionalRooms
.map(rooms -> rooms.stream().collect(Collectors.toMap(Room::getEmailAddress, Function.identity())));
if (emailAddressToRoom.isPresent()) {
return optionalRooms
.map(rooms -> rooms.stream().map(Room::getEmailAddress).collect(Collectors.toList()))
.map(emailAddresses -> getAvailability(emailAddresses, new Date(), new Date()))
.map(m -> m.entrySet().stream()
.collect(Collectors.toMap(entry -> emailAddressToRoom.get().get(entry.getKey()),
entry -> entry.getValue())));
} else {
return Optional.empty();
}
}
}