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

Java tutorial

Introduction

Here is the source code for com.microsoft.exchange.impl.ExchangeEventConverterImpl.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.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import javax.xml.datatype.XMLGregorianCalendar;

import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.Date;
import net.fortuna.ical4j.model.DateTime;
import net.fortuna.ical4j.model.Dur;
import net.fortuna.ical4j.model.Parameter;
import net.fortuna.ical4j.model.ParameterList;
import net.fortuna.ical4j.model.TextList;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.model.component.VTimeZone;
import net.fortuna.ical4j.model.component.VToDo;
import net.fortuna.ical4j.model.parameter.Cn;
import net.fortuna.ical4j.model.parameter.CuType;
import net.fortuna.ical4j.model.parameter.PartStat;
import net.fortuna.ical4j.model.parameter.Role;
import net.fortuna.ical4j.model.property.Attendee;
import net.fortuna.ical4j.model.property.Categories;
import net.fortuna.ical4j.model.property.Clazz;
import net.fortuna.ical4j.model.property.DtEnd;
import net.fortuna.ical4j.model.property.DtStamp;
import net.fortuna.ical4j.model.property.DtStart;
import net.fortuna.ical4j.model.property.Duration;
import net.fortuna.ical4j.model.property.Location;
import net.fortuna.ical4j.model.property.Organizer;
import net.fortuna.ical4j.model.property.Priority;
import net.fortuna.ical4j.model.property.Status;
import net.fortuna.ical4j.model.property.Summary;
import net.fortuna.ical4j.model.property.Transp;
import net.fortuna.ical4j.model.property.Uid;
import net.fortuna.ical4j.model.property.XProperty;

import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.Validate;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.CollectionUtils;

import com.microsoft.exchange.ExchangeEventConverter;
import com.microsoft.exchange.exception.ExchangeEventConverterException;
import com.microsoft.exchange.ical.model.EmailAddressMailboxType;
import com.microsoft.exchange.ical.model.EmailAddressRoutingType;
import com.microsoft.exchange.ical.model.ExchangeEndTimeZoneProperty;
import com.microsoft.exchange.ical.model.ExchangeStartTimeZoneProperty;
import com.microsoft.exchange.ical.model.ExchangeTimeZoneProperty;
import com.microsoft.exchange.ical.model.ItemTypeChangeKey;
import com.microsoft.exchange.ical.model.ItemTypeItemId;
import com.microsoft.exchange.ical.model.ItemTypeParentFolderChangeKey;
import com.microsoft.exchange.ical.model.ItemTypeParentFolderId;
import com.microsoft.exchange.ical.model.PathToExtendedFieldTypePropertyId;
import com.microsoft.exchange.ical.model.PathToExtendedFieldTypePropertySetId;
import com.microsoft.exchange.ical.model.PathToExtendedFieldTypePropertyTag;
import com.microsoft.exchange.ical.model.PathToExtendedFieldTypePropertyType;
import com.microsoft.exchange.types.ArrayOfStringsType;
import com.microsoft.exchange.types.AttendeeType;
import com.microsoft.exchange.types.CalendarItemType;
import com.microsoft.exchange.types.CalendarItemTypeType;
import com.microsoft.exchange.types.DistinguishedPropertySetType;
import com.microsoft.exchange.types.EmailAddressType;
import com.microsoft.exchange.types.ExtendedPropertyType;
import com.microsoft.exchange.types.FolderIdType;
import com.microsoft.exchange.types.ImportanceChoicesType;
import com.microsoft.exchange.types.ItemIdType;
import com.microsoft.exchange.types.ItemType;
import com.microsoft.exchange.types.LegacyFreeBusyType;
import com.microsoft.exchange.types.MailboxTypeType;
import com.microsoft.exchange.types.MapiPropertyTypeType;
import com.microsoft.exchange.types.NonEmptyArrayOfAttendeesType;
import com.microsoft.exchange.types.NonEmptyArrayOfPropertyValuesType;
import com.microsoft.exchange.types.PathToExtendedFieldType;
import com.microsoft.exchange.types.ResponseTypeType;
import com.microsoft.exchange.types.SensitivityChoicesType;
import com.microsoft.exchange.types.SingleRecipientType;
import com.microsoft.exchange.types.TaskType;
import com.microsoft.exchange.types.TimeZoneDefinitionType;

public class ExchangeEventConverterImpl implements ExchangeEventConverter {

    protected Log log = LogFactory.getLog(this.getClass());

