com.microsoft.exchange.impl.BaseExchangeCalendarDataDao.java Source code

Java tutorial

Introduction

Here is the source code for com.microsoft.exchange.impl.BaseExchangeCalendarDataDao.java

Source

/**
 * See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Board of Regents of the University of Wisconsin System
 * licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a
 * copy of the License at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package com.microsoft.exchange.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.Random;
import java.util.Set;

import javax.xml.bind.JAXBContext;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.apache.commons.lang.time.StopWatch;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.validator.routines.EmailValidator;
import org.joda.time.Interval;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.support.DataAccessUtils;
import org.springframework.util.CollectionUtils;

import com.microsoft.exchange.ExchangeDateUtils;
import com.microsoft.exchange.ExchangeRequestFactory;
import com.microsoft.exchange.ExchangeResponseUtils;
import com.microsoft.exchange.ExchangeWebServices;
import com.microsoft.exchange.exception.ExchangeExceededFindCountLimitRuntimeException;
import com.microsoft.exchange.exception.ExchangeInvalidUPNRuntimeException;
import com.microsoft.exchange.exception.ExchangeMissingEmailAddressRuntimeException;
import com.microsoft.exchange.exception.ExchangeRuntimeException;
import com.microsoft.exchange.messages.CreateFolder;
import com.microsoft.exchange.messages.CreateFolderResponse;
import com.microsoft.exchange.messages.CreateItem;
import com.microsoft.exchange.messages.CreateItemResponse;
import com.microsoft.exchange.messages.DeleteFolder;
import com.microsoft.exchange.messages.DeleteFolderResponse;
import com.microsoft.exchange.messages.DeleteItem;
import com.microsoft.exchange.messages.DeleteItemResponse;
import com.microsoft.exchange.messages.EmptyFolder;
import com.microsoft.exchange.messages.EmptyFolderResponse;
import com.microsoft.exchange.messages.FindFolder;
import com.microsoft.exchange.messages.FindFolderResponse;
import com.microsoft.exchange.messages.FindItem;
import com.microsoft.exchange.messages.FindItemResponse;
import com.microsoft.exchange.messages.GetFolder;
import com.microsoft.exchange.messages.GetFolderResponse;
import com.microsoft.exchange.messages.GetItem;
import com.microsoft.exchange.messages.GetItemResponse;
import com.microsoft.exchange.messages.GetServerTimeZones;
import com.microsoft.exchange.messages.GetServerTimeZonesResponse;
import com.microsoft.exchange.messages.ResolveNames;
import com.microsoft.exchange.messages.ResolveNamesResponse;
import com.microsoft.exchange.messages.UpdateItem;
import com.microsoft.exchange.messages.UpdateItemResponse;
import com.microsoft.exchange.types.ArrayOfRecipientsType;
import com.microsoft.exchange.types.BaseFolderIdType;
import com.microsoft.exchange.types.BaseFolderType;
import com.microsoft.exchange.types.BodyType;
import com.microsoft.exchange.types.BodyTypeType;
import com.microsoft.exchange.types.CalendarFolderType;
import com.microsoft.exchange.types.CalendarItemCreateOrDeleteOperationType;
import com.microsoft.exchange.types.CalendarItemType;
import com.microsoft.exchange.types.CalendarViewType;
import com.microsoft.exchange.types.ConnectingSIDType;
import com.microsoft.exchange.types.DefaultShapeNamesType;
import com.microsoft.exchange.types.DisposalType;
import com.microsoft.exchange.types.DistinguishedFolderIdNameType;
import com.microsoft.exchange.types.EmailAddressType;
import com.microsoft.exchange.types.ExtendedPropertyType;
import com.microsoft.exchange.types.FolderIdType;
import com.microsoft.exchange.types.FolderQueryTraversalType;
import com.microsoft.exchange.types.ItemIdType;
import com.microsoft.exchange.types.ItemType;
import com.microsoft.exchange.types.MessageType;
import com.microsoft.exchange.types.NonEmptyArrayOfItemChangesType;
import com.microsoft.exchange.types.SetItemFieldType;
import com.microsoft.exchange.types.SingleRecipientType;
import com.microsoft.exchange.types.TaskType;
import com.microsoft.exchange.types.TasksFolderType;
import com.microsoft.exchange.types.TimeZoneDefinitionType;

/**
 * Base class for interacting with {@link ExchangeWebServices}.
 * 
 * If you wish to get (or set) any {@link ExtendedPropertyType}s when retrieving
 * (or creating) items in Exchange you MUST provide your own implementation of {@link ExchangeRequestFactory}.
 * 
 * @author Collin Cudd
 */
public class BaseExchangeCalendarDataDao {

    //================================================================================
    // Properties 
    //================================================================================
    protected final Log log = LogFactory.getLog(this.getClass());
    private JAXBContext jaxbContext;
    private ExchangeWebServices webServices;
    private ExchangeRequestFactory requestFactory = new ExchangeRequestFactory();
    private ExchangeResponseUtils responseUtils = new ExchangeResponseUtilsImpl();
    private int maxRetries = 10;
    private static Random random = new Random();

    /**
     * This is the principal/username which is used when performing non-user actions like:
     *  {@link #getServerTimeZones(String, boolean)}, 
     *  {@link #resolveEmailAddresses(String)} and
     *  {@link #createEmailMessage(List, String, String, String, BodyTypeType, FolderIdType)}
     */
    private String adminUpn;
    /**
     * This property is used as the from address when sending mail.
     * @see #createEmailMessage(List, String, String, String, BodyTypeType, FolderIdType)
     */
    private String adminSendAs;

    //================================================================================
    // Getters/Setters 
    //================================================================================
    /**
     * See: {@link #adminSendAs}
     * @param adminSendAs the from address to use when sending email.
     */
    public void setAdminSendAs(String adminSendAs) {
        this.adminSendAs = adminSendAs;
    }

    /**
     * See: {@link #adminSendAs}
     * @return the from address used when sending email.
     */
    public String getAdminSendAs() {
        return this.adminSendAs;
    }

    /**
     * See: {@link #adminUpn}
     * @param adminUpn the adminUpn to set
     */
    public void setAdminUpn(String adminUpn) {
        this.adminUpn = adminUpn;
    }

