[CalendarServer-changes] [3899] CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/ twistedcaldav/directory

source_changes at macosforge.org source_changes at macosforge.org
Thu Mar 19 13:45:50 PDT 2009


Revision: 3899
          http://trac.macosforge.org/projects/calendarserver/changeset/3899
Author:   sagen at apple.com
Date:     2009-03-19 13:45:50 -0700 (Thu, 19 Mar 2009)
Log Message:
-----------
Merging Cyrus' on-demand directory work forward

Modified Paths:
--------------
    CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/sqldb.py
    CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/xmlaccountsparser.py
    CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/xmlfile.py

Added Paths:
-----------
    CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingappleopendirectory.py
    CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingdirectory.py
    CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingxmlfile.py
    CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/test/test_cachedirectory.py

Copied: CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingappleopendirectory.py (from rev 3898, CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3672/twistedcaldav/directory/cachingappleopendirectory.py)
===================================================================
--- CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingappleopendirectory.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingappleopendirectory.py	2009-03-19 20:45:50 UTC (rev 3899)
@@ -0,0 +1,778 @@
+##
+# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Apple Open Directory directory service implementation.
+"""
+
+__all__ = [
+    "OpenDirectoryService",
+    "OpenDirectoryInitError",
+]
+
+import sys
+from uuid import UUID
+
+from twext.python.plistlib import readPlistFromString
+
+from xml.parsers.expat import ExpatError
+
+import opendirectory
+import dsattributes
+import dsquery
+
+from twisted.internet.threads import deferToThread
+from twisted.cred.credentials import UsernamePassword
+from twisted.web2.auth.digest import DigestedCredentials
+
+from twistedcaldav.directory.cachingdirectory import CachingDirectoryService,\
+    CachingDirectoryRecord
+from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
+from twistedcaldav.directory.directory import DirectoryError, UnknownRecordTypeError
+
+class OpenDirectoryService(CachingDirectoryService):
+    """
+    Open Directory implementation of L{IDirectoryService}.
+    """
+    baseGUID = "891F8321-ED02-424C-BA72-89C32F215C1E"
+
+    def __repr__(self):
+        return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.node)
+
+    def __init__(
+        self,
+        node="/Search",
+        restrictEnabledRecords=False,
+        restrictToGroup="",
+        dosetup=True,
+        cacheTimeout=30
+    ):
+        """
+        @param node: an OpenDirectory node name to bind to.
+        @param restrictEnabledRecords: C{True} if a group in the directory is to be used to determine
+            which calendar users are enabled.
+        @param restrictToGroup: C{str} guid or name of group used to restrict enabled users.
+        @param dosetup: if C{True} then the directory records are initialized,
+                        if C{False} they are not.
+                        This should only be set to C{False} when doing unit tests.
+        @param cacheTimeout: C{int} number of minutes before cache is invalidated.
+        """
+
+        super(OpenDirectoryService, self).__init__(cacheTimeout)
+
+        try:
+            directory = opendirectory.odInit(node)
+        except opendirectory.ODError, e:
+            self.log_error("Open Directory (node=%s) Initialization error: %s" % (node, e))
+            raise
+
+        self.realmName = node
+        self.directory = directory
+        self.node = node
+        self.restrictEnabledRecords = restrictEnabledRecords
+        self.restrictToGroup = restrictToGroup
+        try:
+            UUID(self.restrictToGroup)
+        except:
+            self.restrictToGUID = False
+        else:
+            self.restrictToGUID = True
+        self.restrictedGUIDs = None
+        self.cacheTimeout = cacheTimeout
+        self._records = {}
+        self._delayedCalls = set()
+
+    def __cmp__(self, other):
+        if not isinstance(other, DirectoryRecord):
+            return super(DirectoryRecord, self).__eq__(other)
+
+        for attr in ("directory", "node"):
+            diff = cmp(getattr(self, attr), getattr(other, attr))
+            if diff != 0:
+                return diff
+        return 0
+
+    def __hash__(self):
+        h = hash(self.__class__)
+        for attr in ("directory", "node"):
+            h = (h + hash(getattr(self, attr))) & sys.maxint
+        return h
+
+    def _expandGroupMembership(self, members, nestedGroups, processedGUIDs=None, returnGroups=False):
+
+        if processedGUIDs is None:
+            processedGUIDs = set()
+
+        if isinstance(members, str):
+            members = [members]
+
+        if isinstance(nestedGroups, str):
+            nestedGroups = [nestedGroups]
+
+        for memberGUID in members:
+            if memberGUID not in processedGUIDs:
+                processedGUIDs.add(memberGUID)
+                yield memberGUID
+
+        for groupGUID in nestedGroups:
+            if groupGUID in processedGUIDs:
+                continue
+
+            self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
+                self.directory,
+                dsattributes.kDS1AttrGeneratedUID,
+                groupGUID,
+                dsattributes.eDSExact,
+                False,
+                dsattributes.kDSStdRecordTypeGroups,
+                [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups]
+            ))
+            result = opendirectory.queryRecordsWithAttribute_list(
+                self.directory,
+                dsattributes.kDS1AttrGeneratedUID,
+                groupGUID,
+                dsattributes.eDSExact,
+                False,
+                dsattributes.kDSStdRecordTypeGroups,
+                [dsattributes.kDSNAttrGroupMembers, dsattributes.kDSNAttrNestedGroups]
+            )
+
+            if not result:
+                self.log_error("Couldn't find group %s when trying to expand nested groups."
+                             % (groupGUID,))
+                continue
+
+            group = result[0][1]
+
+            processedGUIDs.add(groupGUID)
+            if returnGroups:
+                yield groupGUID
+
+            for GUID in self._expandGroupMembership(
+                group.get(dsattributes.kDSNAttrGroupMembers, []),
+                group.get(dsattributes.kDSNAttrNestedGroups, []),
+                processedGUIDs,
+                returnGroups,
+            ):
+                yield GUID
+
+    def _calendarUserAddresses(self, recordType, recordData):
+        """
+        Extract specific attributes from the directory record for use as calendar user address.
+        
+        @param recordData: a C{dict} containing the attributes retrieved from the directory.
+        @return: a C{set} of C{str} for each expanded calendar user address.
+        """
+        # Now get the addresses
+        result = set()
+        
+        # Add each email address as a mailto URI
+        emails = recordData.get(dsattributes.kDSNAttrEMailAddress)
+        if emails is not None:
+            if isinstance(emails, str):
+                emails = [emails]
+            for email in emails:
+                result.add("mailto:%s" % (email.lower(),))
+                
+        return result
+
+    def _parseResourceInfo(self, plist, guid, recordType, shortname):
+        """
+        Parse OD ResourceInfo attribute and extract information that the server needs.
+
+        @param plist: the plist that is the attribute value.
+        @type plist: str
+        @param guid: the directory GUID of the record being parsed.
+        @type guid: str
+        @param shortname: the record shortname of the record being parsed.
+        @type shortname: str
+        @return: a C{tuple} of C{bool} for auto-accept, C{str} for proxy GUID, C{str} for read-only proxy GUID.
+        """
+        try:
+            plist = readPlistFromString(plist)
+            wpframework = plist.get("com.apple.WhitePagesFramework", {})
+            autoaccept = wpframework.get("AutoAcceptsInvitation", False)
+            proxy = wpframework.get("CalendaringDelegate", None)
+            read_only_proxy = wpframework.get("ReadOnlyCalendaringDelegate", None)
+        except (ExpatError, AttributeError), e:
+            self.log_error(
+                "Failed to parse ResourceInfo attribute of record (%s)%s (guid=%s): %s\n%s" %
+                (recordType, shortname, guid, e, plist,)
+            )
+            raise ValueError("Invalid ResourceInfo")
+
+        return (autoaccept, proxy, read_only_proxy,)
+
+    def recordTypes(self):
+        return (
+            DirectoryService.recordType_users,
+            DirectoryService.recordType_groups,
+            DirectoryService.recordType_locations,
+            DirectoryService.recordType_resources,
+        )
+
+    def groupsForGUID(self, guid):
+        
+        attrs = [
+            dsattributes.kDS1AttrGeneratedUID,
+        ]
+
+        recordType = dsattributes.kDSStdRecordTypeGroups
+
+        guids = set()
+
+        query = dsquery.match(dsattributes.kDSNAttrGroupMembers, guid, dsattributes.eDSExact)
+        try:
+            self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
+                self.directory,
+                query.attribute,
+                query.value,
+                query.matchType,
+                False,
+                recordType,
+                attrs,
+            ))
+            results = opendirectory.queryRecordsWithAttribute_list(
+                self.directory,
+                query.attribute,
+                query.value,
+                query.matchType,
+                False,
+                recordType,
+                attrs,
+            )
+        except opendirectory.ODError, ex:
+            self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
+            raise
+
+        for (_ignore_recordShortName, value) in results:
+
+            # Now get useful record info.
+            recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
+            if recordGUID:
+                guids.add(recordGUID)
+
+        query = dsquery.match(dsattributes.kDSNAttrNestedGroups, guid, dsattributes.eDSExact)
+        try:
+            self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
+                self.directory,
+                query.attribute,
+                query.value,
+                query.matchType,
+                False,
+                recordType,
+                attrs,
+            ))
+            results = opendirectory.queryRecordsWithAttribute_list(
+                self.directory,
+                query.attribute,
+                query.value,
+                query.matchType,
+                False,
+                recordType,
+                attrs,
+            )
+        except opendirectory.ODError, ex:
+            self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
+            raise
+
+        for (_ignore_recordShortName, value) in results:
+
+            # Now get useful record info.
+            recordGUID = value.get(dsattributes.kDS1AttrGeneratedUID)
+            if recordGUID:
+                guids.add(recordGUID)
+
+        return guids
+
+    def proxiesForGUID(self, recordType, guid):
+        
+        # Lookup in index
+        try:
+            # TODO:
+            return ()
+        except KeyError:
+            return ()
+
+    def readOnlyProxiesForGUID(self, recordType, guid):
+        
+        # Lookup in index
+        try:
+            # TODO:
+            return ()
+        except KeyError:
+            return ()
+
+    _ODFields = {
+        'fullName' : dsattributes.kDS1AttrDistinguishedName,
+        'firstName' : dsattributes.kDS1AttrFirstName,
+        'lastName' : dsattributes.kDS1AttrLastName,
+        'emailAddresses' : dsattributes.kDSNAttrEMailAddress,
+        'recordName' : dsattributes.kDSNAttrRecordName,
+        'guid' : dsattributes.kDS1AttrGeneratedUID,
+    }
+
+    _toODRecordTypes = {
+        DirectoryService.recordType_users :
+            dsattributes.kDSStdRecordTypeUsers,
+        DirectoryService.recordType_locations :
+            dsattributes.kDSStdRecordTypePlaces,
+        DirectoryService.recordType_groups :
+            dsattributes.kDSStdRecordTypeGroups,
+        DirectoryService.recordType_resources :
+            dsattributes.kDSStdRecordTypeResources,
+    }
+
+    _fromODRecordTypes = dict([(b, a) for a, b in _toODRecordTypes.iteritems()])
+
+    def recordsMatchingFields(self, fields, operand="or", recordType=None):
+
+        # Note that OD applies case-sensitivity globally across the entire
+        # query, not per expression, so the current code uses whatever is
+        # specified in the last field in the fields list
+
+        def collectResults(results):
+            self.log_info("Got back %d records from OD" % (len(results),))
+            for key, val in results.iteritems():
+                self.log_debug("OD result: %s %s" % (key, val))
+                try:
+                    guid = val[dsattributes.kDS1AttrGeneratedUID]
+                    record = self.recordWithGUID(guid)
+                    if record:
+                        yield record
+                except KeyError:
+                    pass
+
+
+        operand = (dsquery.expression.OR if operand == "or"
+            else dsquery.expression.AND)
+
+        expressions = []
+        for field, value, caseless, matchType in fields:
+            if field in self._ODFields:
+                ODField = self._ODFields[field]
+                if matchType == "starts-with":
+                    comparison = dsattributes.eDSStartsWith
+                elif matchType == "contains":
+                    comparison = dsattributes.eDSContains
+                else:
+                    comparison = dsattributes.eDSExact
+                expressions.append(dsquery.match(ODField, value, comparison))
+
+
+        if recordType is None:
+            recordTypes = self._toODRecordTypes.values()
+        else:
+            recordTypes = (self._toODRecordTypes[recordType],)
+
+        self.log_info("Calling OD: Types %s, Operand %s, Caseless %s, %s" % (recordTypes, operand, caseless, fields))
+        deferred = deferToThread(
+            opendirectory.queryRecordsWithAttributes,
+            self.directory,
+            dsquery.expression(operand, expressions).generate(),
+            caseless,
+            recordTypes,
+            [ dsattributes.kDS1AttrGeneratedUID ]
+        )
+        deferred.addCallback(collectResults)
+        return deferred
+
+    def queryDirectory(self, recordTypes, indexType, indexKey):
+        
+        attrs = [
+            dsattributes.kDS1AttrGeneratedUID,
+            dsattributes.kDSNAttrRecordName,
+            dsattributes.kDSNAttrRecordType,
+            dsattributes.kDS1AttrDistinguishedName,
+            dsattributes.kDS1AttrFirstName,
+            dsattributes.kDS1AttrLastName,
+            dsattributes.kDSNAttrEMailAddress,
+            dsattributes.kDSNAttrMetaNodeLocation,
+        ]
+
+        listRecordTypes = []
+        for recordType in recordTypes:
+            if recordType == DirectoryService.recordType_users:
+                listRecordTypes.append(dsattributes.kDSStdRecordTypeUsers)
+    
+            elif recordType == DirectoryService.recordType_groups:
+                listRecordTypes.append(dsattributes.kDSStdRecordTypeGroups)
+                attrs.append(dsattributes.kDSNAttrGroupMembers)
+                attrs.append(dsattributes.kDSNAttrNestedGroups)
+    
+            elif recordType == DirectoryService.recordType_locations:
+                listRecordTypes.append(dsattributes.kDSStdRecordTypePlaces)
+                attrs.append(dsattributes.kDSNAttrResourceInfo)
+            
+            elif recordType == DirectoryService.recordType_resources:
+                listRecordTypes.append(dsattributes.kDSStdRecordTypeResources)
+                attrs.append(dsattributes.kDSNAttrResourceInfo)
+            
+            else:
+                raise UnknownRecordTypeError("Unknown Open Directory record type: %s" % (recordType))
+
+        queryattr = {
+            self.INDEX_TYPE_SHORTNAME : dsattributes.kDSNAttrRecordName,
+            self.INDEX_TYPE_GUID      : dsattributes.kDS1AttrGeneratedUID,
+            self.INDEX_TYPE_EMAIL     : dsattributes.kDSNAttrEMailAddress,
+        }.get(indexType)
+        assert queryattr is not None, "Invalid type for record faulting query"
+        query = dsquery.match(queryattr, indexKey, dsattributes.eDSExact)
+
+        try:
+            self.log_debug("opendirectory.queryRecordsWithAttribute_list(%r,%r,%r,%r,%r,%r,%r)" % (
+                self.directory,
+                query.attribute,
+                query.value,
+                query.matchType,
+                False,
+                listRecordTypes,
+                attrs,
+            ))
+            results = opendirectory.queryRecordsWithAttribute_list(
+                self.directory,
+                query.attribute,
+                query.value,
+                query.matchType,
+                False,
+                listRecordTypes,
+                attrs,
+            )
+            self.log_debug("opendirectory.queryRecordsWithAttribute_list matched records: %s" % (len(results),))
+        except opendirectory.ODError, ex:
+            self.log_error("Open Directory (node=%s) error: %s" % (self.realmName, str(ex)))
+            raise
+
+        for (recordShortName, value) in results:
+
+            # Now get useful record info.
+            recordGUID         = value.get(dsattributes.kDS1AttrGeneratedUID)
+            recordShortNames   = value.get(dsattributes.kDSNAttrRecordName)
+            recordType         = value.get(dsattributes.kDSNAttrRecordType)
+            if isinstance(recordType, list):
+                recordType = recordType[0]                
+            if isinstance(recordShortNames, str):
+                recordShortNames = (recordShortNames,)
+            else:
+                recordShortNames = tuple(recordShortNames) if recordShortNames else ()
+            recordFullName     = value.get(dsattributes.kDS1AttrDistinguishedName)
+            recordFirstName    = value.get(dsattributes.kDS1AttrFirstName)
+            recordLastName     = value.get(dsattributes.kDS1AttrLastName)
+            recordEmailAddress = value.get(dsattributes.kDSNAttrEMailAddress)
+            recordNodeName     = value.get(dsattributes.kDSNAttrMetaNodeLocation)
+
+            if not recordType:
+                self.log_debug("Record (unknown)%s in node %s has no recordType; ignoring."
+                               % (recordShortName, recordNodeName))
+                continue
+            recordType = self._fromODRecordTypes[recordType]
+
+            if not recordGUID:
+                self.log_debug("Record (%s)%s in node %s has no GUID; ignoring."
+                               % (recordType, recordShortName, recordNodeName))
+                continue
+
+            if recordGUID.lower().startswith("ffffeeee-dddd-cccc-bbbb-aaaa"):
+                self.log_debug("Ignoring system record (%s)%s in node %s."
+                               % (recordType, recordShortName, recordNodeName))
+                continue
+
+            # Determine enabled state
+            enabledForCalendaring = True
+
+            if self.restrictEnabledRecords and self.restrictedGUIDs is not None:
+                enabledForCalendaring = recordGUID in self.restrictedGUIDs
+
+            if not enabledForCalendaring:
+                # Some records we want to keep even though they are not enabled for calendaring.
+                # Others we discard.
+                if recordType in (
+                    DirectoryService.recordType_users,
+                    DirectoryService.recordType_groups,
+                ):
+                    self.log_debug(
+                        "Record (%s) %s is not enabled for calendaring but may be used in ACLs"
+                        % (recordType, recordShortName)
+                    )
+                else:
+                    self.log_debug(
+                        "Record (%s) %s is not enabled for calendaring"
+                        % (recordType, recordShortName)
+                    )
+                    continue
+
+            # Get calendar user addresses from directory record.
+            if enabledForCalendaring:
+                calendarUserAddresses = self._calendarUserAddresses(recordType, value)
+            else:
+                calendarUserAddresses = ()
+
+            # Get email address from directory record
+            recordEmailAddresses = set()
+            if isinstance(recordEmailAddress, str):
+                recordEmailAddresses.add(recordEmailAddress.lower())
+            elif isinstance(recordEmailAddress, list):
+                for addr in recordEmailAddresses:
+                    recordEmailAddresses.add(addr.lower())
+
+            # Special case for groups, which have members.
+            if recordType == DirectoryService.recordType_groups:
+                memberGUIDs = value.get(dsattributes.kDSNAttrGroupMembers)
+                if memberGUIDs is None:
+                    memberGUIDs = ()
+                elif type(memberGUIDs) is str:
+                    memberGUIDs = (memberGUIDs,)
+                nestedGUIDs = value.get(dsattributes.kDSNAttrNestedGroups)
+                if nestedGUIDs:
+                    if type(nestedGUIDs) is str:
+                        nestedGUIDs = (nestedGUIDs,)
+                    memberGUIDs += tuple(nestedGUIDs)
+            else:
+                memberGUIDs = ()
+
+            # Special case for resources and locations
+            autoSchedule = False
+            proxyGUIDs = ()
+            readOnlyProxyGUIDs = ()
+            if recordType in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
+                resourceInfo = value.get(dsattributes.kDSNAttrResourceInfo)
+                if resourceInfo is not None:
+                    try:
+                        autoSchedule, proxy, read_only_proxy = self._parseResourceInfo(resourceInfo, recordGUID, recordType, recordShortName)
+                    except ValueError:
+                        continue
+                    if proxy:
+                        proxyGUIDs = (proxy,)
+                    if read_only_proxy:
+                        readOnlyProxyGUIDs = (read_only_proxy,)
+
+            record = OpenDirectoryRecord(
+                service               = self,
+                recordType            = recordType,
+                guid                  = recordGUID,
+                nodeName              = recordNodeName,
+                shortNames            = recordShortNames,
+                fullName              = recordFullName,
+                firstName             = recordFirstName,
+                lastName              = recordLastName,
+                emailAddresses        = recordEmailAddresses,
+                calendarUserAddresses = calendarUserAddresses,
+                autoSchedule          = autoSchedule,
+                enabledForCalendaring = enabledForCalendaring,
+                memberGUIDs           = memberGUIDs,
+                proxyGUIDs            = proxyGUIDs,
+                readOnlyProxyGUIDs    = readOnlyProxyGUIDs,
+            )
+            self.recordCacheForType(recordType).addRecord(record)
+
+class OpenDirectoryRecord(CachingDirectoryRecord):
+    """
+    Open Directory implementation of L{IDirectoryRecord}.
+    """
+    def __init__(
+        self, service, recordType, guid, nodeName, shortNames, fullName,
+        firstName, lastName, emailAddresses,
+        calendarUserAddresses, autoSchedule, enabledForCalendaring,
+        memberGUIDs, proxyGUIDs, readOnlyProxyGUIDs,
+    ):
+        super(OpenDirectoryRecord, self).__init__(
+            service               = service,
+            recordType            = recordType,
+            guid                  = guid,
+            shortNames            = shortNames,
+            fullName              = fullName,
+            firstName             = firstName,
+            lastName              = lastName,
+            emailAddresses        = emailAddresses,
+            calendarUserAddresses = calendarUserAddresses,
+            autoSchedule          = autoSchedule,
+            enabledForCalendaring = enabledForCalendaring,
+        )
+        self.nodeName = nodeName
+        self._memberGUIDs = tuple(memberGUIDs)
+        self._proxyGUIDs = tuple(proxyGUIDs)
+        self._readOnlyProxyGUIDs = tuple(readOnlyProxyGUIDs)
+        
+        self._groupMembershipGUIDs = None
+
+    def __repr__(self):
+        if self.service.realmName == self.nodeName:
+            location = self.nodeName
+        else:
+            location = "%s->%s" % (self.service.realmName, self.nodeName)
+
+        return "<%s[%s@%s(%s)] %s(%s) %r>" % (
+            self.__class__.__name__,
+            self.recordType,
+            self.service.guid,
+            location,
+            self.guid,
+            ",".join(self.shortNames),
+            self.fullName
+        )
+
+    def members(self):
+        if self.recordType != DirectoryService.recordType_groups:
+            return
+
+        for guid in self._memberGUIDs:
+            userRecord = self.service.recordWithGUID(guid)
+            if userRecord is not None:
+                yield userRecord
+
+    def groups(self):
+        if self._groupMembershipGUIDs is None:
+            self._groupMembershipGUIDs = self.service.groupsForGUID(self.guid)
+
+        for guid in self._groupMembershipGUIDs:
+            record = self.service.recordWithGUID(guid)
+            if record:
+                yield record
+
+    def proxies(self):
+        if self.recordType not in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
+            return
+
+        for guid in self._proxyGUIDs:
+            proxyRecord = self.service.recordWithGUID(guid)
+            if proxyRecord is None:
+                self.log_error("No record for proxy in (%s)%s with GUID %s" % (
+                    self.recordType,
+                    self.shortNames[0],
+                    guid,
+                ))
+            else:
+                yield proxyRecord
+
+    def proxyFor(self):
+        result = set()
+        result.update(self.service.proxiesForGUID(DirectoryService.recordType_resources, self.guid))
+        result.update(self.service.proxiesForGUID(DirectoryService.recordType_locations, self.guid))
+        return result
+
+    def readOnlyProxies(self):
+        if self.recordType not in (DirectoryService.recordType_resources, DirectoryService.recordType_locations):
+            return
+
+        for guid in self._readOnlyProxyGUIDs:
+            proxyRecord = self.service.recordWithGUID(guid)
+            if proxyRecord is None:
+                self.log_error("No record for proxy in (%s)%s with GUID %s" % (
+                    self.recordType,
+                    self.shortNames[0],
+                    guid,
+                ))
+            else:
+                yield proxyRecord
+
+    def readOnlyProxyFor(self):
+        result = set()
+        result.update(self.service.readOnlyProxiesForGUID(DirectoryService.recordType_resources, self.guid))
+        result.update(self.service.readOnlyProxiesForGUID(DirectoryService.recordType_locations, self.guid))
+        return result
+
+    def verifyCredentials(self, credentials):
+        if isinstance(credentials, UsernamePassword):
+            # Check cached password
+            try:
+                if credentials.password == self.password:
+                    return True
+            except AttributeError:
+                pass
+
+            # Check with directory services
+            try:
+                if opendirectory.authenticateUserBasic(self.service.directory, self.nodeName, self.shortNames[0], credentials.password):
+                    # Cache the password to avoid future DS queries
+                    self.password = credentials.password
+                    return True
+            except opendirectory.ODError, e:
+                self.log_error("Open Directory (node=%s) error while performing basic authentication for user %s: %s"
+                            % (self.service.realmName, self.shortNames[0], e))
+
+            return False
+
+        elif isinstance(credentials, DigestedCredentials):
+            #
+            # We need a special format for the "challenge" and "response" strings passed into open directory, as it is
+            # picky about exactly what it receives.
+            #
+            try:
+                challenge = 'Digest realm="%(realm)s", nonce="%(nonce)s", algorithm=%(algorithm)s' % credentials.fields
+                response = (
+                    'Digest username="%(username)s", '
+                    'realm="%(realm)s", '
+                    'nonce="%(nonce)s", '
+                    'uri="%(uri)s", '
+                    'response="%(response)s",'
+                    'algorithm=%(algorithm)s'
+                ) % credentials.fields
+            except KeyError, e:
+                self.log_error(
+                    "Open Directory (node=%s) error while performing digest authentication for user %s: "
+                    "missing digest response field: %s in: %s"
+                    % (self.service.realmName, self.shortNames[0], e, credentials.fields)
+                )
+                return False
+
+            try:
+                if self.digestcache[credentials.fields["uri"]] == response:
+                    return True
+            except (AttributeError, KeyError):
+                pass
+
+            try:
+                if opendirectory.authenticateUserDigest(
+                    self.service.directory,
+                    self.nodeName,
+                    self.shortNames[0],
+                    challenge,
+                    response,
+                    credentials.method
+                ):
+                    try:
+                        cache = self.digestcache
+                    except AttributeError:
+                        cache = self.digestcache = {}
+
+                    cache[credentials.fields["uri"]] = response
+
+                    return True
+                else:
+                    self.log_debug(
+"""Open Directory digest authentication failed with:
+    Nodename:  %s
+    Username:  %s
+    Challenge: %s
+    Response:  %s
+    Method:    %s
+""" % (self.nodeName, self.shortNames[0], challenge, response, credentials.method))
+
+            except opendirectory.ODError, e:
+                self.log_error(
+                    "Open Directory (node=%s) error while performing digest authentication for user %s: %s"
+                    % (self.service.realmName, self.shortNames[0], e)
+                )
+                return False
+
+            return False
+
+        return super(OpenDirectoryRecord, self).verifyCredentials(credentials)
+
+class OpenDirectoryInitError(DirectoryError):
+    """
+    OpenDirectory initialization error.
+    """

Copied: CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingdirectory.py (from rev 3898, CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3672/twistedcaldav/directory/cachingdirectory.py)
===================================================================
--- CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingdirectory.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingdirectory.py	2009-03-19 20:45:50 UTC (rev 3899)
@@ -0,0 +1,244 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord
+import time
+import types
+
+"""
+Caching directory service implementation.
+"""
+
+__all__ = [
+    "CachingDirectoryService",
+    "CachingDirectoryRecord",
+    "DictRecordTypeCache",
+]
+
+class RecordTypeCache(object):
+    """
+    Abstract class for a record type cache. We will likely have dict and memcache implementations of this.
+    """
+    
+    def __init__(self, directoryService, recordType):
+        
+        self.directoryService = directoryService
+        self.recordType = recordType
+
+    def addRecord(self, record):
+        raise NotImplementedError()
+    
+    def removeRecord(self, record):
+        raise NotImplementedError()
+        
+    def replaceRecord(self, oldRecord, newRecord):
+        raise NotImplementedError()
+
+    def findRecord(self, indexType, indexKey):
+        raise NotImplementedError()
+        
+class DictRecordTypeCache(RecordTypeCache):
+    """
+    Cache implementation using a dict. Does not share the cache with other instances.
+    """
+    
+    def __init__(self, directoryService, recordType):
+        
+        super(DictRecordTypeCache, self).__init__(directoryService, recordType)
+        self.records = set()
+        self.recordsIndexedBy = {
+            CachingDirectoryService.INDEX_TYPE_GUID     : {},
+            CachingDirectoryService.INDEX_TYPE_SHORTNAME: {},
+            CachingDirectoryService.INDEX_TYPE_EMAIL    : {},
+        }
+
+    def addRecord(self, record):
+        
+        self.records.add(record)
+        for indexType in self.directoryService.indexTypes():
+            try:
+                indexData = getattr(record, CachingDirectoryService.indexTypeToRecordAttribute[indexType])
+            except AttributeError:
+                continue
+            if isinstance(indexData, str):
+                indexData = (indexData,)
+            if type(indexData) in (types.ListType, types.TupleType, set):
+                for item in indexData:
+                    self.recordsIndexedBy[indexType][item] = record
+            elif indexData is None:
+                pass
+            else:
+                raise AssertionError("Data from record attribute must be str, list or tuple")
+    
+    def removeRecord(self, record):
+        
+        if record in self.records:
+            self.records.remove(record)
+            for indexType in self.directoryService.indexTypes():
+                try:
+                    indexData = getattr(record, CachingDirectoryService.indexTypeToRecordAttribute[indexType])
+                except AttributeError:
+                    continue
+                if isinstance(indexData, str):
+                    indexData = (indexData,)
+                if type(indexData) in (types.ListType, types.TupleType):
+                    for item in indexData:
+                        try:
+                            del self.recordsIndexedBy[indexType][item]
+                        except KeyError:
+                            raise AssertionError("Missing record index item")
+                else:
+                    raise AssertionError("Data from record attribute must be str, list or tuple")
+        
+    def replaceRecord(self, oldRecord, newRecord):
+        self.removeRecord(oldRecord)
+        self.addRecord(newRecord)
+
+    def findRecord(self, indexType, indexKey):
+        return self.recordsIndexedBy[indexType].get(indexKey)
+
+class CachingDirectoryService(DirectoryService):
+    """
+    Caching Directory implementation of L{IDirectoryService}.
+    
+    This is class must be overridden to provide a concrete implementation.
+    """
+
+    INDEX_TYPE_GUID      = "guid"
+    INDEX_TYPE_SHORTNAME = "shortname"
+    INDEX_TYPE_EMAIL     = "email"
+
+    indexTypeToRecordAttribute = {
+        "guid"     : "guid",
+        "shortname": "shortNames",
+        "email"    : "emailAddresses",
+    }
+
+    def __init__(
+        self,
+        cacheTimeout=30,
+        cacheClass=DictRecordTypeCache,
+    ):
+        """
+        @param cacheTimeout: C{int} number of minutes before cache is invalidated.
+        """
+        
+        self.cacheTimeout = cacheTimeout * 60
+
+        self._initCaches(cacheClass)
+
+    def _initCaches(self, cacheClass):
+        self._recordCaches = dict([
+            (recordType, cacheClass(self, recordType))
+            for recordType in self.recordTypes()
+        ])
+            
+        self._disabledKeys = dict([(indexType, dict()) for indexType in self.indexTypes()])
+
+    def indexTypes(self):
+        
+        return (
+            CachingDirectoryService.INDEX_TYPE_GUID,
+            CachingDirectoryService.INDEX_TYPE_SHORTNAME,
+            CachingDirectoryService.INDEX_TYPE_EMAIL,
+        )
+
+    def recordCacheForType(self, recordType):
+        return self._recordCaches[recordType]
+
+    def listRecords(self, recordType):
+        return self.recordCacheForType(recordType).records
+
+    def recordWithShortName(self, recordType, shortName):
+        return self._lookupRecord((recordType,), CachingDirectoryService.INDEX_TYPE_SHORTNAME, shortName)
+
+    def recordWithEmailAddress(self, emailAddress):
+        return self._lookupRecord(None, CachingDirectoryService.INDEX_TYPE_EMAIL, emailAddress)
+
+    def recordWithGUID(self, guid):
+        return self._lookupRecord(None, CachingDirectoryService.INDEX_TYPE_GUID, guid)
+
+    recordWithUID = recordWithGUID
+
+    def _lookupRecord(self, recordTypes, indexType, indexKey, cacheOnMiss=True):
+        
+        if recordTypes is None:
+            recordTypes = self.recordTypes()
+        
+        def lookup():
+            for recordType in recordTypes:
+                record = self.recordCacheForType(recordType).findRecord(indexType, indexKey)
+                if record:
+                    return record
+            else:
+                return None
+
+        record = lookup()
+        if record:
+            return record
+
+        if cacheOnMiss:
+            
+            # Check negative cache (take cache entry timeout into account)
+            try:
+                disabledTime = self._disabledKeys[indexType][indexKey]
+                if time.time() - disabledTime < self.cacheTimeout:
+                    return None
+            except KeyError:
+                pass
+            
+            # Try query
+            self.log_debug("Faulting record for attribute '%s' with value '%s'" % (indexType, indexKey,))
+            self.queryDirectory(recordTypes, indexType, indexKey)
+            
+            # Now try again from cache
+            record = lookup()
+            if record:
+                self.log_debug("Found record for attribute '%s' with value '%s'" % (indexType, indexKey,))
+                return record
+            
+            # Add to negative cache with timestamp
+            self.log_debug("Failed to fault record for attribute '%s' with value '%s'" % (indexType, indexKey,))
+            self._disabledKeys[indexType][indexKey] = time.time()
+            
+        return None
+
+    def queryDirectory(self, recordTypes, indexType, indexKey):
+        raise NotImplementedError()
+
+class CachingDirectoryRecord(DirectoryRecord):
+
+    def __init__(
+        self, service, recordType, guid, shortNames, fullName,
+        firstName, lastName, emailAddresses,
+        calendarUserAddresses, autoSchedule, enabledForCalendaring=True,
+        uid=None,
+    ):
+        super(CachingDirectoryRecord, self).__init__(
+            service               = service,
+            recordType            = recordType,
+            guid                  = guid,
+            shortNames            = shortNames,
+            fullName              = fullName,
+            firstName             = firstName,
+            lastName              = lastName,
+            emailAddresses        = emailAddresses,
+            calendarUserAddresses = calendarUserAddresses,
+            autoSchedule          = autoSchedule,
+            enabledForCalendaring = enabledForCalendaring,
+            uid                   = uid,
+        )
+        
+        self.cachedTime = time.time()

Copied: CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingxmlfile.py (from rev 3898, CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3672/twistedcaldav/directory/cachingxmlfile.py)
===================================================================
--- CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingxmlfile.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/cachingxmlfile.py	2009-03-19 20:45:50 UTC (rev 3899)
@@ -0,0 +1,228 @@
+##
+# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+XML based user/group/resource directory service implementation.
+"""
+
+__all__ = [
+    "XMLDirectoryService",
+]
+
+from time import time
+import types
+
+from twisted.cred.credentials import UsernamePassword
+from twisted.web2.auth.digest import DigestedCredentials
+from twisted.python.filepath import FilePath
+
+from twistedcaldav.directory.directory import DirectoryService
+from twistedcaldav.directory.cachingdirectory import CachingDirectoryService,\
+    CachingDirectoryRecord
+from twistedcaldav.directory.xmlaccountsparser import XMLAccountsParser
+
+class XMLDirectoryService(CachingDirectoryService):
+    """
+    XML based implementation of L{IDirectoryService}.
+    """
+    baseGUID = "9CA8DEC5-5A17-43A9-84A8-BE77C1FB9172"
+
+    realmName = None
+
+    def __repr__(self):
+        return "<%s %r: %r>" % (self.__class__.__name__, self.realmName, self.xmlFile)
+
+    def __init__(self, xmlFile, alwaysStat=False):
+        super(XMLDirectoryService, self).__init__()
+
+        if type(xmlFile) is str:
+            xmlFile = FilePath(xmlFile)
+
+        self.xmlFile = xmlFile
+        self._fileInfo = None
+        self._lastCheck = 0
+        self._alwaysStat = alwaysStat
+        self._accounts()
+
+    def recordTypes(self):
+        recordTypes = (
+            DirectoryService.recordType_users,
+            DirectoryService.recordType_groups,
+            DirectoryService.recordType_locations,
+            DirectoryService.recordType_resources
+        )
+        return recordTypes
+
+    def queryDirectory(self, recordTypes, indexType, indexKey):
+        
+        for recordType in recordTypes:
+            for xmlPrincipal in self._accounts()[recordType].itervalues():
+                
+                matched = False
+                if indexType == self.INDEX_TYPE_GUID:
+                    matched = indexKey == xmlPrincipal.guid
+                elif indexType == self.INDEX_TYPE_SHORTNAME:
+                    matched = indexKey in xmlPrincipal.shortNames
+                elif indexType == self.INDEX_TYPE_EMAIL:
+                    matched = indexKey in xmlPrincipal.emailAddresses
+                
+                if matched:
+                    record = XMLDirectoryRecord(
+                        service       = self,
+                        recordType    = recordType,
+                        shortNames    = tuple(xmlPrincipal.shortNames),
+                        xmlPrincipal  = xmlPrincipal,
+                    )
+                    self.recordCacheForType(recordType).addRecord(record)
+            
+    def recordsMatchingFields(self, fields, operand="or", recordType=None):
+        # Default, brute force method search of underlying XML data
+
+        def fieldMatches(fieldValue, value, caseless, matchType):
+            if fieldValue is None:
+                return False
+            elif type(fieldValue) in types.StringTypes:
+                fieldValue = (fieldValue,)
+            
+            for testValue in fieldValue:
+                if caseless:
+                    testValue = testValue.lower()
+                    value = value.lower()
+    
+                if matchType == 'starts-with':
+                    if testValue.startswith(value):
+                        return True
+                elif matchType == 'contains':
+                    try:
+                        _ignore_discard = testValue.index(value)
+                        return True
+                    except ValueError:
+                        pass
+                else: # exact
+                    if testValue == value:
+                        return True
+                    
+            return False
+
+        def xmlPrincipalMatches(xmlPrincipal):
+            if operand == "and":
+                for fieldName, value, caseless, matchType in fields:
+                    try:
+                        fieldValue = getattr(xmlPrincipal, fieldName)
+                        if not fieldMatches(fieldValue, value, caseless, matchType):
+                            return False
+                    except AttributeError:
+                        # No property => no match
+                        return False
+                # we hit on every property
+                return True
+            else: # "or"
+                for fieldName, value, caseless, matchType in fields:
+                    try:
+                        fieldValue = getattr(xmlPrincipal, fieldName)
+                        if fieldMatches(fieldValue, value, caseless, matchType):
+                            return True
+                    except AttributeError:
+                        # No value
+                        pass
+                # we didn't hit any
+                return False
+
+        if recordType is None:
+            recordTypes = list(self.recordTypes())
+        else:
+            recordTypes = (recordType,)
+
+        for recordType in recordTypes:
+            for xmlPrincipal in self._accounts()[recordType].itervalues():
+                if xmlPrincipalMatches(xmlPrincipal):
+                    
+                    # Load/cache record from its GUID
+                    record = self.recordWithGUID(xmlPrincipal.guid)
+                    if record:
+                        yield record
+
+    def _accounts(self):
+        currentTime = time()
+        if self._alwaysStat or currentTime - self._lastCheck > 60:
+            self.xmlFile.restat()
+            self._lastCheck = currentTime
+        fileInfo = (self.xmlFile.getmtime(), self.xmlFile.getsize())
+        if fileInfo != self._fileInfo:
+            parser = XMLAccountsParser(self.xmlFile)
+            self._parsedAccounts = parser.items
+            self.realmName = parser.realm
+            self._fileInfo = fileInfo
+        return self._parsedAccounts
+
+class XMLDirectoryRecord(CachingDirectoryRecord):
+    """
+    XML based implementation implementation of L{IDirectoryRecord}.
+    """
+    def __init__(self, service, recordType, shortNames, xmlPrincipal):
+        super(XMLDirectoryRecord, self).__init__(
+            service               = service,
+            recordType            = recordType,
+            guid                  = xmlPrincipal.guid,
+            shortNames            = shortNames,
+            fullName              = xmlPrincipal.fullName,
+            firstName             = xmlPrincipal.firstName,
+            lastName              = xmlPrincipal.lastName,
+            emailAddresses        = xmlPrincipal.emailAddresses,
+            calendarUserAddresses = xmlPrincipal.calendarUserAddresses,
+            autoSchedule          = xmlPrincipal.autoSchedule,
+            enabledForCalendaring = xmlPrincipal.enabledForCalendaring,
+        )
+
+        self.password          = xmlPrincipal.password
+        self._members          = xmlPrincipal.members
+        self._groups           = xmlPrincipal.groups
+        self._proxies          = xmlPrincipal.proxies
+        self._proxyFor         = xmlPrincipal.proxyFor
+        self._readOnlyProxies  = xmlPrincipal.readOnlyProxies
+        self._readOnlyProxyFor = xmlPrincipal.readOnlyProxyFor
+
+    def members(self):
+        for recordType, shortName in self._members:
+            yield self.service.recordWithShortName(recordType, shortName)
+
+    def groups(self):
+        for shortName in self._groups:
+            yield self.service.recordWithShortName(DirectoryService.recordType_groups, shortName)
+
+    def proxies(self):
+        for recordType, shortName in self._proxies:
+            yield self.service.recordWithShortName(recordType, shortName)
+
+    def proxyFor(self, read_write=True):
+        for recordType, shortName in self._proxyFor:
+            yield self.service.recordWithShortName(recordType, shortName)
+
+    def readOnlyProxies(self):
+        for recordType, shortName in self._readOnlyProxies:
+            yield self.service.recordWithShortName(recordType, shortName)
+
+    def readOnlyProxyFor(self, read_write=True):
+        for recordType, shortName in self._readOnlyProxyFor:
+            yield self.service.recordWithShortName(recordType, shortName)
+
+    def verifyCredentials(self, credentials):
+        if isinstance(credentials, UsernamePassword):
+            return credentials.password == self.password
+        if isinstance(credentials, DigestedCredentials):
+            return credentials.checkPassword(self.password)
+
+        return super(XMLDirectoryRecord, self).verifyCredentials(credentials)