    @Override
    public Calendar convertToCalendar(Collection<ItemType> items, String upn) {
        Calendar result = new Calendar();

        result.getProperties().add(PROD_ID);
        result.getProperties().add(VERSION);

        int size = CollectionUtils.isEmpty(items) ? 0 : items.size();
        log.debug("attempting to convert " + size + " items");
        if (!CollectionUtils.isEmpty(items)) {

            for (ItemType item : items) {
                if (item instanceof CalendarItemType) {
                    CalendarItemType calendarItem = (CalendarItemType) item;
                    Pair<VEvent, ArrayList<VTimeZone>> pair = null;
                    try {
                        pair = convertCalendarItemType(calendarItem, upn);
                    } catch (ExchangeEventConverterException e) {
                        log.error("Failed to convert calendarItem:" + e.getMessage());
                    }

                    if (null != pair) {
                        if (null != pair.getLeft()) {
                            result.getComponents().add(pair.getLeft());
                        } else {
                            log.warn("Failed to generate VEvent for CalendarItemType=" + calendarItem);
                        }
                        if (!CollectionUtils.isEmpty(pair.getRight())) {
                            log.debug("Generated " + pair.getRight().size()
                                    + " VTimeZone components for CalendarItemType=" + calendarItem);
                            for (VTimeZone timeZone : pair.getRight()) {
                                result.getComponents().add(timeZone);
                            }
                        } else {
                            log.warn("Failed to generate VTimeZone for CalendarItemType=" + calendarItem);
                        }
                    }

                } else if (item instanceof TaskType) {
                    TaskType taskItem = (TaskType) item;
                    Pair<VToDo, ArrayList<VTimeZone>> pair = convertTaskType(taskItem, upn);
                    //TODO handle tasks
                } else {
                    log.warn("Not an instanceof CalendarItemType | TaskType.  Cannot convert item: " + item);
                }
            }
        }

        return result;
    }

    protected Pair<VToDo, ArrayList<VTimeZone>> convertTaskType(TaskType taskItem, String upn) {
        VToDo task = new VToDo();
        ArrayList<VTimeZone> timeZones = new ArrayList<VTimeZone>();

        Pair<VToDo, ArrayList<VTimeZone>> pair = Pair.of(task, timeZones);
        return pair;
    }