    /**
     * See: {@link #adminUpn}
     * @return the admin user principal name.
     */
    public String getAdminUpn() {
        return this.adminUpn;
    }

    public int getMaxRetries() {
        return maxRetries;
    }

    public void setMaxRetries(int maxRetries) {
        this.maxRetries = maxRetries;
    }

    public ExchangeWebServices getWebServices() {
        return webServices;
    }

    /**
     * @param exchangeWebServices the exchangeWebServices to set
     */
    @Autowired(required = false)
    @Qualifier("ewsClient")
    public void setWebServices(ExchangeWebServices exchangeWebServices) {
        this.webServices = exchangeWebServices;
    }

    /**
     * @return the {@link ExchangeRequestFactory}
     */
    public ExchangeRequestFactory getRequestFactory() {
        return requestFactory;
    }

    /**
     * @param factory the requestFactory to set
     */
    @Autowired(required = false)
    public void setRequestFactory(ExchangeRequestFactory exchangeRequestFactory) {
        this.requestFactory = exchangeRequestFactory;
    }

    /**
     * @return the {@link ExchangeResponseUtils}
     */
    public ExchangeResponseUtils getResponseUtils() {
        return responseUtils;
    }

    @Autowired(required = false)
    public void setResponseUtils(ExchangeResponseUtils exchangeResponseUtils) {
        this.responseUtils = exchangeResponseUtils;
    }

    /**
     * @return the jaxbContext
     */
    public JAXBContext getJaxbContext() {
        return jaxbContext;
    }

    /**
     * @param jaxbContext the jaxbContext to set
     */
    @Autowired
    public void setJaxbContext(JAXBContext jaxbContext) {
        this.jaxbContext = jaxbContext;
    }

    /**
     * Function for generating an exponential backoff time. This method will
     * never return less than 1000L. This method also adds a small random delay
     * in an attempt to prevent threads from backing off/retrying in sync.
     * 
     * @param retryCount
     *            the number of previously failed attempts.
     * @return the number of ms to sleep for.
     */
    public static long getWaitTimeExp(int retryCount) {
        Long baseMultiplier = 1000L;
        long waitTime = ((long) Math.pow(2, retryCount) * baseMultiplier);
        long rand = ((long) random.nextInt(baseMultiplier.intValue())) + 1L;
        return waitTime + rand;
    }

    /**
     * @param upn
     */
    protected void setContextCredentials(String upn) {
        Validate.isTrue(StringUtils.isNotBlank(upn), "upn argument cannot be blank");
        ConnectingSIDType connectingSID = new ConnectingSIDType();
        connectingSID.setPrincipalName(upn);
        ThreadLocalImpersonationConnectingSIDSourceImpl.setConnectingSID(connectingSID);
    }

    //================================================================================
    // GetFolder
    //================================================================================      
    /**
     * Attempt to retrieve a {@link CalendarFolderType} from the Exchange server
     * for the specified {@code upn} and {@link FolderIdType}
     * 
     * @param upn
     * @param folderId
     * @return the {@link CalendarFolderType} if found, otherwise <code>null</code>
     */
    public CalendarFolderType getCalendarFolder(String upn, FolderIdType folderId) {
        BaseFolderType folder = getFolder(upn, folderId);
        CalendarFolderType calendarFolderType = null;
        if (folder instanceof CalendarFolderType) {
            calendarFolderType = (CalendarFolderType) folder;
        }
        return calendarFolderType;
    }

    /**
     * Attempt to retrieve a {@link TasksFolderType} from the Exchange server
     * for the specified {@code upn} and {@link FolderIdType}
     * 
     * @param upn
     * @param folderId
     * @return the {@link TasksFolderType} if found, otherwise <code>null</code>
     */
    public TasksFolderType getTaskFolder(String upn, FolderIdType folderId) {
        BaseFolderType folder = getFolder(upn, folderId);
        TasksFolderType taskFolderType = null;
        if (folder instanceof TasksFolderType) {
            taskFolderType = (TasksFolderType) folder;
        }
        return taskFolderType;
    }

    /**
     * Attempt to retrieve a {@link BaseFolderType} from the Exchange server
     * for the specified {@code upn} and {@link FolderIdType}
     * 
     * @param upn
     * @param folderId
     * @return the {@link BaseFolderType} if found, otherwise <code>null</code>
     */
    public BaseFolderType getFolder(String upn, FolderIdType folderIdType) {
        setContextCredentials(upn);
        GetFolder getFolderRequest = getRequestFactory().constructGetFolderById(folderIdType);
        GetFolderResponse getFolderResponse = getWebServices().getFolder(getFolderRequest);
        Set<BaseFolderType> response = getResponseUtils().parseGetFolderResponse(getFolderResponse);
        return DataAccessUtils.singleResult(response);
    }

    /**
     * Attempt to retrieve a {@link BaseFolderIdType} representing the primary item collection a {@link DistinguishedFolderIdNameType}
     * 
     * For instance, passing {@link DistinguishedFolderIdNameType#CALENDAR} to this function, the result should be the primary calendar folder.
     *  
     * @param upn
     * @param parent
     * @return
     */
    protected BaseFolderType getPrimaryFolder(String upn, DistinguishedFolderIdNameType parent) {
        setContextCredentials(upn);
        GetFolder getFolderRequest = getRequestFactory().constructGetFolderByDistinguishedName(parent);
        GetFolderResponse getFolderResponse = getWebServices().getFolder(getFolderRequest);
        Set<BaseFolderType> response = getResponseUtils().parseGetFolderResponse(getFolderResponse);
        return DataAccessUtils.singleResult(response);
    }

    /**
     * Obtain the primary calendar folder
     * 
     * @param upn
     * @return {@link BaseFolderIdType}
     */
    //TODO this should only return CalendarFolderType
    public BaseFolderType getPrimaryCalendarFolder(String upn) {
        return getPrimaryFolder(upn, DistinguishedFolderIdNameType.CALENDAR);
    }