Modified: CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/sqldb.py
===================================================================
--- CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/sqldb.py	2009-03-19 20:44:02 UTC (rev 3898)
+++ CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/sqldb.py	2009-03-19 20:45:50 UTC (rev 3899)
@@ -176,7 +176,7 @@
         shortName = record.shortNames[0]
         guid = record.guid
         password = record.password
-        name = record.name
+        name = record.fullName
 
         self._db_execute(
             """

Copied: CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/test/test_cachedirectory.py (from rev 3898, CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3672/twistedcaldav/directory/test/test_cachedirectory.py)
===================================================================
--- CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/test/test_cachedirectory.py	                        (rev 0)
+++ CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/test/test_cachedirectory.py	2009-03-19 20:45:50 UTC (rev 3899)
@@ -0,0 +1,219 @@
+#
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.directory.cachingdirectory import CachingDirectoryService,\
+    CachingDirectoryRecord, DictRecordTypeCache
+from twistedcaldav.test.util import TestCase
+from twistedcaldav.directory.directory import DirectoryService
+from twistedcaldav.directory.util import uuidFromName
+from uuid import uuid4
+
+class TestDirectoryService (CachingDirectoryService):
+
+    realmName = "Dummy Realm"
+
+    def recordTypes(self):
+        return (
+            DirectoryService.recordType_users,
+            DirectoryService.recordType_groups,
+            DirectoryService.recordType_locations,
+            DirectoryService.recordType_resources,
+        )
+
+    def queryDirectory(self, recordTypes, indexType, indexKey):
+        
+        self.queried = True
+
+        for recordType in recordTypes:
+            for record in self.fakerecords[recordType]:
+                cacheIt = False
+                if indexType in (
+                    CachingDirectoryService.INDEX_TYPE_SHORTNAME,
+                    CachingDirectoryService.INDEX_TYPE_EMAIL
+                ):
+                    if indexKey in record[indexType]:
+                        cacheIt = True
+                else:
+                    if indexKey == record[indexType]:
+                        cacheIt = True
+
+                if cacheIt:
+                    cacheRecord = CachingDirectoryRecord(
+                        service               = self,
+                        recordType            = recordType,
+                        guid                  = record.get("guid"),
+                        shortNames            = record.get("shortname"),
+                        fullName              = record.get("fullName"),
+                        firstName             = "",
+                        lastName              = "",
+                        emailAddresses        = record.get("email"),
+                        calendarUserAddresses = record.get("email"),
+                        autoSchedule          = False,
+                        enabledForCalendaring = True,
+                    ) 
+                    self.recordCacheForType(recordType).addRecord(cacheRecord)
+
+class CachingDirectoryTest(TestCase):
+    
+    baseGUID = str(uuid4())
+
+    def setUp(self):
+        super(CachingDirectoryTest, self).setUp()
+        self.service = TestDirectoryService()
+        self.service.queried = False
+
+    def loadRecords(self, records):
+        self.service._initCaches(DictRecordTypeCache)
+        self.service.fakerecords = records
+        self.service.queried = False
+
+    def fakeRecord(
+        self,
+        fullName,
+        shortNames=None,
+        guid=None,
+        emails=None,
+        members=None,
+        resourceInfo=None,
+        multinames=False
+    ):
+        if shortNames is None:
+            shortNames = (self.shortNameForFullName(fullName),)
+            if multinames:
+                shortNames += (fullName,)
+    
+        if guid is None:
+            guid = self.guidForShortName(shortNames[0])
+        else:
+            guid = guid.lower()
+    
+        if emails is None:
+            emails = ("%s at example.com" % (shortNames[0],),)
+    
+        attrs = {
+            "fullName": fullName,
+            "guid": guid,
+            "shortname": shortNames,
+            "email": emails,
+        }
+        
+        if members:
+            attrs["members"] = members
+    
+        if resourceInfo:
+            attrs["resourceInfo"] = resourceInfo
+    
+        return attrs
+    
+    def shortNameForFullName(self, fullName):
+        return fullName.lower().replace(" ", "")
+    
+    def guidForShortName(self, shortName):
+        return uuidFromName(self.baseGUID, shortName)
+
+    def dummyRecords(self):
+        SIZE = 10
+        self.loadRecords({
+            DirectoryService.recordType_users: [
+                self.fakeRecord("User %02d" % x, multinames=(x>5)) for x in range(1,SIZE+1)
+            ],
+            DirectoryService.recordType_groups: [
+                self.fakeRecord("Group %02d" % x) for x in range(1,SIZE+1)
+            ],
+            DirectoryService.recordType_resources: [
+                self.fakeRecord("Resource %02d" % x) for x in range(1,SIZE+1)
+            ],
+            DirectoryService.recordType_locations: [
+                self.fakeRecord("Location %02d" % x) for x in range(1,SIZE+1)
+            ],
+        })
+
+    def verifyRecords(self, recordType, expectedGUIDs):
+        
+        records = self.service.listRecords(recordType)
+        recordGUIDs = set([record.guid for record in records])
+        self.assertEqual(recordGUIDs, expectedGUIDs)
+
+class GUIDLookups(CachingDirectoryTest):
+    
+    def test_emptylist(self):
+        self.dummyRecords()
+
+        self.verifyRecords(DirectoryService.recordType_users, set())
+        self.verifyRecords(DirectoryService.recordType_groups, set())
+        self.verifyRecords(DirectoryService.recordType_resources, set())
+        self.verifyRecords(DirectoryService.recordType_locations, set())
+    
+    def test_cacheoneguid(self):
+        self.dummyRecords()
+
+        self.assertTrue(self.service.recordWithGUID(self.guidForShortName("user01")) is not None)
+        self.assertTrue(self.service.queried)
+        self.verifyRecords(DirectoryService.recordType_users, set((
+            self.guidForShortName("user01"),
+        )))
+        self.verifyRecords(DirectoryService.recordType_groups, set())
+        self.verifyRecords(DirectoryService.recordType_resources, set())
+        self.verifyRecords(DirectoryService.recordType_locations, set())
+
+        # Make sure it really is cached and won't cause another query
+        self.service.queried = False
+        self.assertTrue(self.service.recordWithGUID(self.guidForShortName("user01")) is not None)
+        self.assertFalse(self.service.queried)
+        
+    def test_cacheoneshortname(self):
+        self.dummyRecords()
+
+        self.assertTrue(self.service.recordWithShortName(
+            DirectoryService.recordType_users,
+            "user02"
+        ) is not None)
+        self.assertTrue(self.service.queried)
+        self.verifyRecords(DirectoryService.recordType_users, set((
+            self.guidForShortName("user02"),
+        )))
+        self.verifyRecords(DirectoryService.recordType_groups, set())
+        self.verifyRecords(DirectoryService.recordType_resources, set())
+        self.verifyRecords(DirectoryService.recordType_locations, set())
+
+        # Make sure it really is cached and won't cause another query
+        self.service.queried = False
+        self.assertTrue(self.service.recordWithShortName(
+            DirectoryService.recordType_users,
+            "user02"
+        ) is not None)
+        self.assertFalse(self.service.queried)
+
+    def test_cacheoneemail(self):
+        self.dummyRecords()
+
+        self.assertTrue(self.service.recordWithEmailAddress(
+            "user03 at example.com"
+        ) is not None)
+        self.assertTrue(self.service.queried)
+        self.verifyRecords(DirectoryService.recordType_users, set((
+            self.guidForShortName("user03"),
+        )))
+        self.verifyRecords(DirectoryService.recordType_groups, set())
+        self.verifyRecords(DirectoryService.recordType_resources, set())
+        self.verifyRecords(DirectoryService.recordType_locations, set())
+
+        # Make sure it really is cached and won't cause another query
+        self.service.queried = False
+        self.assertTrue(self.service.recordWithEmailAddress(
+            "user03 at example.com"
+        ) is not None)
+        self.assertFalse(self.service.queried)

Modified: CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/xmlaccountsparser.py
===================================================================
--- CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/xmlaccountsparser.py	2009-03-19 20:44:02 UTC (rev 3898)
+++ CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/xmlaccountsparser.py	2009-03-19 20:45:50 UTC (rev 3899)
@@ -165,7 +165,7 @@
         self.shortNames = []
         self.guid = None
         self.password = None
-        self.name = None
+        self.fullName = None
         self.firstName = None
         self.lastName = None
         self.emailAddresses = set()
@@ -202,10 +202,10 @@
             password = self.password % ctr
         else:
             password = self.password
-        if self.name.find("%") != -1:
-            name = self.name % ctr
+        if self.fullName.find("%") != -1:
+            fullName = self.fullName % ctr
         else:
-            name = self.name
+            fullName = self.fullName
         if self.firstName and self.firstName.find("%") != -1:
             firstName = self.firstName % ctr
         else:
@@ -231,7 +231,7 @@
         result.shortNames = shortNames
         result.guid = guid
         result.password = password
-        result.name = name
+        result.fullName = fullName
         result.firstName = firstName
         result.lastName = lastName
         result.emailAddresses = emailAddresses
@@ -264,7 +264,7 @@
                     self.password = child.firstChild.data.encode("utf-8")
             elif child_name == ELEMENT_NAME:
                 if child.firstChild is not None:
-                    self.name = child.firstChild.data.encode("utf-8")
+                    self.fullName = child.firstChild.data.encode("utf-8")
             elif child_name == ELEMENT_FIRST_NAME:
                 if child.firstChild is not None:
                     self.firstName = child.firstChild.data.encode("utf-8")

Modified: CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/xmlfile.py
===================================================================
--- CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/xmlfile.py	2009-03-19 20:44:02 UTC (rev 3898)
+++ CalendarServer/branches/users/sagen/directory-cache-on-demand-3897/twistedcaldav/directory/xmlfile.py	2009-03-19 20:45:50 UTC (rev 3899)
@@ -114,7 +114,7 @@
             recordType            = recordType,
             guid                  = xmlPrincipal.guid,
             shortNames            = shortNames,
-            fullName              = xmlPrincipal.name,
+            fullName              = xmlPrincipal.fullName,
             firstName             = xmlPrincipal.firstName,
             lastName              = xmlPrincipal.lastName,
             emailAddresses        = xmlPrincipal.emailAddresses,
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20090319/3eb65dfc/attachment-0001.html>


More information about the calendarserver-changes mailing list