    /**
     * 
     * TimeZones.
     * 
     * @param calendarItem
     * @param upn
     * @return
     * @throws ExchangeEventConverterException 
     */
    protected Pair<VEvent, ArrayList<VTimeZone>> convertCalendarItemType(CalendarItemType calendarItem, String upn)
            throws ExchangeEventConverterException {
        VEvent event = new VEvent();
        ArrayList<VTimeZone> timeZones = new ArrayList<VTimeZone>();

        if (calendarItem.getStart() == null) {
            throw new ExchangeEventConverterException("calendarItem must have a valid start time.");
        }

        if (calendarItem.getEnd() == null && calendarItem.getDuration() == null) {
            throw new ExchangeEventConverterException("calendarItem must have a valid end time or duration.");
        }

        //does this element have a timezone?
        XMLGregorianCalendar start = calendarItem.getStart();
        DtStart dtStart = new DtStart(new DateTime(start.toGregorianCalendar().getTime()));
        DtEnd dtEnd = null;

        if (null != calendarItem.getEnd()) {
            dtEnd = new DtEnd(new DateTime(calendarItem.getEnd().toGregorianCalendar().getTime()));
        }

        //if all day event, must use Date
        if (null != calendarItem.isIsAllDayEvent() && calendarItem.isIsAllDayEvent()) {
            dtStart = new DtStart(new Date(start.toGregorianCalendar().getTime()), true);
            dtEnd = new DtEnd(new Date(calendarItem.getEnd().toGregorianCalendar().getTime()), true);
            log.debug("set to all day event");
        }
        //this way no vtimezone is needed
        dtStart.setUtc(true);

        event.getProperties().add(dtStart);
        log.debug("added dtStart=" + dtStart);

        if (null != dtEnd) {
            dtEnd.setUtc(true);
            event.getProperties().add(dtEnd);
            log.debug("added dtEnd=" + dtEnd);
        }

        //in case dtEnd is not present but duration is.
        String duration = calendarItem.getDuration();
        if (StringUtils.isNotBlank(duration) && event.getProperty(DtEnd.DTEND) == null) {
            Dur dur = new Dur(duration);
            Duration durationProperty = new Duration(dur);
            event.getProperties().add(durationProperty);
            event.getProperties().remove(DtEnd.DTEND);
            log.debug("dtend overridden with duration=" + durationProperty);
        }

        String uid = calendarItem.getUID();
        if (StringUtils.isNotBlank(uid)) {
            Uid uidProperty = new Uid(uid);
            event.getProperties().add(uidProperty);
            log.debug("added Uid=" + uidProperty);
        } else {
            log.debug("could not generate Uid property.");
        }

        //should always set dtstamp, otherwise it's auto-generated and !veventCreatedNow.equals(veventCreatedLater);
        if (null != calendarItem.getDateTimeCreated()) {
            DtStamp dtstamp = new DtStamp(
                    new DateTime(calendarItem.getDateTimeCreated().toGregorianCalendar().getTime()));
            dtstamp.setUtc(true);

            event.getProperties().remove(event.getProperty(DtStamp.DTSTAMP));
            event.getProperties().add(dtstamp);
            log.debug("overide DtStamp=" + dtstamp);
        } else {
            log.debug("could not generate DtStamp, property will be autogenerated.");
        }

        String subject = calendarItem.getSubject();
        if (StringUtils.isNotBlank(subject)) {
            Summary summaryProperty = new Summary(subject);
            event.getProperties().add(summaryProperty);
            log.debug("add summary=" + summaryProperty);
        } else {
            log.debug("could not generate Summary property");
        }

        String location = calendarItem.getLocation();
        if (StringUtils.isNotBlank(location)) {
            event.getProperties().add(new Location(location));
        } else {
            log.debug("could not generate location property");
        }

        LegacyFreeBusyType freeBusy = calendarItem.getLegacyFreeBusyStatus();
        Transp transpProperty = Transp.OPAQUE;
        if (LegacyFreeBusyType.FREE.equals(freeBusy)) {
            transpProperty = Transp.TRANSPARENT;
        }
        event.getProperties().add(transpProperty);
        log.debug("added Transp=" + transpProperty);

        Status status = Status.VEVENT_CONFIRMED;
        if (BooleanUtils.isTrue(calendarItem.isIsCancelled())) {
            status = Status.VEVENT_CANCELLED;
        }
        event.getProperties().add(status);
        log.debug("added Status=" + status);

        boolean organizerIsSet = false;
        SingleRecipientType calendarItemOrganizer = calendarItem.getOrganizer();
        if (null != calendarItemOrganizer) {
            Organizer organizer = convertToOrganizer(calendarItemOrganizer);
            if (null != organizer) {
                event.getProperties().add(organizer);
                organizerIsSet = true;
                log.debug("added Organizer=" + organizer);
            } else {
                log.debug("could not gernate Organizer. As a result, attendees will not be added.");
            }
        } else {
            log.debug("could not gernate Organizer. As a result, attendees will not be added.");
        }

        //only add RequiredAttendees, OptionalAttendees and Resources if and only if organizer present.
        if (organizerIsSet) {

            ResponseTypeType myResponseType = calendarItem.getMyResponseType();

            //add RequiredAttendees
            NonEmptyArrayOfAttendeesType requiredAttendees = calendarItem.getRequiredAttendees();
            if (null != requiredAttendees) {
                Set<Attendee> attendees = convertRequiredAttendees(requiredAttendees, myResponseType);
                for (Attendee attendee : attendees) {
                    event.getProperties().add(attendee);
                }
            } else {
                log.debug("no required attendees.");
            }

            //add OptionalAttendees
            NonEmptyArrayOfAttendeesType optionalAttendees = calendarItem.getOptionalAttendees();
            if (null != optionalAttendees) {
                Set<Attendee> attendees = convertOptionalAttendees(optionalAttendees, myResponseType);
                for (Attendee attendee : attendees) {
                    event.getProperties().add(attendee);
                }
            } else {
                log.debug("no optional attendees");
            }

            //add Resources
            NonEmptyArrayOfAttendeesType resourceAttendees = calendarItem.getResources();
            if (null != resourceAttendees) {
                Set<Attendee> attendees = convertResourceAttendees(resourceAttendees, myResponseType);
                for (Attendee attendee : attendees) {
                    event.getProperties().add(attendee);
                }
            }
        }

        CalendarItemTypeType calendarItemType = calendarItem.getCalendarItemType();
        if (null != calendarItemType) {
            if (CalendarItemTypeType.EXCEPTION.equals(calendarItemType)
                    || CalendarItemTypeType.RECURRING_MASTER.equals(calendarItemType)) {
                log.warn(
                        "Recurring Event Detected!  This implementation of ExchangeEventConverter does not expand recurrance.  You should use a CalendarView to expand recurrence on the Exchagne server. --http://msdn.microsoft.com/en-us/library/office/aa564515(v=exchg.150).aspx");
            }
        }

        //generate xproperties for standard item properties
        Collection<XProperty> itemXProperties = generateItemTypeXProperties(calendarItem);
        for (XProperty xp : itemXProperties) {
            event.getProperties().add(xp);
        }

        //generate XProperty's for ExtendedProperties...
        List<ExtendedPropertyType> extendedProperties = calendarItem.getExtendedProperties();
        if (!CollectionUtils.isEmpty(extendedProperties)) {
            for (ExtendedPropertyType extendedProperty : extendedProperties) {
                Collection<XProperty> xProperties = convertExtendedPropertyType(extendedProperty);
                for (XProperty xp : xProperties) {
                    event.getProperties().add(xp);
                }
            }
        }

        Pair<VEvent, ArrayList<VTimeZone>> pair = Pair.of(event, timeZones);
        return pair;
    }