    /**
     * Obtain the primary task folder
     * @param upn
     * @return {@link BaseFolderIdType}
     */
    //TODO this should only return TasksFolderType
    public BaseFolderType getPrimaryTaskFolder(String upn) {
        return getPrimaryFolder(upn, DistinguishedFolderIdNameType.TASKS);
    }

    /**
     * Obtain the primary folder and all sub-folders for the given {@link DistinguishedFolderIdNameType}
     * 
     * @param upn
     * @param parent
     * @return a never null but possibly empty {@link Set} of {@link BaseFolderIdType}
     */
    private Set<BaseFolderType> getFoldersByType(String upn, DistinguishedFolderIdNameType parent) {
        Set<BaseFolderType> folders = new HashSet<BaseFolderType>();
        BaseFolderType baseFolderType = getPrimaryFolder(upn, parent);
        if (null != baseFolderType) {
            folders.add(baseFolderType);
        }
        Set<BaseFolderType> seondaryFolders = getSecondaryFolders(upn, parent);
        if (!CollectionUtils.isEmpty(seondaryFolders)) {
            for (BaseFolderType b : seondaryFolders) {
                //TODO class comparison is not neccesary?
                if (baseFolderType.getClass().equals(b.getClass())) {
                    folders.add(b);
                }
            }
        }
        return folders;
    }

    /**
     * Obtain all sub-folders for given {@link DistinguishedFolderIdNameType}
     * @param upn
     * @param parent
     * @return
     */
    private Set<BaseFolderType> getSecondaryFolders(String upn, DistinguishedFolderIdNameType parent) {
        setContextCredentials(upn);
        FindFolder findFolderRequest = getRequestFactory().constructFindFolder(parent,
                DefaultShapeNamesType.ALL_PROPERTIES, FolderQueryTraversalType.DEEP, null);
        FindFolderResponse findFolderResponse = getWebServices().findFolder(findFolderRequest);
        return getResponseUtils().parseFindFolderResponse(findFolderResponse);
    }

    /**
     * Obtain all {@link BaseFolderType}s representing calendar collections
     * @param upn
     * @return
     */
    //TODO this method should only return Set<CalendarItemType>
    public Set<BaseFolderType> getAllCalendarFolders(String upn) {
        Validate.notEmpty(upn, "upn cannnot be empty");
        return getFoldersByType(upn, DistinguishedFolderIdNameType.CALENDAR);
    }

    /**
     * Obtain all {@link BaseFolderType}s representing Task collections
     * @param upn
     * @return
     */
    //TODO this method should only return Set<TasksItemType>
    public Set<BaseFolderType> getAllTaskFolders(String upn) {
        Validate.notEmpty(upn, "upn cannnot be empty");
        return getFoldersByType(upn, DistinguishedFolderIdNameType.TASKS);
    }

    /**
     * Obtain the {@link FolderIdType} for a {@link CalendarFolderType} given the display name of the calendar.
     * @param upn
     * @param calendarName
     * @return
     */
    //TODO this should definately not throw a RuntimeException
    public FolderIdType getCalendarFolderId(String upn, String calendarName) {
        Map<String, String> calendarFolderMap = getCalendarFolderMap(upn);
        if (!CollectionUtils.isEmpty(calendarFolderMap) && calendarFolderMap.containsValue(calendarName)) {
            for (String c_id : calendarFolderMap.keySet()) {
                String c_name = calendarFolderMap.get(c_id);
                if (calendarName.equals(c_name)) {
                    FolderIdType folderIdType = new FolderIdType();
                    folderIdType.setId(c_id);
                    return folderIdType;
                }
            }
        }
        throw new ExchangeRuntimeException("No calendar folder with name of '" + calendarName + "' for " + upn);
    }

    /**
     * Obtain all {@link CalendarFolderType}s from the exchange server and return a map
     * where the key represents the FolderId and the value represents the
     * {@link CalendarFolderType}s dispaly name
     * 
     * @param upn
     * @return
     */
    public Map<String, String> getCalendarFolderMap(String upn) {
        Map<String, String> calendarsMap = new HashMap<String, String>();
        Set<BaseFolderType> allCalendarFolders = getAllCalendarFolders(upn);
        log.debug("getCalendarFolderMap found " + allCalendarFolders.size());
        Integer i = 1;
        for (BaseFolderType folderType : allCalendarFolders) {
            String name = folderType.getDisplayName();
            String id = folderType.getFolderId().getId();
            log.debug("CalendarFolderMap " + upn + " " + i + ": " + name);
            calendarsMap.put(id, name);
            i++;
        }
        return calendarsMap;
    }

    /**
     * Obtain all {@link TasksFolderType}s from the exchange server and return a map
     * where the key represents the FolderId and the value represents the
     * {@link TasksFolderType}s dispaly name
     * 
     * @param upn
     * @return
     */
    public Map<String, String> getTaskFolderMap(String upn) {
        Map<String, String> taskFolderMap = new HashMap<String, String>();
        Set<BaseFolderType> allTaskFolders = getAllTaskFolders(upn);
        for (BaseFolderType b : allTaskFolders) {
            String displayName = b.getDisplayName();
            String id = b.getFolderId().getId();
            taskFolderMap.put(id, displayName);
        }
        return taskFolderMap;
    }

    //================================================================================
    // FindItem
    //================================================================================   

    /**
     * Find all {@link ItemIdType} within the primary {@link CalendarFolderType} between {@code startDate} and {@code endDate}
     * @param upn
     * @param startDate
     * @param endDate
     * @return a never null but possibly empty {@link Set} of {@link ItemIdType}
     */
    public Set<ItemIdType> findCalendarItemIds(String upn, Date startDate, Date endDate) {
        return findCalendarItemIdsInternal(upn, startDate, endDate, null, 0);
    }

    /**
     * Find all {@link ItemIdType} within the specified {@link FolderIdType}s between {@code startDate} and {@code endDate}
     * @param upn
     * @param startDate
     * @param endDate
     * @return a never null but possibly empty {@link Set} of {@link ItemIdType}
     */
    public Set<ItemIdType> findCalendarItemIds(String upn, Date startDate, Date endDate,
            Collection<FolderIdType> calendarIds) {
        return findCalendarItemIdsInternal(upn, startDate, endDate, calendarIds, 0);
    }

