[CalendarServer-changes] [14683] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Fri Apr 17 11:11:46 PDT 2015


Revision: 14683
          http://trac.calendarserver.org//changeset/14683
Author:   cdaboo at apple.com
Date:     2015-04-17 11:11:45 -0700 (Fri, 17 Apr 2015)
Log Message:
-----------
Refactor freebusy queries and remove a bunch of duplicate code, in preparation for trying to do some optimizations.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/freebusyurl.py
    CalendarServer/trunk/twistedcaldav/method/report_common.py
    CalendarServer/trunk/twistedcaldav/method/report_freebusy.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py
    CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_freebusy.py
    CalendarServer/trunk/txdav/common/datastore/podding/store_api.py
    CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py

Modified: CalendarServer/trunk/twistedcaldav/freebusyurl.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/freebusyurl.py	2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/twistedcaldav/freebusyurl.py	2015-04-17 18:11:45 UTC (rev 14683)
@@ -26,7 +26,6 @@
 
 from twext.python.log import Logger
 from txweb2 import responsecode
-from txdav.xml import element as davxml
 from txweb2.dav.http import ErrorResponse
 from txweb2.dav.util import joinURL
 from txweb2.http import HTTPError
@@ -36,19 +35,19 @@
 from txweb2.stream import MemoryStream
 
 from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import TimeRange
 from twistedcaldav.config import config
 from twistedcaldav.customxml import calendarserver_namespace
 from twistedcaldav.ical import Property
 from twistedcaldav.resource import CalDAVResource, ReadOnlyNoCopyResourceMixIn
 from twistedcaldav.scheduling_store.caldav.resource import deliverSchedulePrivilegeSet
 
-from txdav.caldav.datastore.scheduling.caldav.delivery import ScheduleViaCalDAV
 from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser
-from txdav.caldav.datastore.scheduling.scheduler import Scheduler
+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
+from txdav.xml import element as davxml
 
 from pycalendar.datetime import DateTime
 from pycalendar.duration import Duration
+from pycalendar.period import Period
 
 log = Logger()
 
@@ -266,26 +265,16 @@
         if inbox is None:
             raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox for principal: %s" % (principal,)))
 
-        scheduler = Scheduler(request, self)
-        scheduler.timeRange = TimeRange(start="20000101T000000Z", end="20070102T000000Z")
-        scheduler.timeRange.start = self.start
-        scheduler.timeRange.end = self.end
+        organizer = recipient = LocalCalendarUser(cuaddr, principal.record)
+        recipient.inbox = inbox._newStoreObject
+        attendeeProp = Property("ATTENDEE", recipient.cuaddr)
+        timerange = Period(self.start, self.end)
 
-        scheduler.organizer = LocalCalendarUser(cuaddr, principal.record)
-        scheduler.organizer.inbox = inbox._newStoreObject
+        fbresult = (yield FreebusyQuery(
+            organizer, None, recipient, attendeeProp, None,
+            timerange, None, None,
+        ).generateAttendeeFreeBusyResponse())
 
-        attendeeProp = Property("ATTENDEE", scheduler.organizer.cuaddr)
-
-        requestor = ScheduleViaCalDAV(scheduler, (), [], True)
-        fbresult = (yield requestor.generateAttendeeFreeBusyResponse(
-            scheduler.organizer,
-            None,
-            None,
-            None,
-            attendeeProp,
-            True,
-        ))
-
         response = Response()
         response.stream = MemoryStream(str(fbresult))
         response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (self.format,)))

Modified: CalendarServer/trunk/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_common.py	2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/twistedcaldav/method/report_common.py	2015-04-17 18:11:45 UTC (rev 14683)
@@ -29,55 +29,33 @@
     "buildFreeBusyResult",
 ]
 
-import time
-
-try:
-    from hashlib import md5
-except ImportError:
-    from md5 import new as md5
-
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.python.failure import Failure
 
 from txweb2 import responsecode
 from txweb2.dav.http import statusForFailure
 from txweb2.dav.method.propfind import propertyName
-from txweb2.dav.method.report import NumberOfMatchesWithinLimits
-from txweb2.dav.method.report import max_number_of_matches
 from txweb2.dav.resource import AccessDeniedError
-from txweb2.http import HTTPError, StatusResponse
+from txweb2.http import HTTPError
 
 from twext.python.log import Logger
 
 from twistedcaldav import caldavxml
 from twistedcaldav import carddavxml
-from twistedcaldav.caldavxml import CalendarData, CalendarTimeZone, TimeRange
+from twistedcaldav.caldavxml import CalendarData
 from twistedcaldav.carddavxml import AddressData
-from twistedcaldav.config import config
 from twistedcaldav.datafilters.calendardata import CalendarDataFilter
 from twistedcaldav.datafilters.hiddeninstance import HiddenInstanceFilter
 from twistedcaldav.datafilters.privateevents import PrivateEventFilter
 from twistedcaldav.datafilters.addressdata import AddressDataFilter
-from twistedcaldav.dateops import clipPeriod, normalizePeriodList, timeRangesOverlap, \
-    compareDateTime, normalizeToUTC, parseSQLTimestampToPyCalendar
-from twistedcaldav.ical import Component, Property, iCalendarProductID
-from twistedcaldav.instance import InstanceList
-from twistedcaldav.memcacher import Memcacher
 
-from txdav.caldav.datastore.query.filter import Filter
-from txdav.common.icommondatastore import IndexedSearchException
 from txdav.xml import element
 
-from pycalendar.duration import Duration
-from pycalendar.datetime import DateTime
-from pycalendar.timezone import Timezone
-from pycalendar.period import Period
-
 log = Logger()
 
 COLLECTION_TYPE_REGULAR = "collection"
 COLLECTION_TYPE_CALENDAR = "calendar"
-COLLECTION_TYPE_ADDRESSBOOK = "adressbook"
+COLLECTION_TYPE_ADDRESSBOOK = "addressbook"
 
 @inlineCallbacks
 def applyToCalendarCollections(resource, request, request_uri, depth, apply, privileges):
@@ -422,607 +400,3 @@
             properties_by_status[responsecode.NOT_FOUND].append(propertyName(qname))
 
     returnValue(properties_by_status)