    private Collection<XProperty> generateCalendarItemTypeXProperties(CalendarItemType calendarItem) {
        Collection<XProperty> xprops = new LinkedHashSet<XProperty>();

        String timeZone = calendarItem.getTimeZone();
        if (StringUtils.isNotBlank(timeZone)) {
            xprops.add(new ExchangeTimeZoneProperty(timeZone));
        } else {
            log.warn("unable to generate ExchangeTimeZoneProperty, timeZone is blank");
        }
        TimeZoneDefinitionType startTimeZone = calendarItem.getStartTimeZone();
        if (null != startTimeZone && StringUtils.isNotBlank(startTimeZone.getId())) {
            xprops.add(new ExchangeStartTimeZoneProperty(startTimeZone.getId()));
        } else {
            log.debug("unable to generate ExchangeStartTimeZoneProperty, startTimeZone is blank");
        }
        TimeZoneDefinitionType endTimeZone = calendarItem.getEndTimeZone();
        if (null != endTimeZone && StringUtils.isNotBlank(endTimeZone.getId())) {
            xprops.add(new ExchangeEndTimeZoneProperty(endTimeZone.getId()));
        } else {
            log.debug("unable to generate ExchangeEndTimeZoneProperty, endTimeZone is blank");
        }

        return xprops;
    }

    /**
     * Return a never null but possibly empty {@link Collection} of {@link XProperty}
     * 
     * Returned {@link XProperty}s may include:
     * {@link ItemTypeParentFolderId}, 
     * 
     * @param item
     * @return
     */
    private Collection<XProperty> generateItemTypeXProperties(ItemType item) {
        Collection<XProperty> xprops = new LinkedHashSet<XProperty>();

        FolderIdType parentFolderId = item.getParentFolderId();
        if (null != parentFolderId) {
            String p_id = parentFolderId.getId();
            String p_ck = parentFolderId.getChangeKey();
            if (StringUtils.isNotBlank(p_id)) {
                xprops.add(new ItemTypeParentFolderId(parentFolderId));
            } else {
                log.warn("unable to generate X_EWS_PARENT_FOLDER_ID, parentFolderId is blank");
            }
            if (StringUtils.isNotBlank(p_ck)) {
                xprops.add(new ItemTypeParentFolderChangeKey(parentFolderId));
            } else {
                log.warn("unable to generate X_EWS_PARENT_FOLDER_CHANGEKEY, parentFolderChangeKey is blank");
            }
        }

        ItemIdType itemId = item.getItemId();
        if (null != itemId) {
            String i_id = itemId.getId();
            String i_ck = itemId.getChangeKey();
            if (StringUtils.isNotBlank(i_id)) {
                xprops.add(new ItemTypeItemId(itemId));
            } else {
                log.warn("unable to generate X_EWS_ITEM_ID, itemId is blank");
            }
            if (StringUtils.isNotBlank(i_ck)) {
                xprops.add(new ItemTypeChangeKey(itemId));
            } else {
                log.warn("unable to generate X_EWS_ITEM_CHANGEKEY, itemChangeKey is blank");
            }
        }
        if (item instanceof CalendarItemType) {
            CalendarItemType calendarItem = (CalendarItemType) item;
            Collection<XProperty> calendarXProps = generateCalendarItemTypeXProperties(calendarItem);
            if (!CollectionUtils.isEmpty(calendarXProps)) {
                xprops.addAll(calendarXProps);
            }
        } else {
            log.warn("item is not a CalendarItemType, X_EWS...TIMEZONE properties will not be generated.");
        }
        return xprops;
    }