    /**
     * This method issues a {@link FindItem} request using a {@link CalendarViewType} to obtain identifiers for all {@link CalendarItemType}s between {@code startDate} and {@code endDate}
     * 
     * Note: CalendarView element returns single calendar items and all occurrences.  In other words, this method expands recurrence for you.
     * 
     * @see <a href='http://msdn.microsoft.com/en-us/library/office/aa566107(v=exchg.140).aspx'>FindItem Operation</a>
     * 
     * @param upn
     * @param startDate
     * @param endDate
     * @param calendarIds - if omitted the primary calendar folder will be targeted
     * @param depth
     * @return a never null but possibly empty {@link Set} of {@link ItemIdType}
     */
    private Set<ItemIdType> findCalendarItemIdsInternal(String upn, Date startDate, Date endDate,
            Collection<FolderIdType> calendarIds, int depth) {
        Validate.isTrue(StringUtils.isNotBlank(upn), "upn argument cannot be blank");
        Validate.notNull(startDate, "startDate argument cannot be null");
        Validate.notNull(endDate, "endDate argument cannot be null");
        //if folderIds is empty the primary calendar folder will be targeted
        int newDepth = depth + 1;
        if (depth > getMaxRetries()) {
            throw new ExchangeRuntimeException("findCalendarItemIdsInternal(upn=" + upn + ",startDate=" + startDate
                    + ",+endDate=" + endDate + ",...) failed " + getMaxRetries() + " consecutive attempts.");
        } else {
            setContextCredentials(upn);
            FindItem request = getRequestFactory().constructCalendarViewFindCalendarItemIdsByDateRange(startDate,
                    endDate, calendarIds);
            try {
                FindItemResponse response = getWebServices().findItem(request);
                return getResponseUtils().parseFindItemIdResponseNoOffset(response);

            } catch (ExchangeInvalidUPNRuntimeException e0) {
                log.warn("findCalendarItemIdsInternal(upn=" + upn + ",startDate=" + startDate + ",+endDate="
                        + endDate
                        + ",...) ExchangeInvalidUPNRuntimeException.  Attempting to resolve valid upn... - failure #"
                        + newDepth);

                String resolvedUpn = resolveUpn(upn);
                if (StringUtils.isNotBlank(resolvedUpn) && (!resolvedUpn.equalsIgnoreCase(upn))) {
                    return findCalendarItemIdsInternal(resolvedUpn, startDate, endDate, calendarIds, newDepth);
                } else {
                    //rethrow
                    throw e0;
                }
            } catch (ExchangeExceededFindCountLimitRuntimeException e1) {
                log.warn("findCalendarItemIdsInternal(upn=" + upn + ",startDate=" + startDate + ",+endDate="
                        + endDate + ",...) ExceededFindCountLimit splitting request and trying again. - failure #"
                        + newDepth);
                Set<ItemIdType> foundItems = new HashSet<ItemIdType>();
                List<Interval> intervals = ExchangeDateUtils.generateIntervals(startDate, endDate);
                for (Interval i : intervals) {
                    foundItems.addAll(findCalendarItemIdsInternal(upn, i.getStart().toDate(), i.getEnd().toDate(),
                            calendarIds, newDepth));
                }
                return foundItems;
            } catch (ExchangeRuntimeException e2) {
                long backoff = getWaitTimeExp(newDepth);
                log.warn("findCalendarItemIdsInternal(upn=" + upn + ",startDate=" + startDate + ",+endDate="
                        + endDate + ",...) - failure #" + newDepth + ". Sleeping for " + backoff + " before retry. "
                        + e2.getMessage());
                try {
                    Thread.sleep(backoff);
                } catch (InterruptedException e1) {
                    log.warn("InterruptedException=" + e1);
                }
                return findCalendarItemIdsInternal(upn, startDate, endDate, calendarIds, newDepth);
            }
        }
    }

    /**
     * Locate the {@link ItemIdType}s within the specified folder(s).
     * This method will return a maximum of {@link BaseExchangeCalendarDataDao#getRequestFactory()}.getMaxFindItems()
     * 
     * @param upn
     * @param folderIds
     * @return {@link Set} of {@link ItemIdType}
     */
    public Set<ItemIdType> findItemIds(String upn, Collection<FolderIdType> folderIds) {
        FindItem request = getRequestFactory().constructFindFirstItemIdSet(folderIds);
        Pair<Set<ItemIdType>, Integer> pair = findItemIdsInternal(upn, request, 0);
        return pair.getLeft();
    }

    /**
     * Obtain all {@link ItemIdType}s within a specified {@link FolderIdType} by
     * repeatedly callling {@link FindItem} and paging the results
     * 
     * @param upn
     * @param folderIds
     * @return a never null but possibly empty {@link Set} of {@link ItemIdType}
     */
    public Set<ItemIdType> findAllItemIds(String upn, Collection<FolderIdType> folderIds) {
        FindItem request = getRequestFactory().constructFindFirstItemIdSet(folderIds);
        Pair<Set<ItemIdType>, Integer> pair = findItemIdsInternal(upn, request, 0);
        Set<ItemIdType> itemIds = pair.getLeft();
        Integer nextOffset = pair.getRight();
        while (nextOffset > 0) {
            request = getRequestFactory().constructFindNextItemIdSet(nextOffset, folderIds);
            pair = findItemIdsInternal(upn, request, 0);
            itemIds.addAll(pair.getLeft());
            nextOffset = pair.getRight();
        }
        return itemIds;
    }

