/// <summary> /// Returns the free busy times for the specified exchange users /// </summary> /// <param name="users">The user which free/busy blocks will be looked up for</param> /// <param name="window">The time period to look up FB info for</param> /// <returns></returns> public Dictionary<ExchangeUser, FreeBusy> LookupFreeBusyTimes( ExchangeUserDict users, DateTimeRange window ) { /* Create an array of mailboxes to retrieve from exchange */ Dictionary<ExchangeUser, FreeBusy> result = new Dictionary<ExchangeUser, FreeBusy>(); try { using (BlockTimer bt = new BlockTimer("LookupFreeBusyTimes")) { /* Perform the retrieval of free busy times through WebDav */ result = webDavQuery.LoadFreeBusy(exchangeServerUrl, users, window); } } catch (Exception ex) { throw new GCalExchangeException( GCalExchangeErrorCode.ExchangeUnreachable, "Error occured while retrieving free busy ranges", ex); } return result; }
/// <summary> /// Load a set of appointments from the server /// </summary> /// <param name="folderUrl">The mailbox URL</param> /// <param name="start">Start time of date range to fetch</param> /// <param name="end">End time of date range to fetch</param> /// <returns>A list of appointments from the mailbox</returns> public virtual List<Appointment> LoadAppointments( string folderUrl, DateTime start, DateTime end) { using (BlockTimer bt = new BlockTimer("LoadAppointments")) { string request = WebDavXmlResources.GetText( WebDavXmlResources.FindAppointments, folderUrl, DateUtil.FormatDateForDASL(start), DateUtil.FormatDateForDASL(end)); string response = IssueRequestAndFetchReponse(folderUrl, Method.SEARCH, request); XmlDocument responseXML = ParseExchangeXml(response); return ParseAppointmentResultSetXml(responseXML); } }
/// <summary> /// Perform sync of users from Google Calendar to Exchnage until there are no more /// users to sync. This can be called from a worker thread. /// </summary> public void SyncUsers() { ExchangeUser user; int userCount = 0; string login = string.Empty; errorThreshold = ConfigCache.ServiceErrorCountThreshold; errorCount = 0; while ( exchangeUsers.Count > 0 ) { lock ( lockObject ) { if ( exchangeUsers.Count == 0 ) { break; } else { user = exchangeUsers[0]; exchangeUsers.RemoveAt( 0 ); } } try { userCount++; login = user.Email.ToLower(); DateTime modifiedDate = modifiedDateUtil.GetModifiedDateForUser(login); DateTime currentDate = DateUtil.NowUtc; // Pick a window to synchronize for: // // [-N, +2N] days where N is settable in the config file // // Scanning back in time is necessary so that we pickup changes to meetings and events that were // made invisible. // // TODO: The window we're syncing for should also be used when we modify events in Exchange // so we only modify events in the window. DateTime start = currentDate.AddDays(-ConfigCache.GCalSyncWindow); DateTime end = currentDate.AddDays(2 * ConfigCache.GCalSyncWindow); DateTimeRange syncWindow = new DateTimeRange(start, end); log.InfoFormat("Processing user {0} for {1}", login, syncWindow); EventFeed feed = gcalGateway.QueryGCal( user.Email, GCalVisibility.Private, ConfigCache.FreeBusyDetailLevel, modifiedDate, syncWindow); // if feed is null, then that means no calendar items changed for the user since we last queried. if ( feed == null ) { log.DebugFormat( "Calendar has not changed for user {0}. User will not be synced this round.", login ); continue; } if ( !ValidateFeed( feed ) ) { /* No Google App Feed was returned, skip to next user */ log.WarnFormat( "GCal feed could not be read for '{0}'. This user may not have activated their account or may be inactive." + "For resources the admin must share their calendars for the domain and set correctly the timezone.", login ); continue; } log.InfoFormat("Calendar Query returned {0} events", feed.Entries.Count); using (BlockTimer freeBusyTimer = new BlockTimer("WriteFreeBusy")) { /* User and feed retrieval was succesful, merge the two datasources down into Exchange */ freeBusyWriter.SyncUser( user, feed, exchangeGateway, syncWindow ); } // Only update the modified time if we sync'd modifiedDateUtil.UpdateUserModifiedTime(login, currentDate); } catch ( Exception ex ) { Interlocked.Increment( ref errorCount ); log.Error( string.Format( "Error occured while executing sync process for user '{0}'. [running error count={1}]", login, errorCount ), ex ); } if ( errorCount > errorThreshold ) throw new GCalExchangeException( GCalExchangeErrorCode.GenericError, "Error threshold has been surpassed, aborting sync process." ); } log.InfoFormat( "User synchronization complete. {0} users processed.", userCount ); }
/// <summary> /// Run the sync process from Google Calendar to Exchange /// </summary> /// <param name="threadCount">Number of threads to use to sync</param> public void RunSyncProcess( int threadCount ) { log.Info( "Exchange synchronization process started." ); using ( BlockTimer bt = new BlockTimer( "RunSyncProcess" ) ) { gcalGateway = new GCalGateway(googleAppsLogin, googleAppsPassword, googleAppsDomain, null); exchangeGateway = new ExchangeService(exchangeServer, networkLogin, networkPassword); ExchangeUserDict users; using ( BlockTimer loadUserTimer = new BlockTimer( "LoadUserList" ) ) { users = QueryExchangeForValidUsers(); } exchangeUsers = new List<ExchangeUser>(); freeBusyWriter = FreeBusyWriterFactory.GetWriter( exchangeUsers ); foreach ( ExchangeUser user in users.Values ) { exchangeUsers.Add( user ); } if ( exchangeUsers.Count == 0 ) { log.Warn( "No eligible users found for synchronization. Aborting sync process." ); return; } if ( threadCount < 1 ) threadCount = 1; modifiedDateUtil = new ModifiedDateUtil( ConfigCache.ServiceModifiedXmlFileName ); modifiedDateUtil.LoadModifiedTimes(); if ( threadCount == 1 ) { SyncUsers(); } else { StartSyncUserThreads( threadCount ); } modifiedDateUtil.PersistModifiedTimes(); gcalGateway = null; exchangeGateway = null; freeBusyWriter = null; modifiedDateUtil = null; exchangeUsers = null; System.GC.Collect(); log.DebugFormat("Memory after sync: {0}", System.GC.GetTotalMemory(false)); } }
/// <summary> /// Combines the free busy and appointment blocks supplied to the exchange user object /// If no appointments are supplied the user will still have free busy time blocks assigned /// to them, with a null appointment assigned to the free busy time. /// </summary> /// <param name="exchangeUser">Exchange users to apply freeBusy and appointments</param> /// <param name="freeBusy">The collection of FreeBusy blocks to assign to exchangeUser</param> /// <param name="appointments">The collection of appointment blocks to assign to exchangeUser</param> /// <param name="window">Window to merge for</param> protected void MergeFreeBusyWithAppointments( ExchangeUser exchangeUser, FreeBusy freeBusy, List<Appointment> appointments, DateTimeRange window) { using (BlockTimer bt = new BlockTimer("MergeFreeBusyWithAppointments")) { IntervalTree<FreeBusyTimeBlock> busyIntervals = new IntervalTree<FreeBusyTimeBlock>(); FreeBusyCollection busyTimes = new FreeBusyCollection(); int appointmentsCount = 0; List<DateTimeRange> combinedTimes = FreeBusyConverter.MergeFreeBusyLists(freeBusy.All, freeBusy.Tentative); /* Add the date ranges from each collection in the FreeBusy object */ ConvertFreeBusyToBlocks(window, combinedTimes, busyTimes, busyIntervals); if (appointments != null && appointments.Count > 0) { appointmentsCount = appointments.Count; foreach (Appointment appt in appointments) { log.DebugFormat("Appt \"{0}\" {1} {2} response = {3} status = {4} busy = {5}", appt.Subject, appt.Range, appt.StartDate.Kind, appt.ResponseStatus, appt.MeetingStatus, appt.BusyStatus); if (appt.BusyStatus == BusyStatus.Free) { continue; } DateTimeRange range = new DateTimeRange(appt.StartDate, appt.EndDate); List<FreeBusyTimeBlock> result = busyIntervals.FindAll(range, IntervalTreeMatch.Overlap); log.DebugFormat("Found {0} ranges overlap {1}", result.Count, range); foreach (FreeBusyTimeBlock block in result) { log.DebugFormat("Adding \"{0}\" to FB {1} {2}", appt.Subject, block.Range, block.StartDate.Kind); block.Appointments.Add(appt); } busyTimes.Appointments.Add(appt); } } foreach (FreeBusyTimeBlock block in busyTimes.Values) { block.Appointments.Sort(CompareAppointmentsByRanges); } log.InfoFormat("Merge Result of {0} + Appointment {1} -> {2}", combinedTimes.Count, appointmentsCount, busyTimes.Count); /* Assign the data structure to the exchange user */ exchangeUser.BusyTimes = busyTimes; } }
private EventFeed QueryGCal(FeedQuery query, string userName, DateTime modifiedSince) { if (log.IsInfoEnabled) log.Info(String.Format("Querying GCal for User '{0}': {1}", userName, query.Uri)); EventFeed feed = null; // Wait as necessary before making new request. connectionThrottler.WaitBeforeNewConnection(); try { using (BlockTimer bt = new BlockTimer("QueryGCal")) { feed = service.Query(query, modifiedSince) as EventFeed; } } catch (GDataNotModifiedException e) { // Content was not modified log.InfoFormat("NotModified: {0}", e.Message); } catch (Exception) { // Report a failure, regardless of the exception, as long as it is not GDataNotModifiedException. // This could be a bit overly aggressive, but it is hard to make bets // what exception was caught and rethrown in GData and what was let to fly. connectionThrottler.ReportFailure(); throw; } // Everything went well, report it. // Note this is valid even when we caught GDataNotModifiedException. connectionThrottler.ReportSuccess(); LogResponse(feed, userName); return feed; }