    /**
     * return a never null but possibly empty {@link Collection} of {@link XProperty}
     * 
     * if an {@link ExtendedPropertyType} contains multiple values this method will return multiple {@link XProperty}'s.
     * 
     * @param extendedProperty
     * @return
     */
    private Collection<XProperty> convertExtendedPropertyType(ExtendedPropertyType extendedProperty) {

        Collection<XProperty> xprops = new LinkedHashSet<XProperty>();
        PathToExtendedFieldType extendedFieldURI = extendedProperty.getExtendedFieldURI();
        if (null != extendedFieldURI) {
            String propertyName = extendedFieldURI.getPropertyName();
            if (StringUtils.isBlank(propertyName)) {
                DistinguishedPropertySetType distinguishedPropertySetId = extendedFieldURI
                        .getDistinguishedPropertySetId();
                if (null != distinguishedPropertySetId) {
                    propertyName = distinguishedPropertySetId.value();
                }
            }
            ParameterList params = new ParameterList();
            String exPropSetId = extendedFieldURI.getPropertySetId();
            if (StringUtils.isNotBlank(exPropSetId)) {
                params.add(new PathToExtendedFieldTypePropertySetId(extendedFieldURI));
            }
            Integer exPropId = extendedFieldURI.getPropertyId();
            if (StringUtils.isNotBlank(exPropId.toString())) {
                params.add(new PathToExtendedFieldTypePropertyId(extendedFieldURI));
            }
            MapiPropertyTypeType propertyType = extendedFieldURI.getPropertyType();
            if (null != propertyType && StringUtils.isNotBlank(propertyType.value())) {
                params.add(new PathToExtendedFieldTypePropertyType(extendedFieldURI));
            }
            String propertyTag = extendedFieldURI.getPropertyTag();
            if (StringUtils.isNotBlank(propertyTag)) {
                params.add(new PathToExtendedFieldTypePropertyTag(extendedFieldURI));
            }
            Set<String> xPropertyValues = new HashSet<String>();
            if (StringUtils.isNotBlank(propertyName)) {
                NonEmptyArrayOfPropertyValuesType values = extendedProperty.getValues();
                if (null != values && !CollectionUtils.isEmpty(values.getValues())) {
                    xPropertyValues.addAll(values.getValues());
                } else if (null != extendedProperty.getValue()) {
                    xPropertyValues.add(extendedProperty.getValue());
                }
            } else {
                log.error("Unable to generate XProperty(s). propertyName not found for ExtendedPropertyType="
                        + extendedProperty);
            }
            if (!CollectionUtils.isEmpty(xPropertyValues)) {
                Integer count = 0;
                for (String xValue : xPropertyValues) {
                    xprops.add(new XProperty(propertyName, params, xValue));
                    propertyName += "_" + count;
                    count++;
                }
            } else {
                log.error("Unable to generate XProperty(s). propertyValue(s) not found for ExtendedPropertyType="
                        + extendedProperty);
            }
        }
        return xprops;
    }

    /**
     * Convert the {@link String} argument to a mailto {@link URI} if possible.
     * 
     * @param emailAddress
     * @return the email as a URI
     * @throws IllegalArgumentException
     *             if conversion failed, or if the argument was empty
     *
     * <strong>WARNING</strong >A {@link CalendarItemType} may contain attendees that no longer have a valid email address.
     * If an event contains an attendee that has been deleted, the email address field takes the value of <legacyDn> example: <t:EmailAddress>/O=EXCHANGELABS/OU=EXCHANGE ADMINISTRATIVE GROUP (FYDIBOHF23SPDLT)/CN=RECIPIENTS/CN=F450764bd9384fd3b7a38722504c8815-Documentati</t:EmailAddress>
     */
    public URI emailToURI(final String emailAddress) {
        Validate.notEmpty(emailAddress, "emailAddress cannot be null");
        URI uri = null;
        try {
            uri = new URI("mailto:" + emailAddress);
        } catch (URISyntaxException e) {
            log.debug("caught URISyntaxException trying to construct mailto URI for " + emailAddress + "\n"
                    + e.getMessage());
        }
        return uri;
    }