    /**
     * 
     * @param upn
     * @param request
     * @param depth
     * @return
     */
    private Pair<Set<ItemIdType>, Integer> findItemIdsInternal(String upn, FindItem request, int depth) {
        Validate.isTrue(StringUtils.isNotBlank(upn), "upn argument cannot be blank");
        Validate.notNull(request, "request argument cannot be null");
        int newDepth = depth + 1;
        if (depth > getMaxRetries()) {
            throw new ExchangeRuntimeException("findCalendarItemIdsInternal(upn=" + upn + ",request=" + request
                    + ",...) failed " + getMaxRetries() + " consecutive attempts.");
        } else {
            setContextCredentials(upn);
            try {
                FindItemResponse response = getWebServices().findItem(request);
                Pair<Set<ItemIdType>, Integer> parsed = getResponseUtils().parseFindItemIdResponse(response);

                return parsed;
            } catch (ExchangeInvalidUPNRuntimeException e0) {
                log.warn("findCalendarItemIdsInternal(upn=" + upn + ",request=" + request
                        + ",...) ExchangeInvalidUPNRuntimeException.  Attempting to resolve valid upn... - failure #"
                        + newDepth);

                String resolvedUpn = resolveUpn(upn);
                if (StringUtils.isNotBlank(resolvedUpn) && (!resolvedUpn.equalsIgnoreCase(upn))) {
                    return findItemIdsInternal(resolvedUpn, request, newDepth);
                } else {
                    //rethrow
                    throw e0;
                }
                //         }catch(ExchangeExceededFindCountLimitRuntimeException e1) {
                //            log.warn("findCalendarItemIdsInternal(upn="+upn+",request="+request+",...) ExceededFindCountLimit splitting request and trying again. - failure #"+newDepth);
                //            Set<ItemIdType> foundItems = new HashSet<ItemIdType>();
                //            List<Interval> intervals = DateHelp.generateIntervals(startDate, endDate);
                //            for(Interval i: intervals) {
                //               foundItems.addAll(findCalendarItemIdsInternal(upn,i.getStart().toDate(), i.getEnd().toDate(),calendarIds,newDepth));
                //            }
                //            return foundItems;
            } catch (ExchangeRuntimeException e2) {
                long backoff = getWaitTimeExp(newDepth);
                log.warn("findCalendarItemIdsInternal(upn=" + upn + ",request=" + request + ",...) - failure #"
                        + newDepth + ". Sleeping for " + backoff + " before retry. " + e2.getMessage());
                try {
                    Thread.sleep(backoff);
                } catch (InterruptedException e1) {
                    log.warn("InterruptedException=" + e1);
                }
                return findItemIdsInternal(upn, request, newDepth);
            }
        }

    }

    //================================================================================
    // GetItem
    //================================================================================   

    /**
     * Obtain all {@link CalendarItemType}s found within the specified
     * {@code calendarFolderId} which intersect the date range from
     * {@code startDate} to {@code endDate}
     * 
     * @param upn
     * @param startDate
     * @param endDate
     * @param calendarFolderId
     * @return a never null but possibly empty {@link Collection} of {@link CalendarItemType}
     */
    public Collection<CalendarItemType> getCalendarItems(String upn, Date startDate, Date endDate,
            Collection<FolderIdType> calendarFolderId) {
        Set<ItemIdType> itemIds = findCalendarItemIds(upn, startDate, endDate, calendarFolderId);
        return getCalendarItems(upn, itemIds);
    }

    /**
     * Obtain a {@link CalendarItemType} for each {@link ItemIdType} specified.
     * @param upn
     * @param itemIds
     * @return a never null but possibly empty {@link Collection} of {@link CalendarItemType}
     */
    public Collection<CalendarItemType> getCalendarItems(String upn, Collection<ItemIdType> itemIds) {
        Set<CalendarItemType> calendarItems = new HashSet<CalendarItemType>();
        Set<ItemType> items = getItemsInternal(upn, itemIds, 0);
        for (ItemType item : items) {
            if (item instanceof CalendarItemType) {
                calendarItems.add((CalendarItemType) item);
            } else {
                log.warn("non-calendarItemType will be excluded from result set");
            }
        }
        return calendarItems;
    }

    /**
     * Get the {@link CalendarItemType} for the specified {@link ItemIdType} 
     * @param upn
     * @param itemId
     * @return a {@link CalendarItemType} if found, <code>null</code> otherwise
     */
    public CalendarItemType getCalendarItem(String upn, ItemIdType itemId) {
        Collection<CalendarItemType> items = getCalendarItems(upn, Collections.singleton(itemId));
        return DataAccessUtils.singleResult(items);
    }

    /**
     * Obtain a {@link TaskType} for each {@link ItemIdType} specified.
     * @param upn
     * @param itemIds
     * @return a never null but possibly empty {@link Collection} of {@link TaskType}
     */
    public Set<TaskType> getTaskItems(String upn, Set<ItemIdType> itemIds) {
        Set<TaskType> taskItems = new HashSet<TaskType>();
        Set<ItemType> items = getItemsInternal(upn, itemIds, 0);
        for (ItemType item : items) {
            if (item instanceof TaskType) {
                taskItems.add((TaskType) item);
            } else {
                log.warn("non-TaskType will be excluded from result set");
            }
        }
        return taskItems;
    }

    /**
     * 
     * @param upn the UserPrincipalName
     * @param itemIds the {@link ItemIdType}s
     * @param depth the number of recursive calls.
     * @return a never null but possbly empty {@link Set} of {@link ItemType}s
     */
    private Set<ItemType> getItemsInternal(String upn, Collection<ItemIdType> itemIds, int depth) {
        Set<ItemType> results = new HashSet<ItemType>();
        Validate.isTrue(StringUtils.isNotBlank(upn), "upn argument cannot be blank");
        if (CollectionUtils.isEmpty(itemIds)) {
            return results;
        }

        int newDepth = depth + 1;
        if (depth > getMaxRetries()) {
            throw new ExchangeRuntimeException(
                    "getItemsInternal(upn=" + upn + ",...) failed " + getMaxRetries() + " consecutive attempts.");
        } else {
            setContextCredentials(upn);
            GetItem request = getRequestFactory().constructGetItems(itemIds);
            try {
                GetItemResponse response = getWebServices().getItem(request);
                return getResponseUtils().parseGetItemResponse(response);
            } catch (ExchangeRuntimeException e) {
                long backoff = getWaitTimeExp(newDepth);
                log.warn("getItemsInternal - failure #" + newDepth + ". Sleeping for " + backoff + " before retry. "
                        + e.getMessage());
                try {
                    Thread.sleep(backoff);
                } catch (InterruptedException e1) {
                    log.warn("InterruptedException=" + e1);
                }
                return getItemsInternal(upn, itemIds, newDepth);
            }
        }
    }