-
-fbtype_mapper = {"BUSY": 0, "BUSY-TENTATIVE": 1, "BUSY-UNAVAILABLE": 2}
-fbtype_index_mapper = {'B': 0, 'T': 1, 'U': 2}
-
-fbcacher = Memcacher("FBCache", pickle=True)
-
-class FBCacheEntry(object):
-
-    CACHE_DAYS_FLOATING_ADJUST = 1
-
-    def __init__(self, key, token, timerange, fbresults):
-        self.key = key
-        self.token = token
-        self.timerange = timerange
-        self.fbresults = fbresults
-
-
-    @classmethod
-    @inlineCallbacks
-    def getCacheEntry(cls, calresource, useruid, timerange):
-
-        key = calresource.resourceID() + "/" + useruid
-        token = (yield calresource.getInternalSyncToken())
-        entry = (yield fbcacher.get(key))
-
-        if entry:
-
-            # Offset one day at either end to account for floating
-            cached_start = entry.timerange.start + Duration(days=FBCacheEntry.CACHE_DAYS_FLOATING_ADJUST)
-            cached_end = entry.timerange.end - Duration(days=FBCacheEntry.CACHE_DAYS_FLOATING_ADJUST)
-
-            # Verify that the requested timerange lies within the cache timerange
-            if compareDateTime(timerange.end, cached_end) <= 0 and compareDateTime(timerange.start, cached_start) >= 0:
-
-                # Verify that cached entry is still valid
-                if token == entry.token:
-                    returnValue(entry.fbresults)
-
-        returnValue(None)
-
-
-    @classmethod
-    @inlineCallbacks
-    def makeCacheEntry(cls, calresource, useruid, timerange, fbresults):
-
-        key = calresource.resourceID() + "/" + useruid
-        token = (yield calresource.getInternalSyncToken())
-        entry = cls(key, token, timerange, fbresults)
-        yield fbcacher.set(key, entry)
-
-
-
- at inlineCallbacks
-def generateFreeBusyInfo(
-    request,
-    calresource,
-    fbinfo,
-    timerange,
-    matchtotal,
-    excludeuid=None,
-    organizer=None,
-    organizerPrincipal=None,
-    same_calendar_user=False,
-    servertoserver=False,
-    event_details=None,
-):
-    """
-    Run a free busy report on the specified calendar collection
-    accumulating the free busy info for later processing.
-    @param request:     the L{IRequest} for the current request.
-    @param calresource: the L{CalDAVResource} for a calendar collection.
-    @param fbinfo:      the array of busy periods to update.
-    @param timerange:   the L{TimeRange} for the query.
-    @param matchtotal:  the running total for the number of matches.
-    @param excludeuid:  a C{str} containing a UID value to exclude any
-        components with that UID from contributing to free-busy.
-    @param organizer:   a C{str} containing the value of the ORGANIZER property
-        in the VFREEBUSY request.  This is used in conjunction with the UID
-        value to process exclusions.
-    @param same_calendar_user: a C{bool} indicating whether the calendar user
-        requesting the free-busy information is the same as the calendar user
-        being targeted.
-    @param servertoserver: a C{bool} indicating whether we are doing a local or
-        remote lookup request.
-    @param event_details: a C{list} into which to store extended VEVENT details if not C{None}
-    """
-
-    # First check the privilege on this collection
-    # TODO: for server-to-server we bypass this right now as we have no way to authorize external users.
-    if not servertoserver:
-        try:
-            yield calresource.checkPrivileges(request, (caldavxml.ReadFreeBusy(),), principal=organizerPrincipal)
-        except AccessDeniedError:
-            returnValue(matchtotal)
-
-    # May need organizer principal
-    organizer_principal = (yield calresource.principalForCalendarUserAddress(organizer)) if organizer else None
-    organizer_uid = organizer_principal.principalUID() if organizer_principal else ""
-
-    # Free busy is per-user
-    userPrincipal = (yield calresource.resourceOwnerPrincipal(request))
-    if userPrincipal:
-        useruid = userPrincipal.principalUID()
-    else:
-        useruid = ""
-
-    # Get the timezone property from the collection.
-    has_prop = (yield calresource.hasProperty(CalendarTimeZone(), request))
-    if has_prop:
-        tz = (yield calresource.readProperty(CalendarTimeZone(), request))
-    else:
-        tz = None
-
-    # Look for possible extended free busy information
-    rich_options = {
-        "organizer": False,
-        "delegate": False,
-        "resource": False,
-    }
-    do_event_details = False
-    if event_details is not None and organizer_principal is not None and userPrincipal is not None:
-
-        # Check if organizer is attendee
-        if organizer_principal == userPrincipal:
-            do_event_details = True
-            rich_options["organizer"] = True
-
-        # Check if organizer is a delegate of attendee
-        proxy = (yield organizer_principal.isProxyFor(userPrincipal))
-        if config.Scheduling.Options.DelegeteRichFreeBusy and proxy:
-            do_event_details = True
-            rich_options["delegate"] = True
-
-        # Check if attendee is room or resource
-        if config.Scheduling.Options.RoomResourceRichFreeBusy and userPrincipal.getCUType() in ("RESOURCE", "ROOM",):
-            do_event_details = True
-            rich_options["resource"] = True
-
-    # Try cache
-    resources = (yield FBCacheEntry.getCacheEntry(calresource, useruid, timerange)) if config.EnableFreeBusyCache else None
-
-    if resources is None:
-
-        caching = False
-        if config.EnableFreeBusyCache:
-            # Log extended item
-            if not hasattr(request, "extendedLogItems"):
-                request.extendedLogItems = {}
-            request.extendedLogItems["fb-uncached"] = request.extendedLogItems.get("fb-uncached", 0) + 1
-
-            # We want to cache a large range of time based on the current date
-            cache_start = normalizeToUTC(DateTime.getToday() + Duration(days=0 - config.FreeBusyCacheDaysBack))
-            cache_end = normalizeToUTC(DateTime.getToday() + Duration(days=config.FreeBusyCacheDaysForward))
-
-            # If the requested timerange would fit in our allowed cache range, trigger the cache creation
-            if compareDateTime(timerange.start, cache_start) >= 0 and compareDateTime(timerange.end, cache_end) <= 0:
-                cache_timerange = TimeRange(start=cache_start.getText(), end=cache_end.getText())
-                caching = True
-
-        #
-        # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range.
-        # We then take those results and merge them into one VFREEBUSY component
-        # with appropriate FREEBUSY properties, and return that single item as iCal data.
-        #
-
-        # Create fake filter element to match time-range
-        filter = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                caldavxml.ComponentFilter(
-                    cache_timerange if caching else timerange,
-                    name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
-                ),
-                name="VCALENDAR",
-            )
-        )
-        filter = Filter(filter)
-        tzinfo = filter.settimezone(tz)
-
-        try:
-            resources = yield calresource.search(filter, useruid=useruid, fbtype=True)
-            if caching:
-                yield FBCacheEntry.makeCacheEntry(calresource, useruid, cache_timerange, resources)
-        except IndexedSearchException:
-            raise HTTPError(StatusResponse(
-                responsecode.INTERNAL_SERVER_ERROR,
-                "Failed freebusy query"
-            ))
-
-    else:
-        # Log extended item
-        if not hasattr(request, "extendedLogItems"):
-            request.extendedLogItems = {}
-        request.extendedLogItems["fb-cached"] = request.extendedLogItems.get("fb-cached", 0) + 1
-
-        # Determine appropriate timezone (UTC is the default)
-        tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True)
-
-    # We care about separate instances for VEVENTs only
-    aggregated_resources = {}
-    for name, uid, type, test_organizer, float, start, end, fbtype, transp in resources:
-        if transp == 'T' and fbtype != '?':
-            fbtype = 'F'
-        aggregated_resources.setdefault((name, uid, type, test_organizer,), []).append((float, start, end, fbtype,))
-
-    for key in aggregated_resources.iterkeys():
-
-        name, uid, type, test_organizer = key
-
-        # Short-cut - if an fbtype exists we can use that
-        if type == "VEVENT" and aggregated_resources[key][0][3] != '?':
-
-            matchedResource = False
-
-            # Look at each instance
-            for float, start, end, fbtype in aggregated_resources[key]:
-                # Ignore free time or unknown
-                if fbtype in ('F', '?'):
-                    continue
-
-                # Ignore ones of this UID
-                if excludeuid:
-                    # See if we have a UID match
-                    if (excludeuid == uid):
-                        test_principal = (yield calresource.principalForCalendarUserAddress(test_organizer)) if test_organizer else None
-                        test_uid = test_principal.principalUID() if test_principal else ""
-
-                        # Check that ORGANIZER's match (security requirement)
-                        if (organizer is None) or (organizer_uid == test_uid):
-                            continue
-                        # Check for no ORGANIZER and check by same calendar user
-                        elif (test_uid == "") and same_calendar_user:
-                            continue
-
-                # Apply a timezone to any floating times
-                fbstart = parseSQLTimestampToPyCalendar(start)
-                if float == 'Y':
-                    fbstart.setTimezone(tzinfo)
-                else:
-                    fbstart.setTimezone(Timezone(utc=True))
-                fbend = parseSQLTimestampToPyCalendar(end)
-                if float == 'Y':
-                    fbend.setTimezone(tzinfo)
-                else:
-                    fbend.setTimezone(Timezone(utc=True))
-
-                # Clip instance to time range
-                clipped = clipPeriod(Period(fbstart, duration=fbend - fbstart), Period(timerange.start, timerange.end))
-
-                # Double check for overlap
-                if clipped:
-                    matchedResource = True
-                    fbinfo[fbtype_index_mapper.get(fbtype, 0)].append(clipped)
-
-            if matchedResource:
-                # Check size of results is within limit
-                matchtotal += 1
-                if matchtotal > max_number_of_matches:
-                    raise NumberOfMatchesWithinLimits(max_number_of_matches)
-
-                # Add extended details
-                if do_event_details:
-                    child = (yield request.locateChildResource(calresource, name))
-                    calendar = (yield child.componentForUser())
-                    _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo)
-
-        else:
-            child = (yield request.locateChildResource(calresource, name))
-            calendar = (yield child.componentForUser())
-
-            # The calendar may come back as None if the resource is being changed, or was deleted
-            # between our initial index query and getting here. For now we will ignore this error, but in
-            # the longer term we need to implement some form of locking, perhaps.
-            if calendar is None:
-                log.error("Calendar %s is missing from calendar collection %r" % (name, calresource))
-                continue
-
-            # Ignore ones of this UID
-            if excludeuid:
-                # See if we have a UID match
-                if (excludeuid == uid):
-                    test_organizer = calendar.getOrganizer()
-                    test_principal = (yield calresource.principalForCalendarUserAddress(test_organizer)) if test_organizer else None
-                    test_uid = test_principal.principalUID() if test_principal else ""
-
-                    # Check that ORGANIZER's match (security requirement)
-                    if (organizer is None) or (organizer_uid == test_uid):
-                        continue
-                    # Check for no ORGANIZER and check by same calendar user
-                    elif (test_organizer is None) and same_calendar_user:
-                        continue
-
-            if filter.match(calendar, None):
-                # Check size of results is within limit
-                matchtotal += 1
-                if matchtotal > max_number_of_matches:
-                    raise NumberOfMatchesWithinLimits(max_number_of_matches)
-
-                if calendar.mainType() == "VEVENT":
-                    processEventFreeBusy(calendar, fbinfo, timerange, tzinfo)
-                elif calendar.mainType() == "VFREEBUSY":
-                    processFreeBusyFreeBusy(calendar, fbinfo, timerange)
-                elif calendar.mainType() == "VAVAILABILITY":
-                    processAvailabilityFreeBusy(calendar, fbinfo, timerange)
-                else:
-                    assert "Free-busy query returned unwanted component: %s in %r", (name, calresource,)
-
-                # Add extended details
-                if calendar.mainType() == "VEVENT" and do_event_details:
-                    child = (yield request.locateChildResource(calresource, name))
-                    calendar = (yield child.componentForUser())
-                    _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo)
-
-    returnValue(matchtotal)
-
-
-
-def _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo):
-    """
-    Expand events within the specified time range and limit the set of properties to those allowed for
-    delegate extended free busy.
-
-    @param calendar: the calendar object to expand
-    @type calendar: L{Component}
-    @param event_details: list to append VEVENT components to
-    @type event_details: C{list}
-    @param timerange: the time-range in which to expand
-    @type timerange: L{TimeRange}
-    @param tzinfo: timezone for floating time calculations
-    @type tzinfo: L{Timezone}
-    """
-
-    # First expand the component
-    expanded = calendar.expand(timerange.start, timerange.end, timezone=tzinfo)
-
-    keep_props = (
-        "UID",
-        "RECURRENCE-ID",
-        "DTSTAMP",
-        "DTSTART",
-        "DTEND",
-        "DURATION",
-    )
-
-    if rich_options["organizer"] or rich_options["delegate"]:
-        keep_props += ("SUMMARY",)
-
-    if rich_options["organizer"] or rich_options["resource"]:
-        keep_props += ("ORGANIZER",)
-
-    # Remove all but essential properties
-    expanded.filterProperties(keep=keep_props)
-
-    # Need to remove all child components of VEVENT
-    for subcomponent in expanded.subcomponents():
-        if subcomponent.name() == "VEVENT":
-            for sub in tuple(subcomponent.subcomponents()):
-                subcomponent.removeComponent(sub)
-
-    event_details.extend([subcomponent for subcomponent in expanded.subcomponents() if subcomponent.name() == "VEVENT"])
-
-
-
-def processEventFreeBusy(calendar, fbinfo, timerange, tzinfo):
-    """
-    Extract free busy data from a VEVENT component.
-    @param calendar: the L{Component} that is the VCALENDAR containing the VEVENT's.
-    @param fbinfo: the tuple used to store the three types of fb data.
-    @param timerange: the time range to restrict free busy data to.
-    @param tzinfo: the L{Timezone} for the timezone to use for floating/all-day events.
-    """
-
-    # Expand out the set of instances for the event with in the required range
-    instances = calendar.expandTimeRanges(timerange.end, lowerLimit=timerange.start, ignoreInvalidInstances=True)
-
-    # Can only do timed events
-    for key in instances:
-        instance = instances[key]
-        if instance.start.isDateOnly():
-            return
-        break
-    else:
-        return
-
-    for key in instances:
-        instance = instances[key]
-
-        # Apply a timezone to any floating times
-        fbstart = instance.start
-        if fbstart.floating():
-            fbstart.setTimezone(tzinfo)
-        fbend = instance.end
-        if fbend.floating():
-            fbend.setTimezone(tzinfo)
-
-        # Check TRANSP property of underlying component
-        if instance.component.hasProperty("TRANSP"):
-            # If its TRANSPARENT we always ignore it
-            if instance.component.propertyValue("TRANSP") == "TRANSPARENT":
-                continue
-
-        # Determine status
-        if instance.component.hasProperty("STATUS"):
-            status = instance.component.propertyValue("STATUS")
-        else:
-            status = "CONFIRMED"
-
-        # Ignore cancelled
-        if status == "CANCELLED":
-            continue
-
-        # Clip period for this instance - use duration for period end if that
-        # is what original component used
-        if instance.component.hasProperty("DURATION"):
-            period = Period(fbstart, duration=fbend - fbstart)
-        else:
-            period = Period(fbstart, fbend)
-        clipped = clipPeriod(period, Period(timerange.start, timerange.end))
-
-        # Double check for overlap
-        if clipped:
-            if status == "TENTATIVE":
-                fbinfo[1].append(clipped)
-            else:
-                fbinfo[0].append(clipped)
-
-
-
-def processFreeBusyFreeBusy(calendar, fbinfo, timerange):
-    """
-    Extract FREEBUSY data from a VFREEBUSY component.
-    @param calendar: the L{Component} that is the VCALENDAR containing the VFREEBUSY's.
-    @param fbinfo: the tuple used to store the three types of fb data.
-    @param timerange: the time range to restrict free busy data to.
-    """
-
-    for vfb in [x for x in calendar.subcomponents() if x.name() == "VFREEBUSY"]:
-        # First check any start/end in the actual component
-        start = vfb.getStartDateUTC()
-        end = vfb.getEndDateUTC()
-        if start and end:
-            if not timeRangesOverlap(start, end, timerange.start, timerange.end):
-                continue
-
-        # Now look at each FREEBUSY property
-        for fb in vfb.properties("FREEBUSY"):
-            # Check the type
-            fbtype = fb.parameterValue("FBTYPE", default="BUSY")
-            if fbtype == "FREE":
-                continue
-
-            # Look at each period in the property
-            assert isinstance(fb.value(), list), "FREEBUSY property does not contain a list of values: %r" % (fb,)
-            for period in fb.value():
-                # Clip period for this instance
-                clipped = clipPeriod(period.getValue(), Period(timerange.start, timerange.end))
-                if clipped:
-                    fbinfo[fbtype_mapper.get(fbtype, 0)].append(clipped)
-
-
-
-def processAvailabilityFreeBusy(calendar, fbinfo, timerange):
-    """
-    Extract free-busy data from a VAVAILABILITY component.
-    @param calendar: the L{Component} that is the VCALENDAR containing the VAVAILABILITY's.
-    @param fbinfo: the tuple used to store the three types of fb data.
-    @param timerange: the time range to restrict free busy data to.
-    """
-
-    for vav in [x for x in calendar.subcomponents() if x.name() == "VAVAILABILITY"]:
-
-        # Get overall start/end
-        start = vav.getStartDateUTC()
-        if start is None:
-            start = DateTime(1900, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
-        end = vav.getEndDateUTC()
-        if end is None:
-            end = DateTime(2100, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
-        period = Period(start, end)
-        overall = clipPeriod(period, Period(timerange.start, timerange.end))
-        if overall is None:
-            continue
-
-        # Now get periods for each instance of AVAILABLE sub-components
-        periods = processAvailablePeriods(vav, timerange)
-
-        # Now invert the periods and store in accumulator
-        busyperiods = []
-        last_end = timerange.start
-        for period in periods:
-            if last_end < period.getStart():
-                busyperiods.append(Period(last_end, period.getStart()))
-            last_end = period.getEnd()
-        if last_end < timerange.end:
-            busyperiods.append(Period(last_end, timerange.end))
-
-        # Add to actual results mapped by busy type
-        fbtype = vav.propertyValue("BUSYTYPE")
-        if fbtype is None:
-            fbtype = "BUSY-UNAVAILABLE"
-
-        fbinfo[fbtype_mapper.get(fbtype, 2)].extend(busyperiods)
-
-
-
-def processAvailablePeriods(calendar, timerange):
-    """
-    Extract instance period data from an AVAILABLE component.
-    @param calendar: the L{Component} that is the VAVAILABILITY containing the AVAILABLE's.
-    @param timerange: the time range to restrict free busy data to.
-    """
-
-    periods = []
-
-    # First we need to group all AVAILABLE sub-components by UID
-    uidmap = {}
-    for component in calendar.subcomponents():
-        if component.name() == "AVAILABLE":
-            uid = component.propertyValue("UID")
-            uidmap.setdefault(uid, []).append(component)
-
-    # Then we expand each uid set separately
-    for componentSet in uidmap.itervalues():
-        instances = InstanceList(ignoreInvalidInstances=True)
-        instances.expandTimeRanges(componentSet, timerange.end)
-
-        # Now convert instances into period list
-        for key in instances:
-            instance = instances[key]
-            # Ignore any with floating times (which should not happen as the spec requires UTC or local
-            # but we will try and be safe here).
-            start = instance.start
-            if start.floating():
-                continue
-            end = instance.end
-            if end.floating():
-                continue
-
-            # Clip period for this instance - use duration for period end if that
-            # is what original component used
-            if instance.component.hasProperty("DURATION"):
-                period = Period(start, duration=end - start)
-            else:
-                period = Period(start, end)
-            clipped = clipPeriod(period, Period(timerange.start, timerange.end))
-            if clipped:
-                periods.append(clipped)
-
-    normalizePeriodList(periods)
-    return periods
-
-
-
-def buildFreeBusyResult(fbinfo, timerange, organizer=None, attendee=None, uid=None, method=None, event_details=None):
-    """
-    Generate a VCALENDAR object containing a single VFREEBUSY that is the
-    aggregate of the free busy info passed in.
-
-    @param fbinfo:        the array of busy periods to use.
-    @param timerange:     the L{TimeRange} for the query.
-    @param organizer:     the L{Property} for the Organizer of the free busy request, or None.
-    @param attendee:      the L{Property} for the Attendee responding to the free busy request, or None.
-    @param uid:           the UID value from the free busy request.
-    @param method:        the METHOD property value to insert.
-    @param event_details: VEVENT components to add.
-    @return:              the L{Component} containing the calendar data.
-    """
-
-    # Merge overlapping time ranges in each fb info section
-    normalizePeriodList(fbinfo[0])
-    normalizePeriodList(fbinfo[1])
-    normalizePeriodList(fbinfo[2])
-
-    # Now build a new calendar object with the free busy info we have
-    fbcalendar = Component("VCALENDAR")
-    fbcalendar.addProperty(Property("VERSION", "2.0"))
-    fbcalendar.addProperty(Property("PRODID", iCalendarProductID))
-    if method:
-        fbcalendar.addProperty(Property("METHOD", method))
-    fb = Component("VFREEBUSY")
-    fbcalendar.addComponent(fb)
-    if organizer is not None:
-        fb.addProperty(organizer)
-    if attendee is not None:
-        fb.addProperty(attendee)
-    fb.addProperty(Property("DTSTART", timerange.start))
-    fb.addProperty(Property("DTEND", timerange.end))
-    fb.addProperty(Property("DTSTAMP", DateTime.getNowUTC()))
-    if len(fbinfo[0]) != 0:
-        fb.addProperty(Property("FREEBUSY", fbinfo[0], {"FBTYPE": "BUSY"}))
-    if len(fbinfo[1]) != 0:
-        fb.addProperty(Property("FREEBUSY", fbinfo[1], {"FBTYPE": "BUSY-TENTATIVE"}))
-    if len(fbinfo[2]) != 0:
-        fb.addProperty(Property("FREEBUSY", fbinfo[2], {"FBTYPE": "BUSY-UNAVAILABLE"}))
-    if uid is not None:
-        fb.addProperty(Property("UID", uid))
-    else:
-        uid = md5(str(fbcalendar) + str(time.time())).hexdigest()
-        fb.addProperty(Property("UID", uid))
-
-    if event_details:
-        for vevent in event_details:
-            fbcalendar.addComponent(vevent)
-
-    return fbcalendar

Modified: CalendarServer/trunk/twistedcaldav/method/report_freebusy.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_freebusy.py	2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/twistedcaldav/method/report_freebusy.py	2015-04-17 18:11:45 UTC (rev 14683)
@@ -20,9 +20,10 @@
 
 __all__ = ["report_urn_ietf_params_xml_ns_caldav_free_busy_query"]
 
-from twisted.internet.defer import inlineCallbacks, returnValue
+from twext.python.log import Logger
 
-from twext.python.log import Logger
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+
 from txweb2 import responsecode
 from txweb2.dav.http import ErrorResponse
 from txweb2.dav.method.report import NumberOfMatchesWithinLimits
@@ -36,12 +37,15 @@
 from twistedcaldav.util import bestAcceptType
 
 from txdav.caldav.icalendarstore import TimeRangeLowerLimit, TimeRangeUpperLimit
+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
 from txdav.xml import element as davxml
 
+from pycalendar.period import Period
+
 log = Logger()
 
 @inlineCallbacks
-def report_urn_ietf_params_xml_ns_caldav_free_busy_query(self, request, freebusy): #@UnusedVariable
+def report_urn_ietf_params_xml_ns_caldav_free_busy_query(self, request, freebusy):
     """
     Generate a free-busy REPORT.
     (CalDAV-access-09, section 7.8)
@@ -57,36 +61,36 @@
     if not timerange.valid():
         raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid time-range specified"))
 
-    # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
-    fbinfo = ([], [], [])
+    fbset = []
 
-    matchcount = [0]
-
     accepted_type = bestAcceptType(request.headers.getHeader("accept"), Component.allowedTypes())
     if accepted_type is None:
         raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type"))
 
 
-    def generateFreeBusyInfo(calresource, uri): #@UnusedVariable
+    def getCalendarList(calresource, uri): #@UnusedVariable
         """
-        Run a free busy report on the specified calendar collection
-        accumulating the free busy info for later processing.
+        Store the calendars that match the query in L{fbset} which will then be used with the
+        freebusy query.
+
         @param calresource: the L{CalDAVResource} for a calendar collection.
-        @param uri: the uri for the calendar collecton resource.
+        @param uri: the uri for the calendar collection resource.
         """
 
-        def _gotResult(result):
-            matchcount[0] = result
-            return True
+        fbset.append(calresource._newStoreObject)
+        return succeed(True)
 
-        d = report_common.generateFreeBusyInfo(request, calresource, fbinfo, timerange, matchcount[0])
-        d.addCallback(_gotResult)
-        return d
+    # Run report taking depth into account
+    depth = request.headers.getHeader("depth", "0")
+    yield report_common.applyToCalendarCollections(self, request, request.uri, depth, getCalendarList, (caldavxml.ReadFreeBusy(),))
 
-    # Run report taking depth into account
+    # Do the actual freebusy query against the set of matched calendars
+    timerange = Period(timerange.start, timerange.end)
     try:
-        depth = request.headers.getHeader("depth", "0")
-        yield report_common.applyToCalendarCollections(self, request, request.uri, depth, generateFreeBusyInfo, (caldavxml.ReadFreeBusy(),))
+        fbresult = (yield FreebusyQuery(
+            None, None, None, None, None,
+            timerange, None, None,
+        ).generateAttendeeFreeBusyResponse(fbset=fbset, method=None))
     except NumberOfMatchesWithinLimits:
         log.error("Too many matching components in free-busy report")
         raise HTTPError(ErrorResponse(
@@ -107,11 +111,8 @@
             "Time-range value too far in the future. Must be on or before %s." % (str(e.limit),)
         ))
 
-    # Now build a new calendar object with the free busy info we have
-    fbcalendar = report_common.buildFreeBusyResult(fbinfo, timerange)
-
     response = Response()
-    response.stream = MemoryStream(fbcalendar.getText(accepted_type))
+    response.stream = MemoryStream(fbresult.getText(accepted_type))
     response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (accepted_type,)))
 
     returnValue(response)

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py	2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/caldav/delivery.py	2015-04-17 18:11:45 UTC (rev 14683)
@@ -26,10 +26,8 @@
 from twistedcaldav.config import config
 
 from txdav.base.propertystore.base import PropertyName
-from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser, RemoteCalendarUser, OtherServerCalendarUser
 from txdav.caldav.datastore.scheduling.delivery import DeliveryService
-from txdav.caldav.datastore.scheduling.freebusy import processAvailabilityFreeBusy, \
-    generateFreeBusyInfo, buildFreeBusyResult
+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
 from txdav.caldav.datastore.scheduling.itip import iTIPRequestStatus
 from txdav.caldav.datastore.scheduling.processing import ImplicitProcessor, ImplicitProcessorException
 from txdav.caldav.datastore.scheduling.utils import extractEmailDomain
@@ -97,10 +95,6 @@
         organizerProp = self.scheduler.calendar.getOrganizerProperty()
         uid = self.scheduler.calendar.resourceUID()
 
-        organizerPrincipal = None
-        if type(self.scheduler.organizer) in (LocalCalendarUser, OtherServerCalendarUser,):
-            organizerPrincipal = self.scheduler.organizer.record.uid
-
         for recipient in self.recipients:
 
             #
@@ -113,7 +107,7 @@
                 # Look for special delegate extended free-busy request
                 event_details = [] if self.scheduler.calendar.getExtendedFreeBusy() else None
 
-                yield self.generateFreeBusyResponse(recipient, self.responses, organizerProp, organizerPrincipal, uid, event_details)
+                yield self.generateFreeBusyResponse(recipient, self.responses, organizerProp, uid, event_details)
             else:
                 yield self.generateResponse(recipient, self.responses)
 
@@ -199,32 +193,26 @@
 
 
     @inlineCallbacks
-    def generateFreeBusyResponse(self, recipient, responses, organizerProp, organizerPrincipal, uid, event_details):
+    def generateFreeBusyResponse(self, recipient, responses, organizerProp, uid, event_details):
 
         # Extract the ATTENDEE property matching current recipient from the calendar data
         cuas = recipient.record.calendarUserAddresses
         attendeeProp = self.scheduler.calendar.getAttendeeProperty(cuas)
 
-        remote = isinstance(self.scheduler.organizer, RemoteCalendarUser)
-
         try:
-            fbresult = (yield self.generateAttendeeFreeBusyResponse(
-                recipient,
-                organizerProp,
-                organizerPrincipal,
-                uid,
-                attendeeProp,
-                remote,
-                event_details,
-            ))
-        except Exception:
+            fbresult = (yield FreebusyQuery(
+                self.scheduler.organizer, organizerProp, recipient, attendeeProp, uid,
+                self.scheduler.timeRange, self.scheduler.excludeUID, self.scheduler.logItems,
+                event_details=event_details,
+            ).generateAttendeeFreeBusyResponse())
+        except Exception as e:
             log.failure(
                 "Could not determine free busy information for recipient {cuaddr}",
                 cuaddr=recipient.cuaddr, level=LogLevel.debug
             )
             log.error(
-                "Could not determine free busy information for recipient {cuaddr}",
-                cuaddr=recipient.cuaddr
+                "Could not determine free busy information for recipient {cuaddr}: {ex}",
+                cuaddr=recipient.cuaddr, ex=e
             )
             err = HTTPError(ErrorResponse(
                 responsecode.FORBIDDEN,
@@ -245,56 +233,3 @@
                 calendar=fbresult
             )
             returnValue(True)
-
-
-    @inlineCallbacks
-    def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, organizerPrincipal, uid, attendeeProp, remote, event_details=None):
-
-        # Find the current recipients calendars that are not transparent
-        fbset = (yield recipient.inbox.ownerHome().loadCalendars())
-        fbset = [calendar for calendar in fbset if calendar.isUsedForFreeBusy()]
-
-        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
-        fbinfo = ([], [], [])
-
-        # Process the availability property from the Inbox.
-        availability = recipient.inbox.ownerHome().getAvailability()
-        if availability is not None:
-            processAvailabilityFreeBusy(availability, fbinfo, self.scheduler.timeRange)
-
-        # Check to see if the recipient is the same calendar user as the organizer.
-        # Needed for masked UID stuff.
-        if isinstance(self.scheduler.organizer, LocalCalendarUser):
-            same_calendar_user = self.scheduler.organizer.record.uid == recipient.record.uid
-        else:
-            same_calendar_user = False
-
-        # Now process free-busy set calendars
-        matchtotal = 0
-        for calendar in fbset:
-            matchtotal = (yield generateFreeBusyInfo(
-                calendar,
-                fbinfo,
-                self.scheduler.timeRange,
-                matchtotal,
-                excludeuid=self.scheduler.excludeUID,
-                organizer=self.scheduler.organizer.cuaddr,
-                organizerPrincipal=organizerPrincipal,
-                same_calendar_user=same_calendar_user,
-                servertoserver=remote,
-                event_details=event_details,
-                logItems=self.scheduler.logItems,
-            ))
-
-        # Build VFREEBUSY iTIP reply for this recipient
-        fbresult = buildFreeBusyResult(
-            fbinfo,
-            self.scheduler.timeRange,
-            organizer=organizerProp,
-            attendee=attendeeProp,
-            uid=uid,
-            method="REPLY",
-            event_details=event_details,
-        )
-
-        returnValue(fbresult)

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py	2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/freebusy.py	2015-04-17 18:11:45 UTC (rev 14683)
@@ -35,28 +35,26 @@
 
 from txdav.caldav.datastore.query.filter import Filter
 from txdav.caldav.icalendarstore import QueryMaxResources
+from txdav.caldav.datastore.scheduling.cuaddress import LocalCalendarUser
 from txdav.common.icommondatastore import IndexedSearchException, \
     InternalDataStoreError
 
 import uuid
+from collections import namedtuple
 
 log = Logger()
 
-fbtype_mapper = {"BUSY": 0, "BUSY-TENTATIVE": 1, "BUSY-UNAVAILABLE": 2}
-fbtype_index_mapper = {'B': 0, 'T': 1, 'U': 2}
 
-fbcacher = Memcacher("FBCache", pickle=True)
-
-
-
 class FBCacheEntry(object):
 
     CACHE_DAYS_FLOATING_ADJUST = 1
 
+    fbcacher = Memcacher("FBCache", pickle=True)
+
     def __init__(self, key, token, timerange, fbresults):
         self.key = key
         self.token = token
-        self.timerange = timerange
+        self.timerange = timerange.getText()
         self.fbresults = fbresults
 
 
@@ -66,16 +64,17 @@
 
         key = str(calresource.id()) + "/" + useruid
         token = (yield calresource.syncToken())
-        entry = (yield fbcacher.get(key))
+        entry = (yield cls.fbcacher.get(key))
 
         if entry:
 
             # Offset one day at either end to account for floating
-            cached_start = entry.timerange.start + Duration(days=FBCacheEntry.CACHE_DAYS_FLOATING_ADJUST)
-            cached_end = entry.timerange.end - Duration(days=FBCacheEntry.CACHE_DAYS_FLOATING_ADJUST)
+            entry_timerange = Period.parseText(entry.timerange)
+            cached_start = entry_timerange.getStart() + Duration(days=cls.CACHE_DAYS_FLOATING_ADJUST)
+            cached_end = entry_timerange.getEnd() - Duration(days=cls.CACHE_DAYS_FLOATING_ADJUST)
 
             # Verify that the requested time range lies within the cache time range
-            if compareDateTime(timerange.end, cached_end) <= 0 and compareDateTime(timerange.start, cached_start) >= 0:
+            if compareDateTime(timerange.getEnd(), cached_end) <= 0 and compareDateTime(timerange.getStart(), cached_start) >= 0:
 
                 # Verify that cached entry is still valid
                 if token == entry.token:
@@ -91,693 +90,700 @@
         key = str(calresource.id()) + "/" + useruid
         token = (yield calresource.syncToken())
         entry = cls(key, token, timerange, fbresults)
-        yield fbcacher.set(key, entry)
+        yield cls.fbcacher.set(key, entry)
 
 
 
-def generateFreeBusyInfo(
-    calresource,
-    fbinfo,
-    timerange,
-    matchtotal,
-    excludeuid=None,
-    organizer=None,
-    organizerPrincipal=None,
-    same_calendar_user=False,
-    servertoserver=False,
-    event_details=None,
-    logItems=None,
-    accountingItems=None,
-):
+class FreebusyQuery(object):
     """
-    Get freebusy information for a calendar. Different behavior for internal vs external calendars.
-
-    See L{_internalGenerateFreeBusyInfo} for argument description.
+    Class that manages the process of getting free busy information of a particular attendee.
     """
 
-    # TODO: this method really should be moved to L{CalendarObject} so the internal/external pieces
-    # can be split across L{CalendarObject} and L{CalendarObjectExternal}
-    if calresource.external():
-        return _externalGenerateFreeBusyInfo(
-            calresource,
-            fbinfo,
-            timerange,
-            matchtotal,
-            excludeuid,
-            organizer,
-            organizerPrincipal,
-            same_calendar_user,
-            servertoserver,
-            event_details,
-            logItems,
-            accountingItems,
-        )
-    else:
-        return _internalGenerateFreeBusyInfo(
-            calresource,
-            fbinfo,
-            timerange,
-            matchtotal,
-            excludeuid,
-            organizer,
-            organizerPrincipal,
-            same_calendar_user,
-            servertoserver,
-            event_details,
-            logItems,
-            accountingItems,
-        )
+    FBInfo = namedtuple("FBInfo", ("busy", "tentative", "unavailable",))
+    FBInfo_mapper = {"BUSY": "busy", "BUSY-TENTATIVE": "tentative", "BUSY-UNAVAILABLE": "unavailable"}
+    FBInfo_index_mapper = {'B': "busy", 'T': "tentative", 'U': "unavailable"}
 
+    def __init__(self, organizer, organizerProp, recipient, attendeeProp, uid, timeRange, excludeUID, logItems, accountingItems=None, event_details=None):
+        """
 
+        @param organizer: the organizer making the freebusy request
+        @type organizer: L{CalendarUser}
+        @param organizerProp: iCalendar ORGANIZER property from the request
+        @type organizerProp: L{Property}
+        @param recipient: the attendee whose freebusy is being requested
+        @type recipient: L{CalendarUser}
+        @param attendeeProp: iCalendar ATTENDEE property from the request
+        @type attendeeProp: L{Property}
+        @param uid: iCalendar UID in the request
+        @type uid: L{str}
+        @param timeRange: time range for freebusy request
+        @type timeRange: L{Period}
+        @param excludeUID: an iCalendar UID to exclude from busy results
+        @type excludeUID: L{str}
+        @param logItems: items to add to logging
+        @type logItems: L{dict}
+        @param accountingItems: items to add to accounting logging
+        @type accountingItems: L{dict}
+        @param event_details: if not L{None}, a list in which busy event details are stored
+        @type event_details: L{list} or L{None}
+        """
+        self.organizer = organizer
+        self.organizerProp = organizerProp
+        self.recipient = recipient
+        self.attendeeProp = attendeeProp
+        self.uid = uid
+        self.timerange = timeRange
+        self.excludeuid = excludeUID
+        self.logItems = logItems
+        self.accountingItems = accountingItems
+        self.event_details = event_details
 
- at inlineCallbacks
-def _externalGenerateFreeBusyInfo(
-    calresource,
-    fbinfo,
-    timerange,
-    matchtotal,
-    excludeuid=None,
-    organizer=None,
-    organizerPrincipal=None,
-    same_calendar_user=False,
-    servertoserver=False,
-    event_details=None,
-    logItems=None,
-    accountingItems=None,
-):
-    """
-    Generate a freebusy response for an external (cross-pod) calendar by making a cross-pod call. This will bypass
-    any type of smart caching on this pod in favor of using caching on the pod hosting the actual calendar data.
+        # Check to see if the recipient is the same calendar user as the organizer.
+        # Needed for masked UID stuff.
+        if isinstance(self.organizer, LocalCalendarUser):
+            self.same_calendar_user = self.organizer.record.uid == self.recipient.record.uid
+        else:
+            self.same_calendar_user = False
 
-    See L{_internalGenerateFreeBusyInfo} for argument description.
-    """
-    fbresults, matchtotal = yield calresource._txn.store().conduit.send_freebusy(calresource, timerange, matchtotal, excludeuid, organizer, organizerPrincipal, same_calendar_user, servertoserver, event_details)
-    for i in range(3):
-        fbinfo[i].extend([Period.parseText(p) for p in fbresults[i]])
-    returnValue(matchtotal)
 
+    @inlineCallbacks
+    def generateAttendeeFreeBusyResponse(self, fbset=None, method="REPLY"):
 
+        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
+        fbinfo = self.FBInfo([], [], [])
 
- at inlineCallbacks
-def _internalGenerateFreeBusyInfo(
-    calresource,
-    fbinfo,
-    timerange,
-    matchtotal,
-    excludeuid=None,
-    organizer=None,
-    organizerPrincipal=None,
-    same_calendar_user=False,
-    servertoserver=False,
-    event_details=None,
-    logItems=None,
-    accountingItems=None,
-):
-    """
-    Run a free busy report on the specified calendar collection
-    accumulating the free busy info for later processing.
-    @param calresource: the L{Calendar} for a calendar collection.
-    @param fbinfo:      the array of busy periods to update.
-    @param timerange:   the L{TimeRange} for the query.
-    @param matchtotal:  the running total for the number of matches.
-    @param excludeuid:  a C{str} containing a UID value to exclude any
-        components with that UID from contributing to free-busy.
-    @param organizer:   a C{str} containing the value of the ORGANIZER property
-        in the VFREEBUSY request.  This is used in conjunction with the UID
-        value to process exclusions.
-    @param same_calendar_user: a C{bool} indicating whether the calendar user
-        requesting the free-busy information is the same as the calendar user
-        being targeted.
-    @param servertoserver: a C{bool} indicating whether we are doing a local or
-        remote lookup request.
-    @param event_details: a C{list} into which to store extended VEVENT details if not C{None}
-    @param logItems: a C{dict} to store logging info to
-    @param accountingItems: a C{dict} to store accounting info to
-    """
+        if self.recipient is not None:
+            # Find the current recipients calendars that are not transparent
+            if fbset is None:
+                fbset = (yield self.recipient.inbox.ownerHome().loadCalendars())
+                fbset = [calendar for calendar in fbset if calendar.isUsedForFreeBusy()]
 
-    # First check the privilege on this collection
-    # TODO: for server-to-server we bypass this right now as we have no way to authorize external users.
-    # TODO: actually we by pass altogether by assuming anyone can check anyone else's freebusy
+            # Process the availability property from the Inbox.
+            availability = self.recipient.inbox.ownerHome().getAvailability()
+            if availability is not None:
+                self.processAvailabilityFreeBusy(availability, fbinfo)
 
-    # May need organizer principal
-    organizer_record = (yield calresource.directoryService().recordWithCalendarUserAddress(organizer)) if organizer else None
-    organizer_uid = organizer_record.uid if organizer_record else ""
+        # Now process free-busy set calendars
+        matchtotal = 0
+        for calendar in fbset:
+            matchtotal = (yield self.generateFreeBusyInfo(
+                calendar,
+                fbinfo,
+                matchtotal,
+            ))
 
-    # Free busy is per-user
-    attendee_uid = calresource.viewerHome().uid()
-    attendee_record = yield calresource.directoryService().recordWithUID(attendee_uid.decode("utf-8"))
+        # Build VFREEBUSY iTIP reply for this recipient
+        fbresult = self.buildFreeBusyResult(fbinfo, method=method)
 
-    # Get the timezone property from the collection.
-    tz = calresource.getTimezone()
+        returnValue(fbresult)
 
-    # Look for possible extended free busy information
-    rich_options = {
-        "organizer": False,
-        "delegate": False,
-        "resource": False,
-    }
-    do_event_details = False
-    if event_details is not None and organizer_record is not None and attendee_record is not None:
 
-        # Get the principal of the authorized user which may be different from the organizer if a delegate of
-        # the organizer is making the request
-        authz_uid = organizer_uid
-        authz_record = organizer_record
-        if calresource._txn._authz_uid is not None and calresource._txn._authz_uid != organizer_uid:
-            authz_uid = calresource._txn._authz_uid
-            authz_record = yield calresource.directoryService().recordWithUID(authz_uid.decode("utf-8"))
+    def processAvailabilityFreeBusy(self, calendar, fbinfo):
+        """
+        Extract free-busy data from a VAVAILABILITY component.
 
-        # Check if attendee is also the organizer or the delegate doing the request
-        if attendee_uid in (organizer_uid, authz_uid):
-            do_event_details = True
-            rich_options["organizer"] = True
+        @param calendar: the L{Component} that is the VCALENDAR containing the VAVAILABILITY's.
+        @param fbinfo: the tuple used to store the three types of fb data.
+        """
 
-        # Check if authorized user is a delegate of attendee
-        proxy = (yield authz_record.isProxyFor(attendee_record))
-        if config.Scheduling.Options.DelegeteRichFreeBusy and proxy:
-            do_event_details = True
-            rich_options["delegate"] = True
+        for vav in [x for x in calendar.subcomponents() if x.name() == "VAVAILABILITY"]:
 
-        # Check if attendee is room or resource
-        if config.Scheduling.Options.RoomResourceRichFreeBusy and attendee_record.getCUType() in ("RESOURCE", "ROOM",):
-            do_event_details = True
-            rich_options["resource"] = True
+            # Get overall start/end
+            start = vav.getStartDateUTC()
+            if start is None:
+                start = DateTime(1900, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
+            end = vav.getEndDateUTC()
+            if end is None:
+                end = DateTime(2100, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
+            period = Period(start, end)
+            overall = clipPeriod(period, self.timerange)
+            if overall is None:
+                continue
 
-    # Try cache
-    resources = (yield FBCacheEntry.getCacheEntry(calresource, attendee_uid, timerange)) if config.EnableFreeBusyCache else None
+            # Now get periods for each instance of AVAILABLE sub-components
+            periods = self.processAvailablePeriods(vav)
 
-    if resources is None:
+            # Now invert the periods and store in accumulator
+            busyperiods = []
+            last_end = self.timerange.getStart()
+            for period in periods:
+                if last_end < period.getStart():
+                    busyperiods.append(Period(last_end, period.getStart()))
+                last_end = period.getEnd()
+            if last_end < self.timerange.getEnd():
+                busyperiods.append(Period(last_end, self.timerange.getEnd()))
 
-        if accountingItems is not None:
-            accountingItems["fb-uncached"] = accountingItems.get("fb-uncached", 0) + 1
+            # Add to actual results mapped by busy type
+            fbtype = vav.propertyValue("BUSYTYPE")
+            if fbtype is None:
+                fbtype = "BUSY-UNAVAILABLE"
 
-        caching = False
-        if config.EnableFreeBusyCache:
-            # Log extended item
-            if logItems is not None:
-                logItems["fb-uncached"] = logItems.get("fb-uncached", 0) + 1
+            getattr(fbinfo, self.FBInfo_mapper.get(fbtype, "unavailable")).extend(busyperiods)
 
-            # We want to cache a large range of time based on the current date
-            cache_start = normalizeToUTC(DateTime.getToday() + Duration(days=0 - config.FreeBusyCacheDaysBack))
-            cache_end = normalizeToUTC(DateTime.getToday() + Duration(days=config.FreeBusyCacheDaysForward))
 
-            # If the requested time range would fit in our allowed cache range, trigger the cache creation
-            if compareDateTime(timerange.start, cache_start) >= 0 and compareDateTime(timerange.end, cache_end) <= 0:
-                cache_timerange = TimeRange(start=cache_start.getText(), end=cache_end.getText())
-                caching = True
+    def processAvailablePeriods(self, calendar):
+        """
+        Extract instance period data from an AVAILABLE component.
+        @param calendar: the L{Component} that is the VAVAILABILITY containing the AVAILABLE's.
+        @param timerange: the time range to restrict free busy data to.
+        """
 
-        #
-        # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range.
-        # We then take those results and merge them into one VFREEBUSY component
-        # with appropriate FREEBUSY properties, and return that single item as iCal data.
-        #
+        periods = []
 
-        # Create fake filter element to match time-range
-        filter = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                caldavxml.ComponentFilter(
-                    cache_timerange if caching else timerange,
-                    name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
-                ),
-                name="VCALENDAR",
-            )
-        )
-        filter = Filter(filter)
-        tzinfo = filter.settimezone(tz)
-        if accountingItems is not None:
-            tr = cache_timerange if caching else timerange
-            accountingItems["fb-query-timerange"] = (str(tr.start), str(tr.end),)
+        # First we need to group all AVAILABLE sub-components by UID
+        uidmap = {}
+        for component in calendar.subcomponents():
+            if component.name() == "AVAILABLE":
+                uid = component.propertyValue("UID")
+                uidmap.setdefault(uid, []).append(component)
 
-        try:
-            resources = yield calresource.search(filter, useruid=attendee_uid, fbtype=True)
-            if caching:
-                yield FBCacheEntry.makeCacheEntry(calresource, attendee_uid, cache_timerange, resources)
-        except IndexedSearchException:
-            raise InternalDataStoreError("Invalid indexedSearch query")
+        # Then we expand each uid set separately
+        for componentSet in uidmap.itervalues():
+            instances = InstanceList(ignoreInvalidInstances=True)
+            instances.expandTimeRanges(componentSet, self.timerange.getEnd())
 
-    else:
-        if accountingItems is not None:
-            accountingItems["fb-cached"] = accountingItems.get("fb-cached", 0) + 1
+            # Now convert instances into period list
+            for key in instances:
+                instance = instances[key]
+                # Ignore any with floating times (which should not happen as the spec requires UTC or local
+                # but we will try and be safe here).
+                start = instance.start
+                if start.floating():
+                    continue
+                end = instance.end
+                if end.floating():
+                    continue
 
-        # Log extended item
-        if logItems is not None:
-            logItems["fb-cached"] = logItems.get("fb-cached", 0) + 1
+                # Clip period for this instance - use duration for period end if that
+                # is what original component used
+                if instance.component.hasProperty("DURATION"):
+                    period = Period(start, duration=end - start)
+                else:
+                    period = Period(start, end)
+                clipped = clipPeriod(period, self.timerange)
+                if clipped:
+                    periods.append(clipped)
 
-        # Determine appropriate timezone (UTC is the default)
-        tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True)
+        normalizePeriodList(periods)
+        return periods
 
-    # We care about separate instances for VEVENTs only
-    aggregated_resources = {}
-    for name, uid, type, test_organizer, float, start, end, fbtype, transp in resources:
-        if transp == 'T' and fbtype != '?':
-            fbtype = 'F'
-        aggregated_resources.setdefault((name, uid, type, test_organizer,), []).append((float, start, end, fbtype,))
 
-    if accountingItems is not None:
-        accountingItems["fb-resources"] = {}
-        for k, v in aggregated_resources.items():
-            name, uid, type, test_organizer = k
-            accountingItems["fb-resources"][uid] = []
-            for float, start, end, fbtype in v:
-                fbstart = parseSQLTimestampToPyCalendar(start)
-                if float == 'Y':
-                    fbstart.setTimezone(tzinfo)
-                else:
-                    fbstart.setTimezone(Timezone(utc=True))
-                fbend = parseSQLTimestampToPyCalendar(end)
-                if float == 'Y':
-                    fbend.setTimezone(tzinfo)
-                else:
-                    fbend.setTimezone(Timezone(utc=True))
-                accountingItems["fb-resources"][uid].append((
-                    float,
-                    str(fbstart),
-                    str(fbend),
-                    fbtype,
-                ))
+    def generateFreeBusyInfo(self, calresource, fbinfo, matchtotal):
+        """
+        Get freebusy information for a calendar. Different behavior for internal vs external calendars.
 
-    # Cache directory record lookup outside this loop as it is expensive and will likely
-    # always end up being called with the same organizer address.
-    recordUIDCache = {}
-    for key in aggregated_resources.iterkeys():
+        See L{_internalGenerateFreeBusyInfo} for argument description.
+        """
 
-        name, uid, type, test_organizer = key
+        # TODO: this method really should be moved to L{CalendarObject} so the internal/external pieces
+        # can be split across L{CalendarObject} and L{CalendarObjectExternal}
+        if calresource.external():
+            return self._externalGenerateFreeBusyInfo(
+                calresource,
+                fbinfo,
+                matchtotal,
+            )
+        else:
+            return self._internalGenerateFreeBusyInfo(
+                calresource,
+                fbinfo,
+                matchtotal,
+            )
 
-        # Short-cut - if an fbtype exists we can use that
-        if type == "VEVENT" and aggregated_resources[key][0][3] != '?':
 
-            matchedResource = False
+    @inlineCallbacks
+    def _externalGenerateFreeBusyInfo(self, calresource, fbinfo, matchtotal):
+        """
+        Generate a freebusy response for an external (cross-pod) calendar by making a cross-pod call. This will bypass
+        any type of smart caching on this pod in favor of using caching on the pod hosting the actual calendar data.
 
-            # Look at each instance
-            for float, start, end, fbtype in aggregated_resources[key]:
-                # Ignore free time or unknown
-                if fbtype in ('F', '?'):
-                    continue
+        See L{_internalGenerateFreeBusyInfo} for argument description.
+        """
+        fbresults, matchtotal = yield calresource._txn.store().conduit.send_freebusy(
+            calresource, self.organizer.cuaddr, self.recipient.cuaddr, self.timerange,
+            matchtotal, self.excludeuid, self.event_details
+        )
+        for i in range(3):
+            fbinfo[i].extend([Period.parseText(p) for p in fbresults[i]])
+        returnValue(matchtotal)
 
-                # Ignore ones of this UID
-                if excludeuid:
-                    # See if we have a UID match
-                    if (excludeuid == uid):
-                        if test_organizer:
-                            test_uid = recordUIDCache.get(test_organizer)
-                            if test_uid is None:
-                                test_record = (yield calresource.directoryService().recordWithCalendarUserAddress(test_organizer))
-                                test_uid = test_record.uid if test_record else ""
-                                recordUIDCache[test_organizer] = test_uid
-                        else:
-                            test_uid = ""
 
-                        # Check that ORGANIZER's match (security requirement)
-                        if (organizer is None) or (organizer_uid == test_uid):
-                            continue
-                        # Check for no ORGANIZER and check by same calendar user
-                        elif (test_uid == "") and same_calendar_user:
-                            continue
+    @inlineCallbacks
+    def _internalGenerateFreeBusyInfo(
+        self,
+        calresource,
+        fbinfo,
+        matchtotal,
+    ):
+        """
+        Run a free busy report on the specified calendar collection
+        accumulating the free busy info for later processing.
+        @param calresource: the L{Calendar} for a calendar collection.
+        @param fbinfo:      the array of busy periods to update.
+        @param matchtotal:  the running total for the number of matches.
+        """
 
-                # Apply a timezone to any floating times
-                fbstart = parseSQLTimestampToPyCalendar(start)
-                if float == 'Y':
-                    fbstart.setTimezone(tzinfo)
-                else:
-                    fbstart.setTimezone(Timezone(utc=True))
-                fbend = parseSQLTimestampToPyCalendar(end)
-                if float == 'Y':
-                    fbend.setTimezone(tzinfo)
-                else:
-                    fbend.setTimezone(Timezone(utc=True))
+        # First check the privilege on this collection
+        # TODO: for server-to-server we bypass this right now as we have no way to authorize external users.
+        # TODO: actually we by pass altogether by assuming anyone can check anyone else's freebusy
 
-                # Clip instance to time range
-                clipped = clipPeriod(Period(fbstart, duration=fbend - fbstart), Period(timerange.start, timerange.end))
+        # May need organizer principal
+        organizer_record = self.organizer.record if self.organizer and self.organizer.hosted() else None
+        organizer_uid = organizer_record.uid if organizer_record else ""
 
-                # Double check for overlap
-                if clipped:
-                    matchedResource = True
-                    fbinfo[fbtype_index_mapper.get(fbtype, 0)].append(clipped)
+        # Free busy is per-user
+        attendee_record = self.recipient.record if self.organizer and self.recipient.hosted() else None
+        attendee_uid = attendee_record.uid if attendee_record else calresource.viewerHome().uid()
 
-            if matchedResource:
-                # Check size of results is within limit
-                matchtotal += 1
-                if matchtotal > config.MaxQueryWithDataResults:
-                    raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal)
+        # Get the timezone property from the collection.
+        tz = calresource.getTimezone()
 
-                # Add extended details
-                if do_event_details:
-                    child = (yield calresource.calendarObjectWithName(name))
-                    # Only add fully public events
-                    if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC:
-                        calendar = (yield child.componentForUser())
-                        _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo)
+        # Look for possible extended free busy information
+        rich_options = {
+            "organizer": False,
+            "delegate": False,
+            "resource": False,
+        }
+        do_event_details = False
+        if self.event_details is not None and organizer_record is not None and attendee_record is not None:
 
-        else:
-            child = (yield calresource.calendarObjectWithName(name))
-            calendar = (yield child.componentForUser())
+            # Get the principal of the authorized user which may be different from the organizer if a delegate of
+            # the organizer is making the request
+            authz_uid = organizer_uid
+            authz_record = organizer_record
+            if calresource._txn._authz_uid is not None and calresource._txn._authz_uid != organizer_uid:
+                authz_uid = calresource._txn._authz_uid
+                authz_record = yield calresource.directoryService().recordWithUID(authz_uid.decode("utf-8"))
 
-            # The calendar may come back as None if the resource is being changed, or was deleted
-            # between our initial index query and getting here. For now we will ignore this error, but in
-            # the longer term we need to implement some form of locking, perhaps.
-            if calendar is None:
-                log.error("Calendar %s is missing from calendar collection %r" % (name, calresource))
-                continue
+            # Check if attendee is also the organizer or the delegate doing the request
+            if attendee_uid in (organizer_uid, authz_uid):
+                do_event_details = True
+                rich_options["organizer"] = True
 
-            # Ignore ones of this UID
-            if excludeuid:
-                # See if we have a UID match
-                if (excludeuid == uid):
-                    test_organizer = calendar.getOrganizer()
-                    if test_organizer:
-                        test_uid = recordUIDCache.get(test_organizer)
-                        if test_uid is None:
-                            test_record = (yield calresource.directoryService().recordWithCalendarUserAddress(test_organizer))
-                            test_uid = test_record.uid if test_record else ""
-                            recordUIDCache[test_organizer] = test_uid
-                    else:
-                        test_uid = ""
+            # Check if authorized user is a delegate of attendee
+            proxy = (yield authz_record.isProxyFor(attendee_record))
+            if config.Scheduling.Options.DelegeteRichFreeBusy and proxy:
+                do_event_details = True
+                rich_options["delegate"] = True
 
-                    # Check that ORGANIZER's match (security requirement)
-                    if (organizer is None) or (organizer_uid == test_uid):
-                        continue
-                    # Check for no ORGANIZER and check by same calendar user
-                    elif (test_organizer is None) and same_calendar_user:
-                        continue
+            # Check if attendee is room or resource
+            if config.Scheduling.Options.RoomResourceRichFreeBusy and attendee_record.getCUType() in ("RESOURCE", "ROOM",):
+                do_event_details = True
+                rich_options["resource"] = True
 
-            if accountingItems is not None:
-                accountingItems.setdefault("fb-filter-match", []).append(uid)
+        # Try cache
+        resources = (yield FBCacheEntry.getCacheEntry(calresource, attendee_uid, self.timerange)) if config.EnableFreeBusyCache else None
 
-            if filter.match(calendar, None):
-                if accountingItems is not None:
-                    accountingItems.setdefault("fb-filter-matched", []).append(uid)
+        if resources is None:
 
-                # Check size of results is within limit
-                matchtotal += 1
-                if matchtotal > config.MaxQueryWithDataResults:
-                    raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal)
+            if self.accountingItems is not None:
+                self.accountingItems["fb-uncached"] = self.accountingItems.get("fb-uncached", 0) + 1
 
-                if calendar.mainType() == "VEVENT":
-                    processEventFreeBusy(calendar, fbinfo, timerange, tzinfo)
-                elif calendar.mainType() == "VFREEBUSY":
-                    processFreeBusyFreeBusy(calendar, fbinfo, timerange)
-                elif calendar.mainType() == "VAVAILABILITY":
-                    processAvailabilityFreeBusy(calendar, fbinfo, timerange)
-                else:
-                    assert "Free-busy query returned unwanted component: %s in %r", (name, calresource,)
+            caching = False
+            if config.EnableFreeBusyCache:
+                # Log extended item
+                if self.logItems is not None:
+                    self.logItems["fb-uncached"] = self.logItems.get("fb-uncached", 0) + 1
 
-                # Add extended details
-                if calendar.mainType() == "VEVENT" and do_event_details:
-                    child = (yield calresource.calendarObjectWithName(name))
-                    # Only add fully public events
-                    if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC:
-                        calendar = (yield child.componentForUser())
-                        _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo)
+                # We want to cache a large range of time based on the current date
+                cache_start = normalizeToUTC(DateTime.getToday() + Duration(days=0 - config.FreeBusyCacheDaysBack))
+                cache_end = normalizeToUTC(DateTime.getToday() + Duration(days=config.FreeBusyCacheDaysForward))
 
-    returnValue(matchtotal)
+                # If the requested time range would fit in our allowed cache range, trigger the cache creation
+                if compareDateTime(self.timerange.getStart(), cache_start) >= 0 and compareDateTime(self.timerange.getEnd(), cache_end) <= 0:
+                    cache_timerange = Period(cache_start, cache_end)
+                    caching = True
 
+            #
+            # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range.
+            # We then take those results and merge them into one VFREEBUSY component
+            # with appropriate FREEBUSY properties, and return that single item as iCal data.
+            #
 
+            # Create fake filter element to match time-range
+            tr = TimeRange(
+                start=(cache_timerange if caching else self.timerange).getStart().getText(),
+                end=(cache_timerange if caching else self.timerange).getEnd().getText(),
+            )
+            filter = caldavxml.Filter(
+                caldavxml.ComponentFilter(
+                    caldavxml.ComponentFilter(
+                        tr,
+                        name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
+                    ),
+                    name="VCALENDAR",
+                )
+            )
+            filter = Filter(filter)
+            tzinfo = filter.settimezone(tz)
+            if self.accountingItems is not None:
+                self.accountingItems["fb-query-timerange"] = (str(tr.start), str(tr.end),)
 
-def _addEventDetails(calendar, event_details, rich_options, timerange, tzinfo):
-    """
-    Expand events within the specified time range and limit the set of properties to those allowed for
-    delegate extended free busy.
+            try:
+                resources = yield calresource.search(filter, useruid=attendee_uid, fbtype=True)
+                if caching:
+                    yield FBCacheEntry.makeCacheEntry(calresource, attendee_uid, cache_timerange, resources)
+            except IndexedSearchException:
+                raise InternalDataStoreError("Invalid indexedSearch query")
 
-    @param calendar: the calendar object to expand
-    @type calendar: L{Component}
-    @param event_details: list to append VEVENT components to
-    @type event_details: C{list}
-    @param timerange: the time-range in which to expand
-    @type timerange: L{TimeRange}
-    @param tzinfo: timezone for floating time calculations
-    @type tzinfo: L{Timezone}
-    """
+        else:
+            if self.accountingItems is not None:
+                self.accountingItems["fb-cached"] = self.accountingItems.get("fb-cached", 0) + 1
 
-    # First expand the component
-    expanded = calendar.expand(timerange.start, timerange.end, timezone=tzinfo)
+            # Log extended item
+            if self.logItems is not None:
+                self.logItems["fb-cached"] = self.logItems.get("fb-cached", 0) + 1
 
-    keep_props = (
-        "UID",
-        "RECURRENCE-ID",
-        "DTSTAMP",
-        "DTSTART",
-        "DTEND",
-        "DURATION",
-    )
+            # Determine appropriate timezone (UTC is the default)
+            tzinfo = tz.gettimezone() if tz is not None else Timezone(utc=True)
 
-    if rich_options["organizer"] or rich_options["delegate"]:
-        keep_props += ("SUMMARY",)
+        # We care about separate instances for VEVENTs only
+        aggregated_resources = {}
+        for name, uid, type, test_organizer, float, start, end, fbtype, transp in resources:
+            if transp == 'T' and fbtype != '?':
+                fbtype = 'F'
+            aggregated_resources.setdefault((name, uid, type, test_organizer,), []).append((float, start, end, fbtype,))
 
-    if rich_options["organizer"] or rich_options["resource"]:
-        keep_props += ("ORGANIZER",)
+        if self.accountingItems is not None:
+            self.accountingItems["fb-resources"] = {}
+            for k, v in aggregated_resources.items():
+                name, uid, type, test_organizer = k
+                self.accountingItems["fb-resources"][uid] = []
+                for float, start, end, fbtype in v:
+                    fbstart = parseSQLTimestampToPyCalendar(start)
+                    if float == 'Y':
+                        fbstart.setTimezone(tzinfo)
+                    else:
+                        fbstart.setTimezone(Timezone(utc=True))
+                    fbend = parseSQLTimestampToPyCalendar(end)
+                    if float == 'Y':
+                        fbend.setTimezone(tzinfo)
+                    else:
+                        fbend.setTimezone(Timezone(utc=True))
+                    self.accountingItems["fb-resources"][uid].append((
+                        float,
+                        str(fbstart),
+                        str(fbend),
+                        fbtype,
+                    ))
 
-    # Remove all but essential properties
-    expanded.filterProperties(keep=keep_props)
+        # Cache directory record lookup outside this loop as it is expensive and will likely
+        # always end up being called with the same organizer address.
+        recordUIDCache = {}
+        for key in aggregated_resources.iterkeys():
 
-    # Need to remove all child components of VEVENT
-    for subcomponent in expanded.subcomponents():
-        if subcomponent.name() == "VEVENT":
-            for sub in tuple(subcomponent.subcomponents()):
-                subcomponent.removeComponent(sub)
+            name, uid, type, test_organizer = key
 
-    event_details.extend([subcomponent for subcomponent in expanded.subcomponents() if subcomponent.name() == "VEVENT"])
+            # Short-cut - if an fbtype exists we can use that
+            if type == "VEVENT" and aggregated_resources[key][0][3] != '?':
 
+                matchedResource = False
 
+                # Look at each instance
+                for float, start, end, fbtype in aggregated_resources[key]:
+                    # Ignore free time or unknown
+                    if fbtype in ('F', '?'):
+                        continue
 
-def processEventFreeBusy(calendar, fbinfo, timerange, tzinfo):
-    """
-    Extract free busy data from a VEVENT component.
-    @param calendar: the L{Component} that is the VCALENDAR containing the VEVENT's.
-    @param fbinfo: the tuple used to store the three types of fb data.
-    @param timerange: the time range to restrict free busy data to.
-    @param tzinfo: the L{Timezone} for the timezone to use for floating/all-day events.
-    """
+                    # Ignore ones of this UID
+                    if self.excludeuid:
+                        # See if we have a UID match
+                        if (self.excludeuid == uid):
+                            if test_organizer:
+                                test_uid = recordUIDCache.get(test_organizer)
+                                if test_uid is None:
+                                    test_record = (yield calresource.directoryService().recordWithCalendarUserAddress(test_organizer))
+                                    test_uid = test_record.uid if test_record else ""
+                                    recordUIDCache[test_organizer] = test_uid
+                            else:
+                                test_uid = ""
 
-    # Expand out the set of instances for the event with in the required range
-    instances = calendar.expandTimeRanges(timerange.end, lowerLimit=timerange.start, ignoreInvalidInstances=True)
+                            # Check that ORGANIZER's match (security requirement)
+                            if (self.organizer is None) or (organizer_uid == test_uid):
+                                continue
+                            # Check for no ORGANIZER and check by same calendar user
+                            elif (test_uid == "") and self.same_calendar_user:
+                                continue
 
-    # Can only do timed events
-    for key in instances:
-        instance = instances[key]
-        if instance.start.isDateOnly():
-            return
-        break
-    else:
-        return
+                    # Apply a timezone to any floating times
+                    fbstart = parseSQLTimestampToPyCalendar(start)
+                    if float == 'Y':
+                        fbstart.setTimezone(tzinfo)
+                    else:
+                        fbstart.setTimezone(Timezone(utc=True))
+                    fbend = parseSQLTimestampToPyCalendar(end)
+                    if float == 'Y':
+                        fbend.setTimezone(tzinfo)
+                    else:
+                        fbend.setTimezone(Timezone(utc=True))
 
-    for key in instances:
-        instance = instances[key]
+                    # Clip instance to time range
+                    clipped = clipPeriod(Period(fbstart, duration=fbend - fbstart), self.timerange)
 
-        # Apply a timezone to any floating times
-        fbstart = instance.start
-        if fbstart.floating():
-            fbstart.setTimezone(tzinfo)
-        fbend = instance.end
-        if fbend.floating():
-            fbend.setTimezone(tzinfo)
+                    # Double check for overlap
+                    if clipped:
+                        matchedResource = True
+                        getattr(fbinfo, self.FBInfo_index_mapper.get(fbtype, "busy")).append(clipped)
 
-        # Check TRANSP property of underlying component
-        if instance.component.hasProperty("TRANSP"):
-            # If its TRANSPARENT we always ignore it
-            if instance.component.propertyValue("TRANSP") == "TRANSPARENT":
-                continue
+                if matchedResource:
+                    # Check size of results is within limit
+                    matchtotal += 1
+                    if matchtotal > config.MaxQueryWithDataResults:
+                        raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal)
 
-        # Determine status
-        if instance.component.hasProperty("STATUS"):
-            status = instance.component.propertyValue("STATUS")
-        else:
-            status = "CONFIRMED"
+                    # Add extended details
+                    if do_event_details:
+                        child = (yield calresource.calendarObjectWithName(name))
+                        # Only add fully public events
+                        if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC:
+                            calendar = (yield child.componentForUser())
+                            self._addEventDetails(calendar, rich_options, tzinfo)
 
-        # Ignore cancelled
-        if status == "CANCELLED":
-            continue
+            else:
+                child = (yield calresource.calendarObjectWithName(name))
+                calendar = (yield child.componentForUser())
 
-        # Clip period for this instance - use duration for period end if that
-        # is what original component used
-        if instance.component.hasProperty("DURATION"):
-            period = Period(fbstart, duration=fbend - fbstart)
-        else:
-            period = Period(fbstart, fbend)
-        clipped = clipPeriod(period, Period(timerange.start, timerange.end))
+                # The calendar may come back as None if the resource is being changed, or was deleted
+                # between our initial index query and getting here. For now we will ignore this error, but in
+                # the longer term we need to implement some form of locking, perhaps.
+                if calendar is None:
+                    log.error("Calendar %s is missing from calendar collection %r" % (name, calresource))
+                    continue
 
-        # Double check for overlap
-        if clipped:
-            if status == "TENTATIVE":
-                fbinfo[1].append(clipped)
-            else:
-                fbinfo[0].append(clipped)
+                # Ignore ones of this UID
+                if self.excludeuid:
+                    # See if we have a UID match
+                    if (self.excludeuid == uid):
+                        test_organizer = calendar.getOrganizer()
+                        if test_organizer:
+                            test_uid = recordUIDCache.get(test_organizer)
+                            if test_uid is None:
+                                test_record = (yield calresource.directoryService().recordWithCalendarUserAddress(test_organizer))
+                                test_uid = test_record.uid if test_record else ""
+                                recordUIDCache[test_organizer] = test_uid
+                        else:
+                            test_uid = ""
 
+                        # Check that ORGANIZER's match (security requirement)
+                        if (self.organizer is None) or (organizer_uid == test_uid):
+                            continue
+                        # Check for no ORGANIZER and check by same calendar user
+                        elif (test_organizer is None) and self.same_calendar_user:
+                            continue
 
+                if self.accountingItems is not None:
+                    self.accountingItems.setdefault("fb-filter-match", []).append(uid)
 
-def processFreeBusyFreeBusy(calendar, fbinfo, timerange):
-    """
-    Extract FREEBUSY data from a VFREEBUSY component.
-    @param calendar: the L{Component} that is the VCALENDAR containing the VFREEBUSY's.
-    @param fbinfo: the tuple used to store the three types of fb data.
-    @param timerange: the time range to restrict free busy data to.
-    """
+                if filter.match(calendar, None):
+                    if self.accountingItems is not None:
+                        self.accountingItems.setdefault("fb-filter-matched", []).append(uid)
 
-    for vfb in [x for x in calendar.subcomponents() if x.name() == "VFREEBUSY"]:
-        # First check any start/end in the actual component
-        start = vfb.getStartDateUTC()
-        end = vfb.getEndDateUTC()
-        if start and end:
-            if not timeRangesOverlap(start, end, timerange.start, timerange.end):
-                continue
+                    # Check size of results is within limit
+                    matchtotal += 1
+                    if matchtotal > config.MaxQueryWithDataResults:
+                        raise QueryMaxResources(config.MaxQueryWithDataResults, matchtotal)
 
-        # Now look at each FREEBUSY property
-        for fb in vfb.properties("FREEBUSY"):
-            # Check the type
-            fbtype = fb.parameterValue("FBTYPE", default="BUSY")
-            if fbtype == "FREE":
-                continue
+                    if calendar.mainType() == "VEVENT":
+                        self.processEventFreeBusy(calendar, fbinfo, tzinfo)
+                    elif calendar.mainType() == "VFREEBUSY":
+                        self.processFreeBusyFreeBusy(calendar, fbinfo)
+                    elif calendar.mainType() == "VAVAILABILITY":
+                        self.processAvailabilityFreeBusy(calendar, fbinfo)
+                    else:
+                        assert "Free-busy query returned unwanted component: %s in %r", (name, calresource,)
 
-            # Look at each period in the property
-            assert isinstance(fb.value(), list), "FREEBUSY property does not contain a list of values: %r" % (fb,)
-            for period in fb.value():
-                # Clip period for this instance
-                clipped = clipPeriod(period.getValue(), Period(timerange.start, timerange.end))
-                if clipped:
-                    fbinfo[fbtype_mapper.get(fbtype, 0)].append(clipped)
+                    # Add extended details
+                    if calendar.mainType() == "VEVENT" and do_event_details:
+                        child = (yield calresource.calendarObjectWithName(name))
+                        # Only add fully public events
+                        if not child.accessMode or child.accessMode == Component.ACCESS_PUBLIC:
+                            calendar = (yield child.componentForUser())
+                            self._addEventDetails(calendar, rich_options, tzinfo)
 
+        returnValue(matchtotal)
 
 
-def processAvailabilityFreeBusy(calendar, fbinfo, timerange):
-    """
-    Extract free-busy data from a VAVAILABILITY component.
-    @param calendar: the L{Component} that is the VCALENDAR containing the VAVAILABILITY's.
-    @param fbinfo: the tuple used to store the three types of fb data.
-    @param timerange: the time range to restrict free busy data to.
-    """
+    def _addEventDetails(self, calendar, rich_options, tzinfo):
+        """
+        Expand events within the specified time range and limit the set of properties to those allowed for
+        delegate extended free busy.
 
-    for vav in [x for x in calendar.subcomponents() if x.name() == "VAVAILABILITY"]:
+        @param calendar: the calendar object to expand
+        @type calendar: L{Component}
+        @param event_details: list to append VEVENT components to
+        @type event_details: C{list}
+        @param tzinfo: timezone for floating time calculations
+        @type tzinfo: L{Timezone}
+        """
 
-        # Get overall start/end
-        start = vav.getStartDateUTC()
-        if start is None:
-            start = DateTime(1900, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
-        end = vav.getEndDateUTC()
-        if end is None:
-            end = DateTime(2100, 1, 1, 0, 0, 0, tzid=Timezone(utc=True))
-        period = Period(start, end)
-        overall = clipPeriod(period, Period(timerange.start, timerange.end))
-        if overall is None:
-            continue
+        # First expand the component
+        expanded = calendar.expand(self.timerange.getStart(), self.timerange.getEnd(), timezone=tzinfo)
 
-        # Now get periods for each instance of AVAILABLE sub-components
-        periods = processAvailablePeriods(vav, timerange)
+        keep_props = (
+            "UID",
+            "RECURRENCE-ID",
+            "DTSTAMP",
+            "DTSTART",
+            "DTEND",
+            "DURATION",
+        )
 
-        # Now invert the periods and store in accumulator
-        busyperiods = []
-        last_end = timerange.start
-        for period in periods:
-            if last_end < period.getStart():
-                busyperiods.append(Period(last_end, period.getStart()))
-            last_end = period.getEnd()
-        if last_end < timerange.end:
-            busyperiods.append(Period(last_end, timerange.end))
+        if rich_options["organizer"] or rich_options["delegate"]:
+            keep_props += ("SUMMARY",)
 
-        # Add to actual results mapped by busy type
-        fbtype = vav.propertyValue("BUSYTYPE")
-        if fbtype is None:
-            fbtype = "BUSY-UNAVAILABLE"
+        if rich_options["organizer"] or rich_options["resource"]:
+            keep_props += ("ORGANIZER",)
 
-        fbinfo[fbtype_mapper.get(fbtype, 2)].extend(busyperiods)
+        # Remove all but essential properties
+        expanded.filterProperties(keep=keep_props)
 
+        # Need to remove all child components of VEVENT
+        for subcomponent in expanded.subcomponents():
+            if subcomponent.name() == "VEVENT":
+                for sub in tuple(subcomponent.subcomponents()):
+                    subcomponent.removeComponent(sub)
 
+        self.event_details.extend([subcomponent for subcomponent in expanded.subcomponents() if subcomponent.name() == "VEVENT"])
 
-def processAvailablePeriods(calendar, timerange):
-    """
-    Extract instance period data from an AVAILABLE component.
-    @param calendar: the L{Component} that is the VAVAILABILITY containing the AVAILABLE's.
-    @param timerange: the time range to restrict free busy data to.
-    """
 
-    periods = []
+    def processEventFreeBusy(self, calendar, fbinfo, tzinfo):
+        """
+        Extract free busy data from a VEVENT component.
+        @param calendar: the L{Component} that is the VCALENDAR containing the VEVENT's.
+        @param fbinfo: the tuple used to store the three types of fb data.
+        @param tzinfo: the L{Timezone} for the timezone to use for floating/all-day events.
+        """
 
-    # First we need to group all AVAILABLE sub-components by UID
-    uidmap = {}
-    for component in calendar.subcomponents():
-        if component.name() == "AVAILABLE":
-            uid = component.propertyValue("UID")
-            uidmap.setdefault(uid, []).append(component)
+        # Expand out the set of instances for the event with in the required range
+        instances = calendar.expandTimeRanges(self.timerange.getEnd(), lowerLimit=self.timerange.getStart(), ignoreInvalidInstances=True)
 
-    # Then we expand each uid set separately
-    for componentSet in uidmap.itervalues():
-        instances = InstanceList(ignoreInvalidInstances=True)
-        instances.expandTimeRanges(componentSet, timerange.end)
+        # Can only do timed events
+        for key in instances:
+            instance = instances[key]
+            if instance.start.isDateOnly():
+                return
+            break
+        else:
+            return
 
-        # Now convert instances into period list
         for key in instances:
             instance = instances[key]
-            # Ignore any with floating times (which should not happen as the spec requires UTC or local
-            # but we will try and be safe here).
-            start = instance.start
-            if start.floating():
+
+            # Apply a timezone to any floating times
+            fbstart = instance.start
+            if fbstart.floating():
+                fbstart.setTimezone(tzinfo)
+            fbend = instance.end
+            if fbend.floating():
+                fbend.setTimezone(tzinfo)
+
+            # Check TRANSP property of underlying component
+            if instance.component.hasProperty("TRANSP"):
+                # If its TRANSPARENT we always ignore it
+                if instance.component.propertyValue("TRANSP") == "TRANSPARENT":
+                    continue
+
+            # Determine status
+            if instance.component.hasProperty("STATUS"):
+                status = instance.component.propertyValue("STATUS")
+            else:
+                status = "CONFIRMED"
+
+            # Ignore cancelled
+            if status == "CANCELLED":
                 continue
-            end = instance.end
-            if end.floating():
-                continue
 
             # Clip period for this instance - use duration for period end if that
             # is what original component used
             if instance.component.hasProperty("DURATION"):
-                period = Period(start, duration=end - start)
+                period = Period(fbstart, duration=fbend - fbstart)
             else:
-                period = Period(start, end)
-            clipped = clipPeriod(period, Period(timerange.start, timerange.end))
+                period = Period(fbstart, fbend)
+            clipped = clipPeriod(period, self.timerange)
+
+            # Double check for overlap
             if clipped:
-                periods.append(clipped)
+                if status == "TENTATIVE":
+                    fbinfo.tentative.append(clipped)
+                else:
+                    fbinfo.busy.append(clipped)
 
-    normalizePeriodList(periods)
-    return periods
 
+    def processFreeBusyFreeBusy(self, calendar, fbinfo):
+        """
+        Extract FREEBUSY data from a VFREEBUSY component.
+        @param calendar: the L{Component} that is the VCALENDAR containing the VFREEBUSY's.
+        @param fbinfo: the tuple used to store the three types of fb data.
+        """
 
+        for vfb in [x for x in calendar.subcomponents() if x.name() == "VFREEBUSY"]:
+            # First check any start/end in the actual component
+            start = vfb.getStartDateUTC()
+            end = vfb.getEndDateUTC()
+            if start and end:
+                if not timeRangesOverlap(start, end, self.timerange.getStart(), self.timerange.getEnd()):
+                    continue
 
-def buildFreeBusyResult(fbinfo, timerange, organizer=None, attendee=None, uid=None, method=None, event_details=None):
-    """
-    Generate a VCALENDAR object containing a single VFREEBUSY that is the
-    aggregate of the free busy info passed in.
+            # Now look at each FREEBUSY property
+            for fb in vfb.properties("FREEBUSY"):
+                # Check the type
+                fbtype = fb.parameterValue("FBTYPE", default="BUSY")
+                if fbtype == "FREE":
+                    continue
 
-    @param fbinfo:        the array of busy periods to use.
-    @param timerange:     the L{TimeRange} for the query.
-    @param organizer:     the L{Property} for the Organizer of the free busy request, or None.
-    @param attendee:      the L{Property} for the Attendee responding to the free busy request, or None.
-    @param uid:           the UID value from the free busy request.
-    @param method:        the METHOD property value to insert.
-    @param event_details: VEVENT components to add.
-    @return:              the L{Component} containing the calendar data.
-    """
+                # Look at each period in the property
+                assert isinstance(fb.value(), list), "FREEBUSY property does not contain a list of values: %r" % (fb,)
+                for period in fb.value():
+                    # Clip period for this instance
+                    clipped = clipPeriod(period.getValue(), self.timerange)
+                    if clipped:
+                        getattr(fbinfo, self.FBInfo_mapper.get(fbtype, "busy")).append(clipped)
 
-    # Merge overlapping time ranges in each fb info section
-    normalizePeriodList(fbinfo[0])
-    normalizePeriodList(fbinfo[1])
-    normalizePeriodList(fbinfo[2])
 
-    # Now build a new calendar object with the free busy info we have
-    fbcalendar = Component("VCALENDAR")
-    fbcalendar.addProperty(Property("VERSION", "2.0"))
-    fbcalendar.addProperty(Property("PRODID", iCalendarProductID))
-    if method:
-        fbcalendar.addProperty(Property("METHOD", method))
-    fb = Component("VFREEBUSY")
-    fbcalendar.addComponent(fb)
-    if organizer is not None:
-        fb.addProperty(organizer)
-    if attendee is not None:
-        fb.addProperty(attendee)
-    fb.addProperty(Property("DTSTART", timerange.start))
-    fb.addProperty(Property("DTEND", timerange.end))
-    fb.addProperty(Property("DTSTAMP", DateTime.getNowUTC()))
-    if len(fbinfo[0]) != 0:
-        fb.addProperty(Property("FREEBUSY", fbinfo[0], {"FBTYPE": "BUSY"}))
-    if len(fbinfo[1]) != 0:
-        fb.addProperty(Property("FREEBUSY", fbinfo[1], {"FBTYPE": "BUSY-TENTATIVE"}))
-    if len(fbinfo[2]) != 0:
-        fb.addProperty(Property("FREEBUSY", fbinfo[2], {"FBTYPE": "BUSY-UNAVAILABLE"}))
-    if uid is not None:
-        fb.addProperty(Property("UID", uid))
-    else:
-        uid = str(uuid.uuid4())
-        fb.addProperty(Property("UID", uid))
+    def buildFreeBusyResult(self, fbinfo, method=None):
+        """
+        Generate a VCALENDAR object containing a single VFREEBUSY that is the
+        aggregate of the free busy info passed in.
 
-    if event_details:
-        for vevent in event_details:
-            fbcalendar.addComponent(vevent)
+        @param fbinfo:        the array of busy periods to use.
+        @param method:        the METHOD property value to insert.
+        @return:              the L{Component} containing the calendar data.
+        """
 
-    return fbcalendar
+        # Merge overlapping time ranges in each fb info section
+        normalizePeriodList(fbinfo.busy)
+        normalizePeriodList(fbinfo.tentative)
+        normalizePeriodList(fbinfo.unavailable)
+
+        # Now build a new calendar object with the free busy info we have
+        fbcalendar = Component("VCALENDAR")
+        fbcalendar.addProperty(Property("VERSION", "2.0"))
+        fbcalendar.addProperty(Property("PRODID", iCalendarProductID))
+        if method:
+            fbcalendar.addProperty(Property("METHOD", method))
+        fb = Component("VFREEBUSY")
+        fbcalendar.addComponent(fb)
+        if self.organizerProp is not None:
+            fb.addProperty(self.organizerProp)
+        if self.attendeeProp is not None:
+            fb.addProperty(self.attendeeProp)
+        fb.addProperty(Property("DTSTART", self.timerange.getStart()))
+        fb.addProperty(Property("DTEND", self.timerange.getEnd()))
+        fb.addProperty(Property("DTSTAMP", DateTime.getNowUTC()))
+        if len(fbinfo.busy) != 0:
+            fb.addProperty(Property("FREEBUSY", fbinfo.busy, {"FBTYPE": "BUSY"}))
+        if len(fbinfo.tentative) != 0:
+            fb.addProperty(Property("FREEBUSY", fbinfo.tentative, {"FBTYPE": "BUSY-TENTATIVE"}))
+        if len(fbinfo.unavailable) != 0:
+            fb.addProperty(Property("FREEBUSY", fbinfo.unavailable, {"FBTYPE": "BUSY-UNAVAILABLE"}))
+        if self.uid is not None:
+            fb.addProperty(Property("UID", self.uid))
+        else:
+            uid = str(uuid.uuid4())
+            fb.addProperty(Property("UID", uid))
+
+        if self.event_details:
+            for vevent in self.event_details:
+                fbcalendar.addComponent(vevent)
+
+        return fbcalendar

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py	2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/processing.py	2015-04-17 18:11:45 UTC (rev 14683)
@@ -16,6 +16,7 @@
 
 from pycalendar.datetime import DateTime
 from pycalendar.duration import Duration
+from pycalendar.period import Period
 from pycalendar.timezone import Timezone
 
 from twext.python.log import Logger
@@ -23,13 +24,13 @@
 
 from twisted.internet.defer import inlineCallbacks, returnValue
 
-from twistedcaldav import customxml, caldavxml
+from twistedcaldav import customxml
 from twistedcaldav.accounting import emitAccounting, accountingEnabled
 from twistedcaldav.config import config
 from twistedcaldav.ical import Property
 from twistedcaldav.instance import InvalidOverriddenInstanceError
 
-from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
 from txdav.caldav.datastore.scheduling.itip import iTipProcessing, iTIPRequestStatus, \
     iTipGenerator
 from txdav.caldav.datastore.scheduling.utils import getCalendarObjectForRecord
@@ -799,6 +800,9 @@
         # in any calendars.
         uid = calendar.resourceUID()
 
+        # Object to do freebusy query
+        freebusy = FreebusyQuery(None, None, None, None, None, None, uid, None, accountingItems=accounting if len(instances) == 1 else None)
+
         # Now compare each instance time-range with the index and see if there is an overlap
         fbset = (yield self.recipient.inbox.ownerHome().loadCalendars())
         fbset = [fbcalendar for fbcalendar in fbset if fbcalendar.isUsedForFreeBusy()]
@@ -822,7 +826,7 @@
                 if instance.partstat == "NEEDS-ACTION" and instance.free and instance.active:
                     try:
                         # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
-                        fbinfo = ([], [], [])
+                        fbinfo = FreebusyQuery.FBInfo([], [], [])
 
                         def makeTimedUTC(dt):
                             dt = dt.duplicate()
@@ -834,18 +838,19 @@
                                 dt.adjustToUTC()
                             return dt
 
-                        tr = caldavxml.TimeRange(
-                            start=str(makeTimedUTC(instance.start)),
-                            end=str(makeTimedUTC(instance.end)),
+                        tr = Period(
+                            makeTimedUTC(instance.start),
+                            makeTimedUTC(instance.end),
                         )
 
-                        yield generateFreeBusyInfo(testcal, fbinfo, tr, 0, uid, servertoserver=True, accountingItems=accounting if len(instances) == 1 else None)
+                        freebusy.timerange = tr
+                        yield freebusy.generateFreeBusyInfo(testcal, fbinfo, 0)
 
                         # If any fbinfo entries exist we have an overlap
-                        if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
+                        if len(fbinfo.busy) or len(fbinfo.tentative) or len(fbinfo.unavailable):
                             instance.free = False
                         if accounting is not None:
-                            accounting["tr"].insert(0, (tr.attributes["start"], tr.attributes["end"], instance.free,))
+                            accounting["tr"].insert(0, (tr.getStart().getText(), tr.getEnd().getText(), instance.free,))
                     except QueryMaxResources:
                         instance.free[instance] = False
                         log.info("Exceeded number of matches whilst trying to find free-time.")

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py	2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/scheduler.py	2015-04-17 18:11:45 UTC (rev 14683)
@@ -42,6 +42,8 @@
 from txdav.caldav.datastore.scheduling.ischedule.delivery import ScheduleViaISchedule
 from txdav.caldav.datastore.scheduling.itip import iTIPRequestStatus
 
+from pycalendar.period import Period
+
 import hashlib
 from collections import namedtuple
 
@@ -400,9 +402,7 @@
                         "VFREEBUSY start or end not UTC",
                     ))
 
-                self.timeRange = caldavxml.TimeRange(start=dtstart.getText(), end=dtend.getText())
-                self.timeRange.start = dtstart
-                self.timeRange.end = dtend
+                self.timeRange = Period(dtstart, dtend)
 
                 # Look for masked UID
                 self.excludeUID = self.calendar.getMaskUID()

Modified: CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_freebusy.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_freebusy.py	2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/caldav/datastore/scheduling/test/test_freebusy.py	2015-04-17 18:11:45 UTC (rev 14683)
@@ -22,11 +22,10 @@
 from twisted.internet.defer import inlineCallbacks
 from twisted.trial.unittest import TestCase
 
-from twistedcaldav import caldavxml
 from twistedcaldav.ical import Component, Property
 
-from txdav.caldav.datastore.scheduling.freebusy import buildFreeBusyResult, \
-    generateFreeBusyInfo
+from txdav.caldav.datastore.scheduling.cuaddress import calendarUserFromCalendarUserAddress
+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
 from txdav.common.datastore.test.util import CommonCommonTests, populateCalendarsFrom
 
 def normalizeiCalendarText(data):
@@ -46,13 +45,8 @@
         data = (
             (
                 "#1.1 No busy time",
-                [
-                    [],
-                    [],
-                    [],
-                ],
-                "20080601T000000Z",
-                "20080602T000000Z",
+                FreebusyQuery.FBInfo([], [], []),
+                Period.parseText("20080601T000000Z/20080602T000000Z"),
                 None,
                 None,
                 None,
@@ -68,13 +62,8 @@
             ),
             (
                 "#1.2 No busy time with organizer & attendee",
-                [
-                    [],
-                    [],
-                    [],
-                ],
-                "20080601T000000Z",
-                "20080602T000000Z",
+                FreebusyQuery.FBInfo([], [], []),
+                Period.parseText("20080601T000000Z/20080602T000000Z"),
                 Property("ORGANIZER", "mailto:user01 at example.com"),
                 Property("ATTENDEE", "mailto:user02 at example.com"),
                 None,
@@ -92,13 +81,8 @@
             ),
             (
                 "#1.3 With single busy time",
-                [
-                    [Period.parseText("20080601T120000Z/20080601T130000Z"), ],
-                    [],
-                    [],
-                ],
-                "20080601T000000Z",
-                "20080602T000000Z",
+                FreebusyQuery.FBInfo([Period.parseText("20080601T120000Z/20080601T130000Z"), ], [], []),
+                Period.parseText("20080601T000000Z/20080602T000000Z"),
                 Property("ORGANIZER", "mailto:user01 at example.com"),
                 Property("ATTENDEE", "mailto:user02 at example.com"),
                 None,
@@ -117,16 +101,15 @@
             ),
             (
                 "#1.4 With multiple busy time",
-                [
+                FreebusyQuery.FBInfo(
                     [
                         Period.parseText("20080601T120000Z/20080601T130000Z"),
                         Period.parseText("20080601T140000Z/20080601T150000Z"),
                     ],
                     [],
                     [],
-                ],
-                "20080601T000000Z",
-                "20080602T000000Z",
+                ),
+                Period.parseText("20080601T000000Z/20080602T000000Z"),
                 Property("ORGANIZER", "mailto:user01 at example.com"),
                 Property("ATTENDEE", "mailto:user02 at example.com"),
                 None,
@@ -145,7 +128,7 @@
             ),
             (
                 "#1.5 With multiple busy time, some overlap",
-                [
+                FreebusyQuery.FBInfo(
                     [
                         Period.parseText("20080601T120000Z/20080601T130000Z"),
                         Period.parseText("20080601T123000Z/20080601T133000Z"),
@@ -154,9 +137,8 @@
                     ],
                     [],
                     [],
-                ],
-                "20080601T000000Z",
-                "20080602T000000Z",
+                ),
+                Period.parseText("20080601T000000Z/20080602T000000Z"),
                 Property("ORGANIZER", "mailto:user01 at example.com"),
                 Property("ATTENDEE", "mailto:user02 at example.com"),
                 None,
@@ -175,7 +157,7 @@
             ),
             (
                 "#1.6 With all busy time types",
-                [
+                FreebusyQuery.FBInfo(
                     [
                         Period.parseText("20080601T120000Z/20080601T130000Z"),
                         Period.parseText("20080601T140000Z/20080601T150000Z"),
@@ -186,9 +168,8 @@
                     [
                         Period.parseText("20080601T160000Z/20080601T170000Z"),
                     ],
-                ],
-                "20080601T000000Z",
-                "20080602T000000Z",
+                ),
+                Period.parseText("20080601T000000Z/20080602T000000Z"),
                 Property("ORGANIZER", "mailto:user01 at example.com"),
                 Property("ATTENDEE", "mailto:user02 at example.com"),
                 None,
@@ -209,13 +190,12 @@
             ),
             (
                 "#1.7 With single busy time and event details",
-                [
+                FreebusyQuery.FBInfo(
                     [Period.parseText("20080601T120000Z/20080601T130000Z"), ],
                     [],
                     [],
-                ],
-                "20080601T000000Z",
-                "20080602T000000Z",
+                ),
+                Period.parseText("20080601T000000Z/20080602T000000Z"),
                 Property("ORGANIZER", "mailto:user01 at example.com"),
                 Property("ATTENDEE", "mailto:user02 at example.com"),
                 [
@@ -250,16 +230,16 @@
             ),
         )
 
-        for description, fbinfo, dtstart, dtend, organizer, attendee, event_details, calendar in data:
-            timerange = caldavxml.TimeRange(start=dtstart, end=dtend)
-            result = buildFreeBusyResult(fbinfo, timerange, organizer=organizer, attendee=attendee, event_details=event_details)
+        for description, fbinfo, timerange, organizer, attendee, event_details, calendar in data:
+            freebusy = FreebusyQuery(None, organizer, None, attendee, None, timerange, None, None, event_details=event_details)
+            result = freebusy.buildFreeBusyResult(fbinfo)
             self.assertEqual(normalizeiCalendarText(str(result)), calendar.replace("\n", "\r\n"), msg=description)
 
 
 
 class GenerateFreeBusyInfo(CommonCommonTests, TestCase):
     """
-    Test txdav.caldav.datastore.scheduling.freebusy.generateFreeBusyInfo
+    Test txdav.caldav.datastore.scheduling.freebusy.FreebusyQuery
     """
 
     @inlineCallbacks
@@ -332,14 +312,16 @@
         """
 
         calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1"))
-        fbinfo = [[], [], [], ]
+        fbinfo = FreebusyQuery.FBInfo([], [], [])
         matchtotal = 0
-        timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText())
-        result = (yield generateFreeBusyInfo(calendar, fbinfo, timerange, matchtotal))
+        timerange = Period(self.now, self.now_1D)
+
+        freebusy = FreebusyQuery(None, None, None, None, None, timerange, None, None)
+        result = (yield freebusy.generateFreeBusyInfo(calendar, fbinfo, matchtotal))
         self.assertEqual(result, 0)
-        self.assertEqual(len(fbinfo[0]), 0)
-        self.assertEqual(len(fbinfo[1]), 0)
-        self.assertEqual(len(fbinfo[2]), 0)
+        self.assertEqual(len(fbinfo.busy), 0)
+        self.assertEqual(len(fbinfo.tentative), 0)
+        self.assertEqual(len(fbinfo.unavailable), 0)
 
 
     @inlineCallbacks
@@ -362,14 +344,16 @@
 
         yield self._createCalendarObject(data, "user01", "test.ics")
         calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1"))
-        fbinfo = [[], [], [], ]
+        fbinfo = FreebusyQuery.FBInfo([], [], [])
         matchtotal = 0
-        timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText())
-        result = (yield generateFreeBusyInfo(calendar, fbinfo, timerange, matchtotal))
+        timerange = Period(self.now, self.now_1D)
+
+        freebusy = FreebusyQuery(None, None, None, None, None, timerange, None, None)
+        result = (yield freebusy.generateFreeBusyInfo(calendar, fbinfo, matchtotal))
         self.assertEqual(result, 1)