    /**
     * This method will return a never null a {@link Pair}<{@link ParameterList},{@link URI}> for a given {@link EmailAddressType}
     * This method will return a null URI element if the  {@link EmailAddressType} does not contain a valid EmailAddress property
     * This method will return a never null but possibly empty {@link ParameterList}.  
     * 
     * ParamaterList may contain the following {@link Parameter}s: 
     * {@link Cn}, {@link EmailAddressRoutingType}, {@link EmailAddressMailboxType}
     * 
     * @param recipient
     * @return
     */
    protected Pair<ParameterList, URI> convertEmailAddressType(EmailAddressType emailAddressType, Role role) {
        URI uri = null;
        ParameterList params = new ParameterList();
        if (null != emailAddressType) {
            String emailAddress = emailAddressType.getEmailAddress();
            if (StringUtils.isNotBlank(emailAddress)) {
                uri = emailToURI(emailAddress);
            } else {
                log.warn("convertEmailAddressType: could not generate URI.");
            }

            if (null != role) {
                params.add(role);
            }

            String name = emailAddressType.getName();
            if (StringUtils.isNotBlank(name)) {
                params.add(new Cn(name));
            } else {
                log.debug("convertEmailAddressType: could not generate Cn");
            }
            String routingType = emailAddressType.getRoutingType();
            if (StringUtils.isNotBlank(routingType)) {
                params.add(new EmailAddressRoutingType(routingType));
            } else {
                log.debug("convertEmailAddressType: could not generate EmailAddressRoutingType");
            }
            MailboxTypeType mailboxType = emailAddressType.getMailboxType();
            if (null != mailboxType) {
                params.add(new EmailAddressMailboxType(mailboxType));
                CuType cuType = convertMailboxTypeTypeToCuType(mailboxType, role);
                params.add(cuType);
            } else {
                log.debug("convertEmailAddressType: could not generate EmailAddressMailboxType");
            }
        } else {
            log.debug("convertEmailAddressType: EmailAddressType = null");
        }
        Pair<ParameterList, URI> pair = Pair.of(params, uri);
        return pair;
    }

    /**
     * This method will attempt to generate a {@link Organizer} from a {@link SingleRecipientType} 
     *     * 
     * This method will add {@link PartStat.ACCEPTED} and {@link net.fortuna.ical4j.model.parameter.Role.CHAIR} when an organizer is found.
     * {@link com.microsoft.exchange.impl.ExchangeEventConverterImpl.convertEmailAddressType(EmailAddressType)}  for a list of other paramaters that may be included in the response
     * 
     * This method will return null if the {@link SingleRecipientType} EmailAddress field  is missing or invalid.
     * 
     * @param calendarItemOrganizer
     * @return
     */
    public Organizer convertToOrganizer(SingleRecipientType calendarItemOrganizer) {
        Organizer organizer = null;
        if (null != calendarItemOrganizer) {
            Pair<ParameterList, URI> pair = convertEmailAddressType(calendarItemOrganizer.getMailbox(), Role.CHAIR);
            URI organizerURI = pair.getRight();
            ParameterList organizerParams = pair.getLeft();
            if (null != organizerURI) {

                organizer = new Organizer(organizerParams, organizerURI);

                //organizer is always ACCEPTED
                organizer.getParameters().add(PartStat.ACCEPTED);

            } else {
                log.debug("convertToOrganizer: organizerURI = null, Organizer = null ");
            }
        } else {
            log.debug("convertToOrganizer: calendarItemOrganizer = null, Organizer = null ");
        }
        return organizer;
    }

    /**
     * This method returns a never null but possibly empty {@link HashSet} of {@link Attendee}s.
     * This method will attempt to generate a {@link Attendee} for each {@link AttendeeType} contained within @link {@link NonEmptyArrayOfAttendeesType}.
     * An a {@link Attendee} will not be generated for any {@link AttendeeType} with a missing or invalid {@link EmailAddressType}
     * 
     * Attendee Responses are only present if you obtained CalendarItem from Exchange as organizer. @see <a href="http://office.microsoft.com/en-us/outlook-help/organize-meetings-with-outlook-RZ001166003.aspx?section=20">Attendees do not see responses</a>
     * 
     * {@link com.microsoft.exchange.impl.ExchangeEventConverterImpl.convertEmailAddressType(EmailAddressType)} for details on how recipient EmailAddressType properties are mapped to {@link Parameter}s.
     * 
     * @param attendees
     * @param myResponseType - a {@link PartStat} parameter will be added to every {@link Attendee} if and only if myResponseType.eqals( {@link ResponseTypeType}.ORGANIZER and the corresponding {@link AttendeeType} contains a valid {@link ResponseTypeType} )
     * @param requiredAttendees - Indicates which {@link Role} parameter to add to attendees. True indicates that the {@link NonEmptyArrayOfAttendeesType} represent Role.REQ_PARTICIPANT, false indicates the {@link NonEmptyArrayOfAttendeesType} are optional attendees
     * @return
     */
    protected Set<Attendee> convertAttendees(NonEmptyArrayOfAttendeesType attendees,
            ResponseTypeType myResponseType, Role role) {

        Set<Attendee> attendeeSet = new HashSet<Attendee>();

        if (null != attendees && !CollectionUtils.isEmpty(attendees.getAttendees())) {
            for (AttendeeType attendeeType : attendees.getAttendees()) {
                if (null != attendeeType) {
                    EmailAddressType mailbox = attendeeType.getMailbox();
                    Pair<ParameterList, URI> attendeePair = convertEmailAddressType(mailbox, role);

                    URI attendeeURI = attendeePair.getRight();
                    ParameterList attendeeParams = attendeePair.getLeft();

                    if (null != attendeeURI) {
                        Attendee attendee = new Attendee(attendeeParams, attendeeURI);

                        if (null != myResponseType && myResponseType.equals(ResponseTypeType.ORGANIZER)) {
                            //responseType should be present
                            ResponseTypeType responseType = attendeeType.getResponseType();
                            if (null != responseType) {
                                //go ahead and add a partstat
                                attendee.getParameters().add(convertResponseTypeTypeToPartStat(responseType));
                            }
                        }

                        if (attendeeSet.add(attendee)) {
                            log.debug("added Attendee=" + attendee);
                        }
                    }
                }
            }
        } else {
            log.debug("no attendees");
        }

        return attendeeSet;
    }