    //================================================================================
    // CreateItem
    //================================================================================   

    /**
     * Create the {@link CalendarItemType} on the exchange server
     * @param upn
     * @param calendarItem
     * @param depth
     * @return {@link ItemIdType}
     */
    private ItemIdType createCalendarItemInternal(String upn, CalendarItemType calendarItem,
            FolderIdType calendarFolderId, int depth) {
        Validate.notNull(calendarItem, "calendarItem argument cannot be empty");
        int newDepth = depth + 1;
        if (depth > getMaxRetries()) {
            throw new ExchangeRuntimeException("createCalendarItemInternal(upn=" + upn + ",...) failed "
                    + getMaxRetries() + " consecutive attempts.");
        } else {
            setContextCredentials(upn);
            Set<CalendarItemType> singleton = Collections.singleton(calendarItem);
            CreateItem request = getRequestFactory().constructCreateCalendarItem(singleton, calendarFolderId);
            try {
                CreateItemResponse response = getWebServices().createItem(request);
                Set<ItemIdType> createdCalendarItems = getResponseUtils().parseCreateItemResponse(response);
                return DataAccessUtils.singleResult(createdCalendarItems);
            } catch (ExchangeRuntimeException e) {
                long backoff = getWaitTimeExp(newDepth);
                log.warn("createCalendarItemInternal - failure #" + newDepth + ". Sleeping for " + backoff
                        + " before retry. " + e.getMessage());
                try {
                    Thread.sleep(backoff);
                } catch (InterruptedException e1) {
                    log.warn("InterruptedException=" + e1);
                }
                return createCalendarItemInternal(upn, calendarItem, calendarFolderId, newDepth);
            }
        }

    }

    /**
     * Create the {@link CalendarItemType} on the exchange server
     * @param upn
     * @param calendarItem
     * @return {@link ItemIdType}
     */
    public ItemIdType createCalendarItem(String upn, CalendarItemType calendarItem) {
        return createCalendarItem(upn, calendarItem, null);
    }

    public ItemIdType createCalendarItem(String upn, CalendarItemType calendarItem, FolderIdType calendarFolderId) {
        return createCalendarItemInternal(upn, calendarItem, calendarFolderId, 0);
    }

    /**
     * Create a {@link CalendarFolderType} with the specified {@code displayName}
     * @param upn
     * @param displayName
     * @return {@link FolderIdType}
     */
    public FolderIdType createCalendarFolder(String upn, String displayName) {
        setContextCredentials(upn);
        log.debug("createCalendarFolder upn=" + upn + ", displayName=" + displayName);
        CreateFolder createCalendarFolderRequest = getRequestFactory().constructCreateCalendarFolder(displayName,
                null);
        CreateFolderResponse createFolderResponse = getWebServices().createFolder(createCalendarFolderRequest);
        Set<FolderIdType> folders = getResponseUtils().parseCreateFolderResponse(createFolderResponse);
        return DataAccessUtils.singleResult(folders);
    }

    /**
     * Create and send an Email (i.e. {@link MessageType}) 
     * @param recips - addresses to send the message to
     * @param replyTo - a single email address, to whom the reply will be addressed
     * @param subject - the email messages subject
     * @param messageBody
     * @param bodyType 
     * @param folderIdType
     * @return {@link ItemIdType}
     */
    public ItemIdType createEmailMessage(List<String> recips, String replyTo, String subject, String messageBody,
            BodyTypeType bodyType, FolderIdType folderIdType) {
        List<MessageType> messages = new ArrayList<MessageType>();
        MessageType messageType = new MessageType();

        //set one or more recipients to receive the message
        ArrayOfRecipientsType arrayOfRecips = new ArrayOfRecipientsType();
        for (String r : recips) {
            EmailAddressType emailAddressType = new EmailAddressType();
            emailAddressType.setEmailAddress(r);
            arrayOfRecips.getMailboxes().add(emailAddressType);
        }
        messageType.setToRecipients(arrayOfRecips);

        //set the replyTo address
        if (StringUtils.isNotBlank(replyTo)) {
            ArrayOfRecipientsType arrayOfReplyTos = new ArrayOfRecipientsType();
            SingleRecipientType fromType = new SingleRecipientType();
            EmailAddressType fromAddressType = new EmailAddressType();
            fromAddressType.setEmailAddress(replyTo);
            fromType.setMailbox(fromAddressType);
            arrayOfReplyTos.getMailboxes().add(fromAddressType);
            messageType.setReplyTo(arrayOfReplyTos);
        }

        //set the from address
        String from = getAdminSendAs();
        if (StringUtils.isNotBlank(from)) {
            SingleRecipientType fromType = new SingleRecipientType();
            EmailAddressType fromAddressType = new EmailAddressType();
            fromAddressType.setEmailAddress(from);
            fromType.setMailbox(fromAddressType);
            messageType.setFrom(fromType);
        }
        //set the message body
        BodyType body = new BodyType();
        body.setBodyType(bodyType);
        body.setValue(messageBody);
        messageType.setBody(body);
        //set the subjec
        messageType.setSubject(subject);
        //message set as not read
        messageType.setIsRead(false);
        //add the message
        messages.add(messageType);

        //in this context we are impersonating an admin with send as rights for admin.sendas
        setContextCredentials(getAdminUpn());
        CreateItem request = getRequestFactory().constructCreateMessageItem(messages, folderIdType);
        CreateItemResponse response = getWebServices().createItem(request);
        Set<ItemIdType> items = getResponseUtils().parseCreateItemResponse(response);
        return DataAccessUtils.singleResult(items);
    }

