/** * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright ownership. Apereo * 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 the * following location: * * <p>http://www.apache.org/licenses/LICENSE-2.0 * * <p>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 org.jasig.portlet.calendar.adapter; import com.microsoft.exchange.messages.FreeBusyResponseType; import com.microsoft.exchange.messages.GetUserAvailabilityRequest; import com.microsoft.exchange.messages.GetUserAvailabilityResponse; import com.microsoft.exchange.types.ArrayOfCalendarEvent; import com.microsoft.exchange.types.ArrayOfMailboxData; import com.microsoft.exchange.types.DayOfWeekType; import com.microsoft.exchange.types.Duration; import com.microsoft.exchange.types.FreeBusyViewOptions; import com.microsoft.exchange.types.Mailbox; import com.microsoft.exchange.types.MailboxData; import com.microsoft.exchange.types.MeetingAttendeeType; import com.microsoft.exchange.types.SerializableTimeZoneTime; import com.microsoft.exchange.types.TimeZone; import java.util.Calendar; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import javax.portlet.PortletRequest; import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; import javax.xml.datatype.XMLGregorianCalendar; import net.fortuna.ical4j.model.PropertyList; import net.fortuna.ical4j.model.component.VEvent; 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.Location; import net.fortuna.ical4j.model.property.Summary; import net.fortuna.ical4j.model.property.Uid; import net.sf.ehcache.Cache; import net.sf.ehcache.Element; import org.apache.commons.lang.StringUtils; import org.jasig.portlet.calendar.CalendarConfiguration; import org.jasig.portlet.calendar.adapter.exchange.ExchangeWebServiceCallBack; import org.jasig.portlet.calendar.adapter.exchange.IExchangeCredentialsInitializationService; import org.jasig.portlet.calendar.caching.DefaultCacheKeyGeneratorImpl; import org.jasig.portlet.calendar.caching.ICacheKeyGenerator; import org.joda.time.DateTime; import org.joda.time.Interval; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ws.client.core.WebServiceMessageCallback; import org.springframework.ws.client.core.WebServiceOperations; /** * Queries Exchange Web Services API for calendar events. * * @author Jen Bourey, jbourey@unicon.net * @version $Revision$ */ public class ExchangeCalendarAdapter extends AbstractCalendarAdapter implements ICalendarAdapter { protected static final String AVAILABILITY_SOAP_ACTION = "http://schemas.microsoft.com/exchange/services/2006/messages/GetUserAvailability"; protected static final String UTC = "UTC"; protected final Logger log = LoggerFactory.getLogger(getClass()); private WebServiceOperations webServiceOperations; /** * Set the Spring WebService operations object to allow making Web Services calls. * * @param webServiceOperations Spring WebService operations object */ public void setWebServiceOperations(WebServiceOperations webServiceOperations) { this.webServiceOperations = webServiceOperations; } private IExchangeCredentialsInitializationService credentialsService; /** * Set the Exchange Credentials service for interacting with credentials information. * * @param credentialsService Exchange Credentials service */ public void setCredentialsService(IExchangeCredentialsInitializationService credentialsService) { this.credentialsService = credentialsService; } private Cache cache; /** * Sets the cache to use for caching calendar events * * @param cache cache to use */ public void setCache(Cache cache) { this.cache = cache; } private ICacheKeyGenerator cacheKeyGenerator = new DefaultCacheKeyGeneratorImpl(); /** * Sets the cache key generator to use. Defaults to <code>DefaultCacheKeyGeneratorImpl</code> * * @param cacheKeyGenerator */ public void setCacheKeyGenerator(ICacheKeyGenerator cacheKeyGenerator) { this.cacheKeyGenerator = cacheKeyGenerator; } private String cacheKeyPrefix = "exchange"; /** * Sets the cache key prefix to use for this adapter. Defaults to "exchange". * * @param cacheKeyPrefix cache key prefix to use */ public void setCacheKeyPrefix(String cacheKeyPrefix) { this.cacheKeyPrefix = cacheKeyPrefix; } private String emailAttribute = "mail"; public void setEmailAttribute(String emailAttribute) { this.emailAttribute = emailAttribute; } private String requestServerVersion = "Exchange2007_SP1"; /** * Sets the minimum Exchange Web Services messaging version required. Defaults to * Exchange2007_SP1. * * @param requestServerVersion */ public void setRequestServerVersion(final String requestServerVersion) { this.requestServerVersion = requestServerVersion; } /* * (non-Javadoc) * @see org.jasig.portlet.calendar.adapter.ICalendarAdapter#getEvents(org.jasig.portlet.calendar.CalendarConfiguration, net.fortuna.ical4j.model.Period, javax.portlet.PortletRequest) */ public CalendarEventSet getEvents( CalendarConfiguration calendarConfiguration, Interval interval, PortletRequest request) throws CalendarException { @SuppressWarnings("unchecked") Map<String, String> userInfo = (Map<String, String>) request.getAttribute(PortletRequest.USER_INFO); String email = userInfo.get(emailAttribute); if (StringUtils.isBlank(email)) { throw new CalendarException( "Null email address obtained from user attribute " + emailAttribute + ". It must be specified (see Person Dir config)"); } // try to get the cached calendar String key = cacheKeyGenerator.getKey( calendarConfiguration, interval, request, cacheKeyPrefix.concat(".").concat(email)); Element cachedElement = this.cache.get(key); CalendarEventSet eventSet; if (cachedElement == null) { log.debug("Retrieving exchange events for account {}", email); Set<VEvent> events = retrieveExchangeEvents(request, calendarConfiguration, interval, email); log.debug("Exchange adapter found {} events", events.size()); // save the calendar event set to the cache eventSet = insertCalendarEventSetIntoCache(this.cache, key, events); } else { log.debug("Cache hit for exchange events for account {}", email); eventSet = (CalendarEventSet) cachedElement.getObjectValue(); } return eventSet; } /** * Retrieve a set of CalendarEvents from the Exchange server for the specified period and email * address. An EWS message is constructed that looks something like the following. The <code> * ExchangeImpersonation</code> element is optional based on whether Exchange Impersonation is * enabled: * * <p> * * <pre> * <SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> * <SOAP-ENV:Header> * <ns3:RequestServerVersion xmlns:ns3="http://schemas.microsoft.com/exchange/services/2006/types" Version="Exchange2007_SP1"/> * <t:ExchangeImpersonation xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"> * <t:ConnectingSID> * <t:PrincipalName>o365st19@ed.ac.uk</t:PrincipalName> * </t:ConnectingSID> * </t:ExchangeImpersonation> * <ns3:RequestServerVersion xmlns:ns3="http://schemas.microsoft.com/exchange/services/2006/types" Version="requestServerVersion"/><t:ExchangeImpersonation xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"><t:ConnectingSID><t:PrincipalName>impersonatedUser@ed.ac.uk</t:PrincipalName></t:ConnectingSID></t:ExchangeImpersonation></SOAP-ENV:Header> * <SOAP-ENV:Body> * <ns2:FindItem xmlns:ns2="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:ns3="http://schemas.microsoft.com/exchange/services/2006/types" Traversal="Shallow"> * <ns2:ItemShape> * <ns3:BaseShape>AllProperties</ns3:BaseShape> * </ns2:ItemShape> * <ns2:ParentFolderIds> * <ns3:DistinguishedFolderId Id="inbox"/> * </ns2:ParentFolderIds> * </ns2:FindItem> * </SOAP-ENV:Body> * </SOAP-ENV:Envelope> * </pre> * * @param request Portlet request * @param calendar calendar configuration * @param interval interval to retrieve events for * @param emailAddress email address of the user to retrieve events for * @return Set of calendar events from the Exchange Server. * @throws CalendarException */ private Set<VEvent> retrieveExchangeEvents( PortletRequest request, CalendarConfiguration calendar, Interval interval, String emailAddress) throws CalendarException { log.debug("Retrieving exchange events for period: {}", interval); Set<VEvent> events = new HashSet<VEvent>(); try { // construct the SOAP request object to use GetUserAvailabilityRequest soapRequest = getAvailabilityRequest(interval, emailAddress); final WebServiceMessageCallback customCallback = new ExchangeWebServiceCallBack( AVAILABILITY_SOAP_ACTION, requestServerVersion, credentialsService.getImpersonatedAccountId(request)); // use the request to retrieve data from the Exchange server GetUserAvailabilityResponse response = (GetUserAvailabilityResponse) webServiceOperations.marshalSendAndReceive(soapRequest, customCallback); // for each FreeBusy response, parse the list of events for (FreeBusyResponseType freeBusy : response.getFreeBusyResponseArray().getFreeBusyResponses()) { ArrayOfCalendarEvent eventArray = freeBusy.getFreeBusyView().getCalendarEventArray(); if (eventArray != null && eventArray.getCalendarEvents() != null) { List<com.microsoft.exchange.types.CalendarEvent> msEvents = eventArray.getCalendarEvents(); for (com.microsoft.exchange.types.CalendarEvent msEvent : msEvents) { // add the new event to the list VEvent event = getInternalEvent(calendar.getId(), msEvent); events.add(event); } } } } catch (DatatypeConfigurationException e) { throw new CalendarException(e); } return events; } protected GetUserAvailabilityRequest getAvailabilityRequest( Interval interval, String emailAddress) throws DatatypeConfigurationException { // construct the SOAP request object to use GetUserAvailabilityRequest soapRequest = new GetUserAvailabilityRequest(); // create an array of mailbox data representing the current user ArrayOfMailboxData mailboxes = new ArrayOfMailboxData(); MailboxData mailbox = new MailboxData(); Mailbox address = new Mailbox(); address.setAddress(emailAddress); address.setName(""); mailbox.setAttendeeType(MeetingAttendeeType.REQUIRED); mailbox.setExcludeConflicts(false); mailbox.setEmail(address); mailboxes.getMailboxDatas().add(mailbox); soapRequest.setMailboxDataArray(mailboxes); // create a FreeBusyViewOptions representing the specified period FreeBusyViewOptions view = new FreeBusyViewOptions(); view.setMergedFreeBusyIntervalInMinutes(60); view.getRequestedView().add("DetailedMerged"); Duration dur = new Duration(); XMLGregorianCalendar start = getXmlDate(interval.getStart()); XMLGregorianCalendar end = getXmlDate(interval.getEnd()); dur.setEndTime(end); dur.setStartTime(start); view.setTimeWindow(dur); soapRequest.setFreeBusyViewOptions(view); // set the bias to the start time's timezone offset (in minutes // rather than milliseconds) TimeZone tz = new TimeZone(); java.util.TimeZone tZone = java.util.TimeZone.getTimeZone(UTC); tz.setBias(tZone.getRawOffset() / 1000 / 60); // TODO: time zone standard vs. daylight info is temporarily hard-coded SerializableTimeZoneTime standard = new SerializableTimeZoneTime(); standard.setBias(0); standard.setDayOfWeek(DayOfWeekType.SUNDAY); standard.setDayOrder((short) 1); standard.setMonth((short) 11); standard.setTime("02:00:00"); SerializableTimeZoneTime daylight = new SerializableTimeZoneTime(); daylight.setBias(0); daylight.setDayOfWeek(DayOfWeekType.SUNDAY); daylight.setDayOrder((short) 1); daylight.setMonth((short) 3); daylight.setTime("02:00:00"); tz.setStandardTime(standard); tz.setDaylightTime(daylight); soapRequest.setTimeZone(tz); return soapRequest; } /** * Return an internal API CalendarEvent for an Microsoft CalendarEvent object. * * @param calendarId * @param msEvent * @return * @throws DatatypeConfigurationException */ protected VEvent getInternalEvent( long calendarId, com.microsoft.exchange.types.CalendarEvent msEvent) throws DatatypeConfigurationException { DatatypeFactory factory = DatatypeFactory.newInstance(); // create a new UTC-based DateTime to represent the event start time net.fortuna.ical4j.model.DateTime eventStart = new net.fortuna.ical4j.model.DateTime(); eventStart.setUtc(true); Calendar startCal = msEvent .getStartTime() .toGregorianCalendar( java.util.TimeZone.getTimeZone(UTC), Locale.getDefault(), factory.newXMLGregorianCalendar()); eventStart.setTime(startCal.getTimeInMillis()); // create a new UTC-based DateTime to represent the event ent time net.fortuna.ical4j.model.DateTime eventEnd = new net.fortuna.ical4j.model.DateTime(); eventEnd.setUtc(true); Calendar endCal = msEvent .getEndTime() .toGregorianCalendar( java.util.TimeZone.getTimeZone(UTC), Locale.getDefault(), factory.newXMLGregorianCalendar()); eventEnd.setTime(endCal.getTimeInMillis()); // create a property list representing the new event PropertyList newprops = new PropertyList(); newprops.add(new Uid(msEvent.getCalendarEventDetails().getID())); newprops.add(new DtStamp()); newprops.add(new DtStart(eventStart)); newprops.add(new DtEnd(eventEnd)); newprops.add(new Summary(msEvent.getCalendarEventDetails().getSubject())); if (StringUtils.isNotBlank(msEvent.getCalendarEventDetails().getLocation())) { newprops.add(new Location(msEvent.getCalendarEventDetails().getLocation())); } VEvent event = new VEvent(newprops); return event; } /** * Get an XMLGregorianCalendar for the specified date. * * @param date * @return * @throws DatatypeConfigurationException */ protected XMLGregorianCalendar getXmlDate(DateTime date) throws DatatypeConfigurationException { // construct an XMLGregorianCalendar DatatypeFactory datatypeFactory = DatatypeFactory.newInstance(); XMLGregorianCalendar start = datatypeFactory.newXMLGregorianCalendar(); start.setYear(date.getYear()); start.setMonth(date.getMonthOfYear()); start.setTime( date.getHourOfDay(), date.getMinuteOfHour(), date.getSecondOfMinute(), date.getMillisOfSecond()); start.setDay(date.getDayOfMonth()); return start; } }