Revision: 784 http://trac.macosforge.org/projects/calendarserver/changeset/784 Author: wsanchez@apple.com Date: 2006-12-08 22:30:19 -0800 (Fri, 08 Dec 2006) Log Message: ----------- Add AggregateDirectoryService Modified Paths: -------------- CalendarServer/trunk/twistedcaldav/directory/test/util.py Added Paths: ----------- CalendarServer/trunk/twistedcaldav/directory/aggregate.py CalendarServer/trunk/twistedcaldav/directory/test/test_aggregate.py Added: CalendarServer/trunk/twistedcaldav/directory/aggregate.py =================================================================== --- CalendarServer/trunk/twistedcaldav/directory/aggregate.py (rev 0) +++ CalendarServer/trunk/twistedcaldav/directory/aggregate.py 2006-12-09 06:30:19 UTC (rev 784) @@ -0,0 +1,111 @@ +## +# Copyright (c) 2006 Apple Computer, 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. +# +# DRI: Wilfredo Sanchez, wsanchez@apple.com +## + +""" +Directory service implementation which aggregates multiple directory +services. +""" + +__all__ = [ + "AggregateDirectoryService", + "DuplicateRecordTypeError", +] + +from twistedcaldav.directory.idirectory import IDirectoryService +from twistedcaldav.directory.directory import DirectoryService, DirectoryRecord, DirectoryError +from twistedcaldav.directory.directory import UnknownRecordTypeError + +import twistedcaldav.directory.test.util + +class AggregateDirectoryService(DirectoryService): + """ + L{IDirectoryService} implementation which aggregates multiple directory services. + """ + baseGUID = "06FB225F-39E7-4D34-B1D1-29925F5E619B" + + def __init__(self, services): + DirectoryService.__init__(self) + + realmName = None + recordTypes = {} + + for service in services: + service = IDirectoryService(service) + + if service.realmName != realmName: + assert realmName is None, ( + "Aggregated directory services must have the same realm name: %r != %r" + % (service.realmName, realmName) + ) + realmName = service.realmName + + if not hasattr(service, "recordTypePrefix"): + service.recordTypePrefix = "" + prefix = service.recordTypePrefix + + for recordType in (prefix + r for r in service.recordTypes()): + if recordType in recordTypes: + raise DuplicateRecordTypeError( + "%r is in multiple services: %s, %s" + % (recordType, recordTypes[recordType], service) + ) + recordTypes[recordType] = service + + self.realmName = realmName + self._recordTypes = recordTypes + + def recordTypes(self): + return set(self._recordTypes) + + def listRecords(self, recordType): + return self._query("listRecords", recordType) + + def recordWithShortName(self, recordType, shortName): + return self._query("recordWithShortName", recordType, shortName) + + def recordWithGUID(self, guid): + return self._queryAll("recordWithGUID", address) + + def recordWithCalendarUserAddress(self, address): + return self._queryAll("recordWithCalendarUserAddress", address) + + def serviceForRecordType(self, recordType): + try: + return self._recordTypes[recordType] + except KeyError: + raise UnknownRecordTypeError(recordType) + + def _query(self, query, recordType, *args): + service = self.serviceForRecordType(recordType) + return getattr(service, query)( + recordType[len(service.recordTypePrefix):], + *[a[len(service.recordTypePrefix):] for a in args] + ) + + def _queryAll(self, query, *args): + for service in self._recordTypes.values(): + record = getattr(service, query)(*args) + if record is not None: + return record + else: + return None + +class DuplicateRecordTypeError(DirectoryError): + """ + Duplicate record type. + """ Added: CalendarServer/trunk/twistedcaldav/directory/test/test_aggregate.py =================================================================== --- CalendarServer/trunk/twistedcaldav/directory/test/test_aggregate.py (rev 0) +++ CalendarServer/trunk/twistedcaldav/directory/test/test_aggregate.py 2006-12-09 06:30:19 UTC (rev 784) @@ -0,0 +1,77 @@ +## +# Copyright (c) 2005-2006 Apple Computer, 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. +# +# DRI: Wilfredo Sanchez, wsanchez@apple.com +## + +from itertools import chain + +from twistedcaldav.directory.apache import BasicDirectoryService +from twistedcaldav.directory.xmlfile import XMLDirectoryService +from twistedcaldav.directory.aggregate import AggregateDirectoryService + +from twistedcaldav.directory.test.test_apache import digestRealm, basicUserFile, groupFile +from twistedcaldav.directory.test.test_xmlfile import xmlFile + +import twistedcaldav.directory.test.util + +apache_prefix = "apache:" +xml_prefix = "xml:" + +testServices = ( + (apache_prefix, twistedcaldav.directory.test.test_apache.Apache ), + (xml_prefix , twistedcaldav.directory.test.test_xmlfile.XMLFile), +) + +class AggregatedDirectories (twistedcaldav.directory.test.util.DirectoryTestCase): + def _recordTypes(self): + recordTypes = set() + for prefix, testClass in testServices: + for recordType in testClass.recordTypes: + recordTypes.add(prefix + recordType) + return recordTypes + + def _records(key): + def get(self): + records = {} + for prefix, testClass in testServices: + for record, info in getattr(testClass, key).iteritems(): + info = dict(info) + info["prefix"] = prefix + info["members"] = tuple( + (t, prefix + s) for t, s in info.get("members", {}) + ) + records[prefix + record] = info + return records + return get + + recordTypes = property(_recordTypes) + users = property(_records("users")) + groups = property(_records("groups")) + resources = property(_records("resources")) + + recordTypePrefixes = tuple(s[0] for s in testServices) + + def service(self): + """ + Returns an IDirectoryService. + """ + apacheService = BasicDirectoryService(digestRealm, basicUserFile, groupFile) + apacheService.recordTypePrefix = apache_prefix + + xmlService = XMLDirectoryService(xmlFile) + xmlService.recordTypePrefix = xml_prefix + + return AggregateDirectoryService((apacheService, xmlService)) Modified: CalendarServer/trunk/twistedcaldav/directory/test/util.py =================================================================== --- CalendarServer/trunk/twistedcaldav/directory/test/util.py 2006-12-09 03:49:36 UTC (rev 783) +++ CalendarServer/trunk/twistedcaldav/directory/test/util.py 2006-12-09 06:30:19 UTC (rev 784) @@ -23,6 +23,8 @@ from twisted.cred.credentials import UsernamePassword from twisted.web2.auth.digest import DigestedCredentials, calcResponse, calcHA1 +from twistedcaldav.directory.directory import UnknownRecordTypeError + # FIXME: Add tests for GUID hooey, once we figure out what that means here class DirectoryTestCase (twisted.trial.unittest.TestCase): @@ -48,6 +50,9 @@ """ raise NotImplementedError("Subclass needs to implement service()") + # For aggregator subclasses + recordTypePrefixes = ("",) + def test_realm(self): """ IDirectoryService.realm @@ -90,44 +95,31 @@ self.assertEquals(self.recordNames("resource"), set(self.resources.keys())) - def test_recordWithShortName_user(self): + def test_recordWithShortName(self): """ - IDirectoryService.recordWithShortName("user") + IDirectoryService.recordWithShortName() """ - if not self.users: - raise SkipTest("No users") + for recordType, data in ( + ( "user" , self.users ), + ( "group" , self.groups ), + ( "resource", self.resources ), + ): + if not data: + raise SkipTest("No %s" % (recordType,)) - service = self.service() - for shortName in self.users: - record = service.recordWithShortName("user", shortName) - self.compare(record, shortName, self.users[shortName]) - self.assertEquals(service.recordWithShortName("user", "IDunnoWhoThisIsIReallyDont"), None) + service = self.service() + for shortName, info in data.iteritems(): + record = service.recordWithShortName(info.get("prefix", "") + recordType, shortName) + self.failUnless(record, "No record (%s)%s" % (info.get("prefix", "") + recordType, shortName)) + self.compare(record, shortName, data[shortName]) - def test_recordWithShortName_group(self): - """ - IDirectoryService.recordWithShortName("group") - """ - if not self.groups: - raise SkipTest("No groups") + for prefix in self.recordTypePrefixes: + try: + record = service.recordWithShortName(prefix + recordType, "IDunnoWhoThisIsIReallyDont") + except UnknownRecordTypeError: + continue + self.assertEquals(record, None) - service = self.service() - for shortName in self.groups: - record = service.recordWithShortName("group", shortName) - self.compare(record, shortName, self.groups[shortName]) - self.assertEquals(service.recordWithShortName("group", "IDunnoWhoThisIsIReallyDont"), None) - - def test_recordWithShortName_resource(self): - """ - XMLDirectoryService.recordWithShortName("resource") - """ - if not self.resources: - raise SkipTest("No resources") - - service = self.service() - for shortName in self.resources: - record = service.recordWithShortName("resource", shortName) - self.compare(record, shortName, self.resources[shortName]) - def test_recordWithGUID(self): service = self.service() record = None @@ -161,9 +153,10 @@ raise SkipTest("No groups") service = self.service() - for group in self.groups: - groupRecord = service.recordWithShortName("group", group) - result = set((m.recordType, m.shortName) for m in groupRecord.members()) + for group, info in self.groups.iteritems(): + prefix = info.get("prefix", "") + groupRecord = service.recordWithShortName(prefix + "group", group) + result = set((m.recordType, prefix + m.shortName) for m in groupRecord.members()) expected = set(self.groups[group]["members"]) self.assertEquals( result, expected, @@ -184,9 +177,10 @@ ( "group", self.groups ), ): service = self.service() - for shortName in data: - record = service.recordWithShortName(recordType, shortName) - result = set(g.shortName for g in record.groups()) + for shortName, info in data.iteritems(): + prefix = info.get("prefix", "") + record = service.recordWithShortName(prefix + recordType, shortName) + result = set(prefix + g.shortName for g in record.groups()) expected = set(g for g in self.groups if (record.recordType, shortName) in self.groups[g]["members"]) self.assertEquals( result, expected, @@ -194,8 +188,18 @@ ) def recordNames(self, recordType): - return set(r.shortName for r in self.service().listRecords(recordType)) + service = self.service() + names = set() + for prefix in self.recordTypePrefixes: + try: + records = service.listRecords(prefix + recordType) + except UnknownRecordTypeError: + continue + for record in records: + names.add(prefix + record.shortName) + return names + def allEntries(self): for data, recordType in ( (self.users, "user" ), @@ -219,7 +223,12 @@ addresses = set(value("addresses")) addresses.add("urn:uuid:%s" % (record.guid,)) - self.assertEquals(record.shortName, shortName) + if hasattr(record.service, "recordTypePrefix"): + prefix = record.service.recordTypePrefix + else: + prefix = "" + + self.assertEquals(prefix + record.shortName, shortName) self.assertEquals(set(record.calendarUserAddresses), addresses) if value("guid"): @@ -228,6 +237,13 @@ if value("name"): self.assertEquals(record.fullName, value("name")) + def servicePrefix(self): + service = self.service() + if hasattr(service, "recordTypePrefix"): + return service.recordTypePrefix + else: + return "" + class BasicTestCase (DirectoryTestCase): """ Tests a directory implementation with basic auth.