    public Set<Attendee> convertRequiredAttendees(NonEmptyArrayOfAttendeesType attendees,
            ResponseTypeType myResponseType) {
        return convertAttendees(attendees, myResponseType, Role.REQ_PARTICIPANT);
    }

    public Set<Attendee> convertOptionalAttendees(NonEmptyArrayOfAttendeesType attendees,
            ResponseTypeType myResponseType) {
        return convertAttendees(attendees, myResponseType, Role.OPT_PARTICIPANT);
    }

    public Set<Attendee> convertResourceAttendees(NonEmptyArrayOfAttendeesType attendees,
            ResponseTypeType myResponseType) {
        return convertAttendees(attendees, myResponseType, Role.NON_PARTICIPANT);
    }

    /**
     * Returns a never null {@link PartStat} for a given {@link ResponseTypeType} 
     * 
     * ResponseTypeType.ORGANIZER   => PartStat.ACCEPTED
     * ResponseTypeType.ACCEPT       => PartStat.ACCEPTED
     * ResponseTypeType.DECLINE      => PartStat.DECLINED 
     * ResponseTypeType.TENTATIVE   => PartStat.TENTATIVE
     * All other ResponseTypeTypes    => PartStat.NEEDS_ACTION
     * 
     * @param responseType
     * @return
     */
    public static PartStat convertResponseTypeTypeToPartStat(ResponseTypeType responseType) {
        if (null != responseType) {
            if (responseType.equals(ResponseTypeType.ACCEPT) || responseType.equals(ResponseTypeType.ORGANIZER)) {
                return PartStat.ACCEPTED;
            } else if (responseType.equals(ResponseTypeType.DECLINE)) {
                return PartStat.DECLINED;
            } else if (responseType.equals(ResponseTypeType.TENTATIVE)) {
                return PartStat.TENTATIVE;
            }
        }
        return PartStat.NEEDS_ACTION;
    }

    /**
     *  Returns a never null {@link ResponseTypeType} for a given {@link PartStat}
     *  
     *  PartStat.ACCEPTED       => ResponseTypeType.ACCEPT
     *  PartStat.DECLINED       => ResponseTypeType.DECLINE  
     *  PartStat.TENTATIVE      => ResponseTypeType.TENTATIVE
     *  PartStat.NEEDS_ACTION   => ResponseTypeType.NO_RESPONSE_RECEIVED
     *  All other PartStats     => ResponseTypeType.UNKNOWN
     *  
     * @param partStat
     * @return
     */
    public static ResponseTypeType convertPartStatToResponseTypeType(PartStat partStat) {
        if (null != partStat) {
            if (partStat.equals(PartStat.ACCEPTED)) {
                return ResponseTypeType.ACCEPT;
            } else if (partStat.equals(PartStat.DECLINED)) {
                return ResponseTypeType.DECLINE;
            } else if (partStat.equals(PartStat.TENTATIVE)) {
                return ResponseTypeType.TENTATIVE;
            } else if (partStat.equals(PartStat.NEEDS_ACTION)) {
                return ResponseTypeType.NO_RESPONSE_RECEIVED;
            }
        }
        return ResponseTypeType.UNKNOWN;
    }

    /**
     * Return a never null {@link Clazz} for a given {@link SensitivityChoicesType}
     * 
     * @see <a href="http://windowsitpro.com/outlook/outlook-using-sensitivity-levels-appointments">Using Sensitivity Levels with Appointments</a>
     * 
     * SensitivityChoicesType.CONFIDENTIAL    => Clazz.CONFIDENTIAL
     * SensitivityChoicesType.NORMAL      => Clazz.PUBLIC
     * All other SensitivityChoicesType      => Clazz.PRIVATE
     * 
     * @param sensitivity
     * @return
     */
    public static Clazz convertSensitivityToClazz(SensitivityChoicesType sensitivity) {
        Clazz clazz = Clazz.PRIVATE;
        if (null != sensitivity) {
            if (sensitivity.equals(SensitivityChoicesType.CONFIDENTIAL)) {
                clazz = Clazz.CONFIDENTIAL;
            } else if (sensitivity.equals(SensitivityChoicesType.NORMAL)) {
                clazz = Clazz.PUBLIC;
            }
        }
        return clazz;
    }