    //================================================================================
    // DeleteItem
    //================================================================================   
    private boolean deleteCalendarItemsInternal(String upn, Collection<ItemIdType> itemIds, int depth) {
        Validate.isTrue(StringUtils.isNotBlank(upn), "upn argument cannot be blank");
        Validate.notEmpty(itemIds, "itemIds argument cannot be empty");

        int newDepth = depth + 1;
        if (depth > getMaxRetries()) {
            throw new ExchangeRuntimeException("deleteCalendarItemsInternal(upn=" + upn + ",...) failed "
                    + getMaxRetries() + " consecutive attempts.");
        } else {
            setContextCredentials(upn);
            DeleteItem request = getRequestFactory().constructDeleteCalendarItems(itemIds);
            try {
                DeleteItemResponse response = getWebServices().deleteItem(request);
                boolean success = getResponseUtils().confirmSuccess(response);
                return success;

            } catch (ExchangeRuntimeException e) {
                long backoff = getWaitTimeExp(newDepth);
                log.warn("deleteCalendarItemsInternal - failure #" + newDepth + ". Sleeping for " + backoff
                        + " before retry. " + e.getMessage());
                try {
                    Thread.sleep(backoff);
                } catch (InterruptedException e1) {
                    log.warn("InterruptedException=" + e1);
                }
                return deleteCalendarItemsInternal(upn, itemIds, newDepth);
            }
        }
    }

    /**
     * Delete {@link CalendarItemType}s for the user.
     * @param upn - the userPrincipalName identifies the user to delete {@link CalendarItemType}s for.
     * @param itemIds - the {@link ItemIdType}s for the {@link CalendarItemType}s to delete.
     * 
     * @see ExchangeRequestFactory#constructDeleteCalendarItems(Collection, DisposalType, CalendarItemCreateOrDeleteOperationType)
     * @return
     */
    public boolean deleteCalendarItems(String upn, Collection<ItemIdType> itemIds) {
        return deleteCalendarItemsInternal(upn, itemIds, 0);
    }

    //================================================================================
    // ResolveNames
    //================================================================================   

    /**
     * Search the exchange server for any contacts with a name similar to
     * {@code alias} and return all corresponding SMTP addresses
     * 
     * @param alias
     * @return
     */
    public Set<String> resolveEmailAddresses(String alias) {
        Set<String> smtpAddresses = Collections.emptySet();
        Validate.isTrue(StringUtils.isNotBlank(alias), "alias argument cannot be blank");
        setContextCredentials(getAdminUpn());
        ResolveNames request = getRequestFactory().constructResolveNames(alias);
        ResolveNamesResponse response = getWebServices().resolveNames(request);
        try {
            smtpAddresses = getResponseUtils().parseResolveNamesResponse(response);
        } catch (ExchangeMissingEmailAddressRuntimeException e) {
            request = getRequestFactory().constructResolveNamesWithDistinguishedFolderId(alias);
            response = getWebServices().resolveNames(request);
            smtpAddresses = getResponseUtils().parseResolveNamesResponse(response);
        }
        return smtpAddresses;
    }

    /**
     * Search the exchange server for any contacts with an email address that matches
     * {@code emailAddress} and return only the UserPrincipalName
     * 
     * @param alias
     * @return a {@link String} representing the UPN
     */
    public String resolveUpn(String emailAddress) {
        Validate.isTrue(StringUtils.isNotBlank(emailAddress), "emailAddress argument cannot be blank");
        Validate.isTrue(EmailValidator.getInstance().isValid(emailAddress), "emailAddress argument must be valid");

        emailAddress = ExchangeRequestFactory.SMTP + emailAddress;
        Map<BaseFolderType, String> resultMap = new HashMap<BaseFolderType, String>();
        Set<String> addresses = resolveEmailAddresses(emailAddress);
        for (String addr : addresses) {
            try {
                BaseFolderType primaryCalendarFolder = getPrimaryCalendarFolder(addr);
                if (null == primaryCalendarFolder) {
                    throw new ExchangeRuntimeException("CALENDAR NOT FOUND");
                } else {
                    resultMap.put(primaryCalendarFolder, addr);
                }
            } catch (RuntimeException e) {
                log.debug("resolveUpn -- " + addr + " NOT VALID. " + e.getMessage());
            }
        }
        if (CollectionUtils.isEmpty(resultMap)) {
            throw new ExchangeRuntimeException("resolveUpn(" + emailAddress + ") failed -- no results.");
        } else {
            if (resultMap.isEmpty()) {
                throw new ExchangeRuntimeException("resolveUpn(" + emailAddress + ") failed -- multiple results.");
            } else {
                //just return the first entry.
                BaseFolderType key = resultMap.keySet().iterator().next();
                return resultMap.get(key);
            }
        }
    }

    //================================================================================
    // ServerTimeZones
    //================================================================================
    /**
     * Get {@link TimeZoneDefinitionType}s from the Exchange Server
     * @param tzid - if specified the server will only return a single matching {@link TimeZoneDefinitionType}
     * @param fullTimeZoneData -
     * @return a never null but possibly empty {@link Set} of {@link TimeZoneDefinitionType}
     */
    public Set<TimeZoneDefinitionType> getServerTimeZones(String tzid, boolean fullTimeZoneData) {
        GetServerTimeZones request = getRequestFactory().constructGetServerTimeZones(tzid, fullTimeZoneData);
        setContextCredentials(getAdminUpn());
        GetServerTimeZonesResponse response = getWebServices().getServerTimeZones(request);
        return getResponseUtils().parseGetServerTimeZonesResponse(response);
    }

    /**
     * Get all available {@link TimeZoneDefinitionType}s from the Exchange Server
     * @param fullTimeZoneData
     * @return a never null but possibly empty {@link Set} of {@link TimeZoneDefinitionType}
     */
    public Set<TimeZoneDefinitionType> getServerTimeZones(boolean fullTimeZoneData) {
        return getServerTimeZones(null, fullTimeZoneData);
    }

    /**
     * Get the {@link TimeZoneDefinitionType} with a timeZoneId of {@code tzid}
     * @param tzid
     * @param fullTimeZoneData
     * @return {@link TimeZoneDefinitionType} if found, <code>null</code> otherwise
     */
    public TimeZoneDefinitionType getServerTimeZone(String tzid, boolean fullTimeZoneData) {
        Set<TimeZoneDefinitionType> serverTimeZones = getServerTimeZones(tzid, fullTimeZoneData);
        return DataAccessUtils.singleResult(serverTimeZones);
    }