-        self.assertEqual(fbinfo[0], [Period.parseText("%s/%s" % (self.now_12H.getText(), self.now_13H.getText(),)), ])
-        self.assertEqual(len(fbinfo[1]), 0)
-        self.assertEqual(len(fbinfo[2]), 0)
+        self.assertEqual(fbinfo.busy, [Period.parseText("%s/%s" % (self.now_12H.getText(), self.now_13H.getText(),)), ])
+        self.assertEqual(len(fbinfo.tentative), 0)
+        self.assertEqual(len(fbinfo.unavailable), 0)
 
 
     @inlineCallbacks
@@ -392,21 +376,23 @@
 
         yield self._createCalendarObject(data, "user01", "test.ics")
         calendar = (yield self.calendarUnderTest(home="user01", name="calendar_1"))
-        fbinfo = [[], [], [], ]
+        fbinfo = FreebusyQuery.FBInfo([], [], [])
         matchtotal = 0
-        timerange = caldavxml.TimeRange(start=self.now.getText(), end=self.now_1D.getText())
+        timerange = Period(self.now, self.now_1D)
         event_details = []
-        result = (yield generateFreeBusyInfo(
+
+        organizer = recipient = (yield calendarUserFromCalendarUserAddress("mailto:user01 at example.com", self.transactionUnderTest()))
+
+        freebusy = FreebusyQuery(organizer, None, recipient, None, None, timerange, None, None, event_details=event_details)
+        freebusy.same_calendar_user = True
+        result = (yield freebusy.generateFreeBusyInfo(
             calendar,
             fbinfo,
-            timerange,
             matchtotal,
-            organizer="mailto:user01 at example.com",
-            event_details=event_details
         ))
         self.assertEqual(result, 1)
-        self.assertEqual(fbinfo[0], [Period.parseText("%s/%s" % (self.now_12H.getText(), self.now_13H.getText(),)), ])
-        self.assertEqual(len(fbinfo[1]), 0)
-        self.assertEqual(len(fbinfo[2]), 0)
+        self.assertEqual(fbinfo.busy, [Period(self.now_12H, self.now_13H), ])
+        self.assertEqual(len(fbinfo.tentative), 0)
+        self.assertEqual(len(fbinfo.unavailable), 0)
         self.assertEqual(len(event_details), 1)
         self.assertEqual(str(event_details[0]), str(tuple(Component.fromString(data).subcomponents())[0]))

Modified: CalendarServer/trunk/txdav/common/datastore/podding/store_api.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/store_api.py	2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/common/datastore/podding/store_api.py	2015-04-17 18:11:45 UTC (rev 14683)
@@ -16,11 +16,13 @@
 
 from twisted.internet.defer import inlineCallbacks, returnValue
 
-from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
+from txdav.caldav.datastore.scheduling.cuaddress import calendarUserFromCalendarUserAddress
+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
 from txdav.common.datastore.podding.util import UtilityConduitMixin
 from txdav.common.datastore.sql_tables import _HOME_STATUS_DISABLED
 
-from twistedcaldav.caldavxml import TimeRange
+from pycalendar.period import Period
+
 from datetime import datetime
 
 
@@ -69,13 +71,11 @@
     def send_freebusy(
         self,
         calresource,
+        organizer,
+        recipient,
         timerange,
         matchtotal,
         excludeuid,
-        organizer,
-        organizerPrincipal,
-        same_calendar_user,
-        servertoserver,
         event_details,
     ):
         """
@@ -84,13 +84,11 @@
         """
         txn, request, server = yield self._getRequestForStoreObject("freebusy", calresource, False)
 
-        request["timerange"] = [timerange.start.getText(), timerange.end.getText()]
+        request["organizer"] = organizer
+        request["recipient"] = organizer
+        request["timerange"] = timerange.getText()
         request["matchtotal"] = matchtotal
         request["excludeuid"] = excludeuid
-        request["organizer"] = organizer
-        request["organizerPrincipal"] = organizerPrincipal
-        request["same_calendar_user"] = same_calendar_user
-        request["servertoserver"] = servertoserver
         request["event_details"] = event_details
 
         response = yield self.sendRequestToServer(txn, server, request)
@@ -109,28 +107,26 @@
         # Operate on the L{CommonHomeChild}
         calresource, _ignore = yield self._getStoreObjectForRequest(txn, request)
 
-        fbinfo = [[], [], []]
-        matchtotal = yield generateFreeBusyInfo(
+        organizer = yield calendarUserFromCalendarUserAddress(request["organizer"], txn)
+        recipient = yield calendarUserFromCalendarUserAddress(request["recipient"], txn)
+
+        freebusy = FreebusyQuery(
+            organizer, None, recipient, None, None,
+            Period.parseText(request["timerange"]), request["excludeuid"], None, event_details=request["event_details"])
+        fbinfo = FreebusyQuery.FBInfo([], [], [])
+        matchtotal = yield freebusy.generateFreeBusyInfo(
             calresource,
             fbinfo,
-            TimeRange(start=request["timerange"][0], end=request["timerange"][1]),
             request["matchtotal"],
-            request["excludeuid"],
-            request["organizer"],
-            request["organizerPrincipal"],
-            request["same_calendar_user"],
-            request["servertoserver"],
-            request["event_details"],
-            logItems=None
         )
 
-        # Convert L{DateTime} objects to text for JSON response
-        for i in range(3):
-            for j in range(len(fbinfo[i])):
-                fbinfo[i][j] = fbinfo[i][j].getText()
-
+        # Convert L{Period} objects to text for JSON response
         returnValue({
-            "fbresults": fbinfo,
+            "fbresults": [
+                [item.getText() for item in fbinfo.busy],
+                [item.getText() for item in fbinfo.tentative],
+                [item.getText() for item in fbinfo.unavailable],
+            ],
             "matchtotal": matchtotal,
         })
 

Modified: CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py	2015-04-16 03:59:52 UTC (rev 14682)
+++ CalendarServer/trunk/txdav/common/datastore/podding/test/test_conduit.py	2015-04-17 18:11:45 UTC (rev 14683)
@@ -26,11 +26,11 @@
 from twisted.internet.defer import inlineCallbacks, succeed, returnValue
 
 from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import TimeRange
 from twistedcaldav.ical import Component, normalize_iCalStr
 
 from txdav.caldav.datastore.query.filter import Filter
-from txdav.caldav.datastore.scheduling.freebusy import generateFreeBusyInfo
+from txdav.caldav.datastore.scheduling.cuaddress import calendarUserFromCalendarUserAddress
+from txdav.caldav.datastore.scheduling.freebusy import FreebusyQuery
 from txdav.caldav.datastore.scheduling.ischedule.localservers import ServersDB, Server
 from txdav.caldav.datastore.sql import ManagedAttachment, AttachmentLink
 from txdav.caldav.datastore.test.common import CaptureProtocol
@@ -927,21 +927,13 @@
 
         shared = yield self.calendarUnderTest(txn=self.theTransactionUnderTest(1), home="puser01", name="shared-calendar")
 
-        fbinfo = [[], [], []]
-        matchtotal = yield generateFreeBusyInfo(
-            shared,
-            fbinfo,
-            TimeRange(start=fbstart, end=fbend),
-            0,
-            excludeuid=None,
-            organizer=None,
-            organizerPrincipal=None,
-            same_calendar_user=False,
-            servertoserver=False,
-            event_details=False,
-            logItems=None
-        )
+        fbinfo = FreebusyQuery.FBInfo([], [], [])
+        timerange = Period(DateTime.parseText(fbstart), DateTime.parseText(fbend))
+        organizer = recipient = (yield calendarUserFromCalendarUserAddress("mailto:puser01 at example.com", self.theTransactionUnderTest(1)))
 
+        freebusy = FreebusyQuery(organizer, None, recipient, None, None, timerange, None, None)
+        matchtotal = (yield freebusy.generateFreeBusyInfo(shared, fbinfo, 0))
+
         self.assertEqual(matchtotal, 1)
         self.assertEqual(fbinfo[0], [Period.parseText("{now:04d}0102T140000Z/PT1H".format(**self.nowYear)), ])
         self.assertEqual(len(fbinfo[1]), 0)
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20150417/cbb4ca03/attachment-0001.html>


More information about the calendarserver-changes mailing list