    /**
     * Return a never null {@link SensitivityChoicesType} for a given {@link Clazz}
     * 
     * Clazz.CONFIDENTIAL    => SensitivityChoicesType.CONFIDENTIAL
     * Clazz.PUBLIC       => SensitivityChoicesType.NORMAL
     * All other Clazz      => SensitivityChoicesType.PRIVATE
     * 
     * @param clazz
     * @return
     */
    public static SensitivityChoicesType convertClazzToSensitivityChoicesType(Clazz clazz) {
        SensitivityChoicesType sensitivity = SensitivityChoicesType.PRIVATE;
        if (null != clazz) {
            if (clazz.equals(Clazz.CONFIDENTIAL)) {
                sensitivity = SensitivityChoicesType.CONFIDENTIAL;
            } else if (clazz.equals(Clazz.PUBLIC)) {
                sensitivity = SensitivityChoicesType.NORMAL;
            }
        }
        return sensitivity;
    }

    /**
     * Returns a never null {@link Priority} for a given {@link ImportanceChoicesType}
     * 
     * Defaults to Priority.MEDIUM;
     * 
     * @param importance
     * @return
     */
    public static Priority convertImportanceChoicesTypeToPriority(ImportanceChoicesType importance) {
        Priority priority = Priority.MEDIUM;
        if (null != importance) {
            if (importance.equals(ImportanceChoicesType.HIGH)) {
                priority = Priority.HIGH;
            } else if (importance.equals(ImportanceChoicesType.LOW))
                priority = Priority.LOW;
        }
        return priority;
    }

    /** 
     * Returns a never null {@link ImportanceChoicesType} for a given {@link Priority}
     * 
     * Defaults to ImportanceChoicesType.NORMAL
     * 
     * Priority (HIGH => 1, NORMAL => 5, LOW => 9) 
     * 
     * @param priority
     * @return
     */
    public static ImportanceChoicesType convertPriorityToImportanceChoicesType(Priority priority) {
        ImportanceChoicesType importance = ImportanceChoicesType.NORMAL;
        if (null != priority) {
            if (priority.equals(Priority.HIGH)) {
                importance = ImportanceChoicesType.HIGH;
            } else if (priority.equals(Priority.LOW)) {
                importance = ImportanceChoicesType.LOW;
            }
        }
        return importance;
    }

    /**
     * Return a never null {@link CuType} for a given {@link MailboxTypeType}
     * 
     * Defaults to CuType.INDIVIDUAL
     * 
     * @see <a href="http://www.kanzaki.com/docs/ical/cutype.html">Calendar User Type</a>
     * @see <a href="http://msdn.microsoft.com/en-us/library/office/aa563493(v=exchg.140).aspx">MailboxType</a>
     *  
     * @param mailboxType
     * @return
     */
    public static CuType convertMailboxTypeTypeToCuType(MailboxTypeType mailboxType, Role role) {
        //If not specified on a property that allows this parameter, the default is INDIVIDUAL.
        CuType cuType = CuType.INDIVIDUAL;
        if (null != mailboxType) {
            if (MailboxTypeType.PRIVATE_DL.equals(mailboxType) || MailboxTypeType.PUBLIC_DL.equals(mailboxType)) {
                cuType = CuType.GROUP;
            }

            //TODO this is bad hack
            if (Role.NON_PARTICIPANT.equals(role)) {
                cuType = CuType.RESOURCE;
            }
        }
        return cuType;
    }

    /**
     * Returns a never null but possibly empty TextList
     * 
     * @param strings
     * @return
     */
    public static TextList convertArrayOfStringsTypeToTextList(ArrayOfStringsType strings) {
        TextList textList = new TextList();
        if (strings != null) {
            List<String> stringList = strings.getStrings();
            if (!CollectionUtils.isEmpty(stringList)) {
                for (String s : stringList) {
                    textList.add(s);
                }
            }
        }
        return textList;
    }

    /**
     * Returns a never null {@link Categories} containing one entry for each entry string contained in {@link ArrayOfStringsType}
     * 
     * @param categories
     * @return
     */
    public static Categories convertCategories(ArrayOfStringsType arrayOfCategories) {

        TextList textList = convertArrayOfStringsTypeToTextList(arrayOfCategories);
        return new Categories(textList);

    }
}