    //================================================================================
    // EmptyFolder
    //================================================================================   
    /**
     * Attempt to find items within the specified {@code folderId}.
     * @param upn
     * @param folderId
     * @return true only if the folder is completely empty
     */
    public boolean isEmpty(String upn, FolderIdType folderId) {
        Set<ItemIdType> itemIds = findItemIds(upn, Collections.singleton(folderId));
        return CollectionUtils.isEmpty(itemIds);
    }

    /**
     * The EmptyFolder operation empties folders in a mailbox. 
     * Optionally, this operation enables you to delete the subfolders of the specified folder. 
     * When a subfolder is deleted, the subfolder and the messages within the subfolder are deleted. 
     * 
     * *Note this method does not work for calendar or search folders: ERROR_CANNOT_EMPTY_FOLDER ... Emptying the calendar folder or search folder isn't permitted.
     * 
     * 
     * @param upn
     * @param folderId
     * @return
     */
    public boolean emptyFolder(String upn, boolean deleteSubFolders, BaseFolderIdType folderId) {
        EmptyFolder request = getRequestFactory().constructEmptyFolder(deleteSubFolders,
                Collections.singleton(folderId));
        setContextCredentials(upn);
        EmptyFolderResponse response = getWebServices().emptyFolder(request);
        return getResponseUtils().parseEmptyFolderResponse(response);
    }

    /**
     * Deleting a calendarFolder with many (1k+) items is a problem.  You will always be throttled because the FindItemCount is 1000 and not configurable in Exchange Online.
     * More info on throttling http://msdn.microsoft.com/en-us/library/office/jj945066(v=exchg.150).aspx
     * 
     * This method will never attempt to delete more than 500 items at once.
     * 
     * @param upn
     * @param folderId
     * @return
     */
    public boolean emptyCalendarFolder(String upn, FolderIdType folderId) {
        Integer deleteRequestCount = 1;
        Set<ItemIdType> itemIds = findItemIds(upn, Collections.singleton(folderId));
        while (!itemIds.isEmpty()) {
            List<ItemIdType> itemIdList = new ArrayList<ItemIdType>(itemIds);
            if (itemIdList.size() > 250) {
                itemIdList = itemIdList.subList(0, 250);
            }
            StopWatch stopWatch = new StopWatch();
            stopWatch.start();
            log.info("emptyCalendarFolder(upn=" + upn + ") #" + deleteRequestCount + " deleting "
                    + itemIdList.size() + " calendar items");
            boolean result = deleteCalendarItems(upn, itemIdList);
            log.info("emptyCalendarFolder(upn=" + upn + ") #" + deleteRequestCount + " "
                    + (result ? "Success" : "Failure") + " in " + stopWatch);
            itemIds = findItemIds(upn, Collections.singleton(folderId));
            deleteRequestCount++;
        }
        return true;
    }

    public boolean purgeCancelledCalendarItems(String upn, FolderIdType folderId) {
        FindItem request = getRequestFactory()
                .constructIndexedPageViewFindItemCancelledCalendarItemIds(Collections.singleton(folderId));
        FindItemResponse response = getWebServices().findItem(request);
        Pair<Set<ItemIdType>, Integer> results = getResponseUtils().parseFindItemIdResponse(response);
        Set<ItemIdType> itemIds = results.getLeft();
        Integer nextOffset = results.getRight();
        while (itemIds.size() > 0) {
            if (deleteCalendarItems(upn, itemIds)) {
                if (nextOffset > 0) {
                    request = getRequestFactory().constructIndexedPageViewFindItemCancelledCalendarItemIds(
                            nextOffset, Collections.singleton(folderId));
                    response = getWebServices().findItem(request);
                    results = getResponseUtils().parseFindItemIdResponse(response);

                    itemIds = results.getLeft();
                    nextOffset = results.getRight();
                }
            } else {
                return false;
            }
        }
        return true;
    }

    //================================================================================
    // DeleteFolder
    //================================================================================   
    /**
     * Delete the specified {@code folderId} and all items contained wihtin it.
     * 
     * @param upn
     * @param folderId
     * @return
     */
    public boolean deleteCalendarFolder(String upn, FolderIdType folderId) {
        boolean empty = emptyCalendarFolder(upn, folderId);
        if (empty) {
            return deleteFolder(upn, folderId);
        }
        return false;
    }

    /**
     * First check the calendar for existing items, if the calendar folder is empty then delete it.
     * @param upn
     * @param folderId
     * @return - True if the calender folder was deleted, false otherwise
     */
    public boolean deleteEmptyCalendarFolder(String upn, FolderIdType folderId) {
        return isEmpty(upn, folderId) ? deleteFolder(upn, folderId) : false;
    }

    /**
     * Delete a calendar folder
     * @param upn - the user 
     * @param disposalType - how the deletion is performed
     * @param folderId - the folder to delete
     * @return
     */
    public boolean deleteFolder(String upn, BaseFolderIdType folderId) {
        DeleteFolder request = getRequestFactory().constructDeleteFolder(folderId);
        setContextCredentials(upn);
        DeleteFolderResponse response = getWebServices().deleteFolder(request);
        return getResponseUtils().parseDeleteFolderResponse(response);
    }

    /**
     * Force the exchange server to update the {@link CalendarItemType}s subject field
       * @param upn
     * @param c
     * @return- true if the update succeded
     */
    public boolean updateCalendarItemSetLegacyFreeBusy(String upn, CalendarItemType c) {
        boolean itemUpdated = false;
        SetItemFieldType setField = getRequestFactory().constructSetCalendarItemLegacyFreeBusy(c);
        NonEmptyArrayOfItemChangesType changes = getRequestFactory().constructUpdateCalendarItemChanges(c,
                Collections.singleton(setField));
        UpdateItem request = getRequestFactory().constructUpdateCalendarItem(c, changes);
        UpdateItemResponse response = getWebServices().updateItem(request);
        Set<ItemIdType> itemIds = getResponseUtils().parseUpdateItemResponse(response);
        if (!CollectionUtils.isEmpty(itemIds)) {
            ItemIdType itemId = DataAccessUtils.singleResult(itemIds);
            itemUpdated = c.getItemId().getId().equals(itemId.getId());
        }
        return itemUpdated;
    }
}