[CalendarServer-changes] [12051] CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav

source_changes at macosforge.org source_changes at macosforge.org
Wed Mar 12 11:16:52 PDT 2014


Revision: 12051
          http://trac.calendarserver.org//changeset/12051
Author:   cdaboo at apple.com
Date:     2013-12-11 14:16:01 -0800 (Wed, 11 Dec 2013)
Log Message:
-----------
Check point. Implemented "external" classes that can proxy various apis through the conduit to get results
from another pod.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/common.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_util.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/util.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/test/test_sql.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/conduit.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/request.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/resource.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_conduit.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_external_home.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_resource.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/test/test_sql.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/test/util.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/upgrade/test/test_migrate.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/icommondatastore.py

Added Paths:
-----------
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql_external.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql_external.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/util.py
    CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql_external.py

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql.py	2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -75,7 +75,7 @@
     InvalidAttachmentOperation, DuplicatePrivateCommentsError
 from txdav.caldav.icalendarstore import QuotaExceeded
 from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
-    CommonObjectResource, ECALENDARTYPE, CommonHomeExternal
+    CommonObjectResource, ECALENDARTYPE
 from txdav.common.datastore.sql_legacy import PostgresLegacyIndexEmulator, \
     PostgresLegacyInboxIndexEmulator
 from txdav.common.datastore.sql_tables import _ATTACHMENTS_MODE_NONE, \
@@ -431,13 +431,6 @@
         "VPOLL": "_default_polls",
     }
 
-    def __init__(self, transaction, ownerUID):
-
-        self._externalClass = CalendarHomeExternal
-        self._childClass = Calendar
-        super(CalendarHome, self).__init__(transaction, ownerUID)
-
-
     @classmethod
     def metadataColumns(cls):
         """
@@ -941,127 +934,6 @@
 
 
 
-class CalendarHomeExternal(CommonHomeExternal, CalendarHome):
-
-    def __init__(self, transaction, ownerUID, resourceID):
-
-        CalendarHome.__init__(self, transaction, ownerUID)
-        CommonHomeExternal.__init__(self, transaction, ownerUID, resourceID)
-
-
-    def hasCalendarResourceUIDSomewhereElse(self, uid, ok_object, mode):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def getCalendarResourcesForUID(self, uid):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def calendarObjectWithDropboxID(self, dropboxID):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def getAllDropboxIDs(self):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def getAllAttachmentNames(self):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def getAllManagedIDs(self):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def createdHome(self):
-        """
-        No children - make this a no-op.
-        """
-        return succeed(None)
-
-
-    def splitCalendars(self):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def ensureDefaultCalendarsExist(self):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def setDefaultCalendar(self, calendar, componentType):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def defaultCalendar(self, componentType, create=True):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def isDefaultCalendar(self, calendar):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def getDefaultAlarm(self, vevent, timed):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def setDefaultAlarm(self, alarm, vevent, timed):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def getAvailability(self):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def setAvailability(self, availability):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-
 class Calendar(CommonHomeChild):
     """
     SQL-based implementation of L{ICalendar}.
@@ -1662,24 +1534,7 @@
         self._cachedComponent = None
         self._cachedCommponentPerUser = {}
 
-    _allColumns = [
-        _objectSchema.RESOURCE_ID,
-        _objectSchema.RESOURCE_NAME,
-        _objectSchema.UID,
-        _objectSchema.MD5,
-        Len(_objectSchema.TEXT),
-        _objectSchema.ATTACHMENTS_MODE,
-        _objectSchema.DROPBOX_ID,
-        _objectSchema.ACCESS,
-        _objectSchema.SCHEDULE_OBJECT,
-        _objectSchema.SCHEDULE_TAG,
-        _objectSchema.SCHEDULE_ETAGS,
-        _objectSchema.PRIVATE_COMMENTS,
-        _objectSchema.CREATED,
-        _objectSchema.MODIFIED
-    ]
 
-
     @classmethod
     @inlineCallbacks
     def _createInternal(cls, parent, name, component, internal_state, options=None, split_details=None):
@@ -1691,7 +1546,8 @@
         if name.startswith("."):
             raise ObjectResourceNameNotAllowedError(name)
 
-        objectResource = cls(parent, name, None, None, options=options)
+        c = cls._externalClass if parent.external() else cls
+        objectResource = c(parent, name, None, None, options=options)
         yield objectResource._setComponentInternal(component, inserting=True, internal_state=internal_state, split_details=split_details)
         yield objectResource._loadPropertyStore(created=True)
 
@@ -1701,27 +1557,51 @@
         returnValue(objectResource)
 
 
-    def _initFromRow(self, row):
+    @classmethod
+    def _allColumns(cls): #@NoSelf
         """
-        Given a select result using the columns from L{_allColumns}, initialize
-        the calendar object resource state.
+        Full set of columns in the object table that need to be loaded to
+        initialize the object resource state.
         """
-        (self._resourceID,
-         self._name,
-         self._uid,
-         self._md5,
-         self._size,
-         self._attachment,
-         self._dropboxID,
-         self._access,
-         self._schedule_object,
-         self._schedule_tag,
-         self._schedule_etags,
-         self._private_comments,
-         self._created,
-         self._modified,) = tuple(row)
+        obj = cls._objectSchema
+        return [
+            obj.RESOURCE_ID,
+            obj.RESOURCE_NAME,
+            obj.UID,
+            obj.MD5,
+            Len(obj.TEXT),
+            obj.ATTACHMENTS_MODE,
+            obj.DROPBOX_ID,
+            obj.ACCESS,
+            obj.SCHEDULE_OBJECT,
+            obj.SCHEDULE_TAG,
+            obj.SCHEDULE_ETAGS,
+            obj.PRIVATE_COMMENTS,
+            obj.CREATED,
+            obj.MODIFIED
+        ]
 
 
+    @classmethod
+    def _rowAttributes(cls): #@NoSelf
+        return (
+            "_resourceID",
+            "_name",
+            "_uid",
+            "_md5",
+            "_size",
+            "_attachment",
+            "_dropboxID",
+            "_access",
+            "_schedule_object",
+            "_schedule_tag",
+            "_schedule_etags",
+            "_private_comments",
+            "_created",
+            "_modified",
+         )
+
+
     @property
     def _calendar(self):
         return self._parentCollection
@@ -4499,4 +4379,10 @@
 
         returnValue(location)
 
+# Hook-up class relationships at the end after they have all been defined
+from txdav.caldav.datastore.sql_external import CalendarHomeExternal, CalendarExternal, CalendarObjectExternal
+CalendarHome._externalClass = CalendarHomeExternal
+CalendarHome._childClass = Calendar
+Calendar._externalClass = CalendarExternal
 Calendar._objectResourceClass = CalendarObject
+CalendarObject._externalClass = CalendarObjectExternal

Added: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql_external.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql_external.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/sql_external.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -0,0 +1,167 @@
+# -*- test-case-name: txdav.caldav.datastore.test.test_sql -*-
+##
+# Copyright (c) 2013 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.
+##
+"""
+SQL backend for CalDAV storage when resources are external.
+"""
+
+from twisted.internet.defer import succeed
+
+from twext.python.log import Logger
+
+from txdav.caldav.datastore.sql import CalendarHome, Calendar, CalendarObject
+from txdav.common.datastore.sql_external import CommonHomeExternal, CommonHomeChildExternal, \
+    CommonObjectResourceExternal
+
+log = Logger()
+
+class CalendarHomeExternal(CommonHomeExternal, CalendarHome):
+    """
+    Wrapper for a CalendarHome that is external and only supports a limited set of operations.
+    """
+
+    def __init__(self, transaction, ownerUID, resourceID):
+
+        CalendarHome.__init__(self, transaction, ownerUID)
+        CommonHomeExternal.__init__(self, transaction, ownerUID, resourceID)
+
+
+    def hasCalendarResourceUIDSomewhereElse(self, uid, ok_object, mode):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def getCalendarResourcesForUID(self, uid):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def calendarObjectWithDropboxID(self, dropboxID):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def getAllDropboxIDs(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def getAllAttachmentNames(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def getAllManagedIDs(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def createdHome(self):
+        """
+        No children - make this a no-op.
+        """
+        return succeed(None)
+
+
+    def splitCalendars(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def ensureDefaultCalendarsExist(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def setDefaultCalendar(self, calendar, componentType):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def defaultCalendar(self, componentType, create=True):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def isDefaultCalendar(self, calendar):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def getDefaultAlarm(self, vevent, timed):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def setDefaultAlarm(self, alarm, vevent, timed):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def getAvailability(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def setAvailability(self, availability):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+
+class CalendarExternal(CommonHomeChildExternal, Calendar):
+    """
+    SQL-based implementation of L{ICalendar}.
+    """
+    pass
+
+
+
+class CalendarObjectExternal(CommonObjectResourceExternal, CalendarObject):
+    """
+    SQL-based implementation of L{ICalendar}.
+    """
+    pass

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/common.py	2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/common.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -1703,7 +1703,7 @@
         L{ICalendarStore.withEachCalendarHomeDo} executes its C{action}
         argument repeatedly with all homes that have been created.
         """
-        additionalUIDs = set('alpha-uid home2 home3 beta-uid'.split())
+        additionalUIDs = set('user01 home2 home3 uid1'.split())
         txn = self.transactionUnderTest()
         for name in additionalUIDs:
             yield txn.calendarHomeWithUID(name, create=True)

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql.py	2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_sql.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -1542,8 +1542,8 @@
 
         # Tests on inbox - resources with properties
         txn = self.transactionUnderTest()
-        yield txn.homeWithUID(ECALENDARTYPE, "byNameTest", create=True)
-        inbox = yield self.calendarUnderTest(txn=txn, name="inbox", home="byNameTest")
+        yield txn.homeWithUID(ECALENDARTYPE, "user01", create=True)
+        inbox = yield self.calendarUnderTest(txn=txn, name="inbox", home="user01")
         caldata = """BEGIN:VCALENDAR
 VERSION:2.0
 CALSCALE:GREGORIAN
@@ -1574,7 +1574,7 @@
         yield _createInboxItem("4.ics", "p4")
         yield self.commit()
 
-        inbox = yield self.calendarUnderTest(name="inbox", home="byNameTest")
+        inbox = yield self.calendarUnderTest(name="inbox", home="user01")
         yield _tests(inbox)
 
         resources = yield inbox.objectResourcesWithNames(("1.ics",))

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_util.py	2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/test_util.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -323,7 +323,12 @@
     @inlineCallbacks
     def setUp(self):
         yield super(HomeMigrationTests, self).setUp()
-        self.theStore = yield buildCalendarStore(self, self.notifierFactory, homes=("conflict1", "conflict2",))
+        self.theStore = yield buildCalendarStore(self, self.notifierFactory, homes=(
+            "conflict1",
+            "conflict2",
+            "empty_home",
+            "non_empty_home",
+        ))
 
 
     def storeUnderTest(self):

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/util.py	2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/caldav/datastore/test/util.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -62,16 +62,12 @@
         calendarUserAddresses,
         cutype="INDIVIDUAL",
         thisServer=True,
+        server=None,
     ):
 
-        super(TestCalendarStoreDirectoryRecord, self).__init__(uid, shortNames, fullName)
-        self.uid = uid
-        self.shortNames = shortNames
-        self.fullName = fullName
-        self.displayName = self.fullName if self.fullName else self.shortNames[0]
+        super(TestCalendarStoreDirectoryRecord, self).__init__(uid, shortNames, fullName, thisServer, server)
         self.calendarUserAddresses = calendarUserAddresses
         self.cutype = cutype
-        self._thisServer = thisServer
 
 
     def canonicalCalendarUserAddress(self):
@@ -90,10 +86,6 @@
         return cua
 
 
-    def thisServer(self):
-        return self._thisServer
-
-
     def calendarsEnabled(self):
         return True
 
@@ -148,12 +140,19 @@
     homes.update((
         "home1",
         "home2",
-        "Home_attachments",
+        "home3",
+        "home_attachments",
         "home_bad",
         "home_defaults",
         "home_no_splits",
+        "home_provision1",
+        "home_provision2",
         "home_splits",
         "home_splits_shared",
+        "uid1",
+        "uid2",
+        "new-home",
+        "xyzzy",
     ))
     for uid in homes:
         directory.addRecord(buildDirectoryRecord(uid))

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql.py	2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -14,7 +14,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 # #
-from txdav.xml import element
 
 """
 SQL backend for CardDAV storage.
@@ -62,6 +61,7 @@
     InvalidUIDError, UIDExistsError, ObjectResourceTooBigError, \
     InvalidObjectResourceError, InvalidComponentForStoreError, \
     AllRetriesFailed, ObjectResourceNameAlreadyExistsError
+from txdav.xml import element
 
 from zope.interface.declarations import implements
 
@@ -86,7 +86,6 @@
 
     def __init__(self, transaction, ownerUID):
 
-        self._childClass = AddressBook
         super(AddressBookHome, self).__init__(transaction, ownerUID)
         self._addressbookPropertyStoreID = None
         self._addressbook = None
@@ -441,9 +440,81 @@
     _objectSchema = schema.ADDRESSBOOK_OBJECT
 
 
-    def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, ownerName=None):
+    @classmethod
+    @inlineCallbacks
+    def _getDBDataIndirect(cls, home, name, resourceID, externalID):
+
+        # Get the bind row data
+        row = None
+
+        # TODO: query cacher
+
+        rows = None
+        ownerHome = None
+
+        # TODO: add queryCacher support
+
+        if rows is None:
+            # No cached copy
+            if name:
+                ownerHome = yield home._txn.addressbookHomeWithUID(name)
+                if ownerHome is None:
+                    returnValue(None)
+                resourceID = ownerHome.addressbook()._resourceID
+            rows = yield AddressBookObject._bindForHomeIDAndAddressBookID.on(
+                home._txn, homeID=home._resourceID, addressbookID=resourceID
+            )
+
+        if not rows:
+            returnValue(None)
+
+        groupID = None
+        overallBindStatus = _BIND_STATUS_INVITED
+        minBindRevision = None
+        for row in rows:
+            bindMode, homeID, resourceGroupID, externalID, name, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount] #@UnusedVariable
+            if groupID is None:
+                groupID = resourceGroupID
+            minBindRevision = min(minBindRevision, bindRevision) if minBindRevision is not None else bindRevision
+            if bindStatus == _BIND_STATUS_ACCEPTED:
+                overallBindStatus = _BIND_STATUS_ACCEPTED
+
+        if ownerHome is None:
+            ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, groupID)
+            ownerHome = yield home.ownerHomeWithChildID(ownerAddressBookID)
+
+        bindData = row[:cls.bindColumnCount]
+        additionalBindData = row[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
+
+        # Adjust for aggregate values
+        bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_ID)] = resourceID
+        bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_NAME)] = ownerHome.uid()
+        bindData[cls.bindColumns().index(cls._bindSchema.BIND_MODE)] = _BIND_MODE_INDIRECT
+        bindData[cls.bindColumns().index(cls._bindSchema.BIND_STATUS)] = overallBindStatus
+        bindData[cls.bindColumns().index(cls._bindSchema.BIND_REVISION)] = minBindRevision
+        bindData[cls.bindColumns().index(cls._bindSchema.MESSAGE)] = ""
+
+        # Get the matching metadata data
+        metadataData = None
+        queryCacher = home._txn._queryCacher
+        if queryCacher:
+            # Retrieve from cache
+            cacheKey = queryCacher.keyForHomeChildMetaData(resourceID)
+            metadataData = yield queryCacher.get(cacheKey)
+
+        if metadataData is None:
+            # No cached copy
+            metadataData = (yield cls._metadataByIDQuery.on(home._txn, resourceID=resourceID))[0]
+            if queryCacher:
+                # Cache the results
+                yield queryCacher.setAfterCommit(home._txn, cacheKey, metadataData)
+
+        returnValue((bindData, additionalBindData, metadataData, ownerHome,))
+
+
+    def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, ownerName=None, externalID=None):
         ownerName = ownerHome.addressbook().name() if ownerHome else None
-        super(AddressBook, self).__init__(home, name, resourceID, mode, status, revision=revision, message=message, ownerHome=ownerHome, ownerName=ownerName)
+        super(AddressBook, self).__init__(home, name, resourceID, mode, status, revision=revision, message=message, ownerHome=ownerHome, ownerName=ownerName, externalID=externalID)
         self._index = PostgresLegacyABIndexEmulator(self)
 
 
@@ -505,7 +576,7 @@
 
 
     @classmethod
-    def create(cls, home, name):
+    def create(cls, home, name, externalID=None):
         if name == home.addressbook().name():
             # raise HomeChildNameAlreadyExistsError
             pass
@@ -767,32 +838,17 @@
 
             # Create the actual objects merging in properties
             for ownerHome, dataRow in ownerHomeToDataRowMap.iteritems():
-                bindMode, homeID, resourceID, externalID, name, bindStatus, bindRevision, bindMessage = dataRow[:cls.bindColumnCount] #@UnusedVariable
-                additionalBind = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
-                metadata = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
+                bindData = dataRow[:cls.bindColumnCount]
+                additionalBindData = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
+                metadataData = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
+                propstore = propertyStores.get(ownerHome._addressbookPropertyStoreID, None)
 
-                child = cls(
-                    home=home,
-                    name=ownerHome.uid(),
-                    resourceID=ownerHome._resourceID,
-                    mode=bindMode,
-                    status=bindStatus,
-                    revision=bindRevision,
-                    message=bindMessage,
-                    ownerHome=ownerHome,
-                )
+                # Some adjustments for addressbook share model
+                bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_ID)] = ownerHome._resourceID
+                bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_NAME)] = ownerHome.uid()
 
-                for attr, value in zip(cls.additionalBindAttributes(), additionalBind):
-                    setattr(child, attr, value)
-                for attr, value in zip(cls.metadataAttributes(), metadata):
-                    setattr(child, attr, value)
+                child = yield cls.makeClass(home, bindData, additionalBindData, metadataData, propstore, ownerHome)
                 child._syncTokenRevision = revisions[child._resourceID]
-                propstore = propertyStores.get(ownerHome._addressbookPropertyStoreID, None)
-                # We have to re-adjust the property store object to account for possible shared
-                # collections as previously we loaded them all as if they were owned
-                if propstore:
-                    propstore._setDefaultUserUID(ownerHome.uid())
-                yield child._loadPropertyStore(propstore)
                 results.append(child)
 
         returnValue(results)
@@ -855,7 +911,7 @@
 
     @classmethod
     @inlineCallbacks
-    def _indirectObjectWithNameOrID(cls, home, name=None, resourceID=None, accepted=True):
+    def _indirectObjectWithNameOrID(cls, home, name=None, resourceID=None, externalID=None, accepted=True):
         # replaces objectWithName()
         """
         Synthesize and indirect child for matching name or id based on whether shared groups exist.
@@ -867,56 +923,17 @@
         @return: an L{CommonHomeChild} or C{None} if no such child
             exists.
         """
-        rows = None
-        ownerHome = None
 
-        # TODO: add queryCacher support
-
-        if rows is None:
-            # No cached copy
-            if name:
-                ownerHome = yield home._txn.addressbookHomeWithUID(name)
-                if ownerHome is None:
-                    returnValue(None)
-                resourceID = ownerHome.addressbook()._resourceID
-            rows = yield AddressBookObject._bindForHomeIDAndAddressBookID.on(
-                home._txn, homeID=home._resourceID, addressbookID=resourceID
-            )
-
-        if not rows:
+        dbData = yield cls._getDBDataIndirect(home, name, resourceID, externalID)
+        if dbData is None:
             returnValue(None)
+        bindData, additionalBindData, metadataData, ownerHome = dbData
 
-        groupID = None
-        overallBindStatus = _BIND_STATUS_INVITED
-        minBindRevision = None
-        for row in rows:
-            bindMode, homeID, resourceGroupID, externalID, name, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount] #@UnusedVariable
-            if groupID is None:
-                groupID = resourceGroupID
-            minBindRevision = min(minBindRevision, bindRevision) if minBindRevision is not None else bindRevision
-            if bindStatus == _BIND_STATUS_ACCEPTED:
-                overallBindStatus = _BIND_STATUS_ACCEPTED
-
-        if accepted is not None and (overallBindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
+        bindStatus = bindData[cls.bindColumns().index(cls._bindSchema.BIND_STATUS)]
+        if accepted is not None and (bindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
             returnValue(None)
-        additionalBind = row[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
 
-        if ownerHome is None:
-            ownerAddressBookID = yield AddressBookObject.ownerAddressBookIDFromGroupID(home._txn, groupID)
-            ownerHome = yield home.ownerHomeWithChildID(ownerAddressBookID)
-
-        child = cls(
-            home=home,
-            name=ownerHome.uid(),
-            resourceID=resourceID,
-            mode=_BIND_MODE_INDIRECT,
-            status=overallBindStatus,
-            revision=minBindRevision,
-            message="",
-            ownerHome=ownerHome,
-            ownerName=ownerHome.uid()
-        )
-        yield child.initFromStore(additionalBind)
+        child = yield cls.makeClass(home, bindData, additionalBindData, metadataData, None, ownerHome)
         returnValue(child)
 
 
@@ -1142,7 +1159,7 @@
 
 
     @inlineCallbacks
-    def shareWith(self, shareeHome, mode, status=None, summary=None):
+    def shareWith(self, shareeHome, mode, status=None, summary=None, shareName=None):
         """
         Share this (owned) L{AddressBookObject} with another home.
 
@@ -1170,11 +1187,12 @@
 
         @inlineCallbacks
         def doInsert(subt):
-            newName = self.newShareName()
+            newName = shareName if shareName is not None else self.newShareName()
             yield self._bindInsertQuery.on(
                 subt,
                 homeID=shareeHome._resourceID,
                 resourceID=self._resourceID,
+                externalID=None,
                 name=newName,
                 mode=mode,
                 bindStatus=status,
@@ -1210,14 +1228,14 @@
 
 
     @inlineCallbacks
-    def createShare(self, shareeUID, mode, summary=None):
+    def createShare(self, shareeUID, mode, summary=None, shareName=None):
         """
         Create a new shared resource. If the mode is direct, the share is created in accepted state,
         otherwise the share is created in invited state.
         """
 
         if self._kind == _ABO_KIND_GROUP:
-            shareeView = yield super(AddressBookObjectSharingMixIn, self).createShare(shareeUID, mode, summary)
+            shareeView = yield super(AddressBookObjectSharingMixIn, self).createShare(shareeUID, mode, summary, shareName)
             returnValue(shareeView)
         else:
             returnValue(None)
@@ -1430,6 +1448,130 @@
     # _homeChildMetaDataSchema = schema.ADDRESSBOOK_OBJECT
 
 
+    @classmethod
+    @inlineCallbacks
+    def makeClass(cls, parent, objectData, groupBindData=None, propstore=None):
+        """
+        Given the various database rows, build the actual class.
+
+        @param parent: the parent collection object
+        @type parent: L{AddressBook}
+        @param objectData: the standard set of object columns
+        @type objectData: C{list}
+        @param groupBindData: additional group bind data
+        @type groupBindData: C{list}
+        @param propstore: a property store to use, or C{None} to load it automatically
+        @type propstore: L{PropertyStore}
+
+        @return: the constructed child class
+        @rtype: L{CommonHomeChild}
+        """
+
+        c = cls._externalClass if parent.external() else cls
+        child = c(
+            parent,
+            objectData[cls._allColumns().index(cls._objectSchema.RESOURCE_NAME)],
+            objectData[cls._allColumns().index(cls._objectSchema.UID)],
+        )
+
+        for attr, value in zip(child._rowAttributes(), objectData):
+            setattr(child, attr, value)
+
+        yield child._loadPropertyStore(propstore)
+
+        if groupBindData:
+            bindMode, homeID, resourceID, externalID, bindName, bindStatus, bindRevision, bindMessage = groupBindData[:AddressBookObject.bindColumnCount] #@UnusedVariable
+            child._bindMode = bindMode
+            child._bindStatus = bindStatus
+            child._bindMessage = bindMessage
+            child._bindName = bindName
+        else:
+            invites = yield child.sharingInvites()
+            if len(invites):
+                child._bindMessage = "shared"
+
+        returnValue(child)
+
+
+    @classmethod
+    @inlineCallbacks
+    def _getDBData(cls, parent, name, uid, resourceID):
+        """
+        Given a set of identifying information, load the data rows for the object. Only one of
+        L{name}, L{uid} or L{resourceID} is specified - others are C{None}.
+
+        @param parent: the parent collection object
+        @type parent: L{AddressBook}
+        @param name: the resource name
+        @type name: C{str}
+        @param uid: the UID of the data
+        @type uid: C{str}
+        @param resourceID: the resource ID
+        @type resourceID: C{int}
+        """
+
+        row = None
+        groupBindRow = None
+
+        if parent.owned() or parent.fullyShared():  # owned or fully shared
+            row = yield super(AddressBookObject, cls)._getDBData(parent, name, uid, resourceID)
+
+            # Might be special group
+            if row is None and parent.fullyShared():
+                if name:
+                    if name == parent._groupForSharedAddressBookName():
+                        row = parent._groupForSharedAddressBookRow()
+                elif uid:
+                    if uid == (yield parent._groupForSharedAddressBookUID()):
+                        row = parent._groupForSharedAddressBookRow()
+                elif resourceID:
+                    if resourceID == parent.id():
+                        rows = parent._groupForSharedAddressBookRow()
+
+        else:
+            acceptedGroupIDs = yield parent.acceptedGroupIDs()
+            allowedObjectIDs = yield parent.expandGroupIDs(parent._txn, acceptedGroupIDs)
+            rows = None
+            if name:
+                if allowedObjectIDs:
+                    rows = (yield cls._allColumnsWithResourceIDsAndName(allowedObjectIDs).on(
+                        parent._txn,
+                        name=name,
+                        resourceIDs=allowedObjectIDs,
+                    ))
+            elif uid:
+                if allowedObjectIDs:
+                    rows = (yield cls._allColumnsWithResourceIDsAndUID(allowedObjectIDs).on(
+                        parent._txn,
+                        uid=uid,
+                        resourceIDs=allowedObjectIDs,
+                    ))
+            elif resourceID:
+                if resourceID not in allowedObjectIDs:
+                    # allow invited groups
+                    allowedObjectIDs = yield parent.unacceptedGroupIDs()
+                if resourceID in allowedObjectIDs:
+                    rows = (yield cls._allColumnsWithResourceID.on(
+                        parent._txn,
+                        resourceID=resourceID,
+                    ))
+            if rows:
+                row = rows[0]
+
+        if row is not None:
+            if row[cls._allColumns().index(cls._objectSchema.KIND)] == _ABO_KIND_GROUP:
+
+                resourceID = row[cls._allColumns().index(cls._objectSchema.RESOURCE_ID)]
+                groupBindRows = yield AddressBookObject._bindForResourceIDAndHomeID.on(
+                    parent._txn, resourceID=resourceID, homeID=parent._home._resourceID
+                )
+
+                if groupBindRows:
+                    groupBindRow = groupBindRows[0]
+
+        returnValue((row, groupBindRow,))
+
+
     def __init__(self, addressbook, name, uid, resourceID=None, options=None):
 
         self._kind = None
@@ -1443,6 +1585,7 @@
         self._bindMessage = None
         self._bindName = None
         super(AddressBookObject, self).__init__(addressbook, name, uid, resourceID, options)
+        self._externalID = None
         self._options = {} if options is None else options
 
 
@@ -1467,6 +1610,15 @@
         return self._resourceID == self.addressbook()._resourceID
 
 
+    def external(self):
+        """
+        Is this an external object.
+
+        @return: a string.
+        """
+        return self.addressbook().external()
+
+
     @classmethod
     def _deleteMembersWithMemberIDAndGroupIDsQuery(cls, memberID, groupIDs):
         aboMembers = schema.ABO_MEMBERS
@@ -1553,7 +1705,7 @@
         """
         obj = cls._objectSchema
         return Select(
-            cls._allColumns, From=obj,
+            cls._allColumns(), From=obj,
             Where=(column == Parameter(paramName)).And(
                 obj.RESOURCE_ID.In(Parameter("resourceIDs", len(resourceIDs)))),
         )
@@ -1573,7 +1725,7 @@
     def _allColumnsWithResourceID(cls): #@NoSelf
         obj = cls._objectSchema
         return Select(
-            cls._allColumns, From=obj,
+            cls._allColumns(), From=obj,
             Where=obj.RESOURCE_ID == Parameter("resourceID"),)
 
 
@@ -1609,92 +1761,20 @@
         returnValue(None)
 
 
+    @classmethod
     @inlineCallbacks
-    def initFromStore(self):
-        """
-        Initialise this object from the store. We read in and cache all the
-        extra metadata from the DB to avoid having to do DB queries for those
-        individually later. Either the name or uid is present, so we have to
-        tweak the query accordingly.
+    def _objectWithNameOrID(cls, parent, name, uid, resourceID):
 
-        @return: L{self} if object exists in the DB, else C{None}
-        """
-        abo = None
-        if self.owned() or self.addressbook().fullyShared():  # owned or fully shared
-            abo = yield super(AddressBookObject, self).initFromStore()
+        row, groupBindRow = yield cls._getDBData(parent, name, uid, resourceID)
 
-            # Might be special group
-            if abo is None and self.addressbook().fullyShared():
-                rows = None
-                if self._name:
-                    if self._name == self.addressbook()._groupForSharedAddressBookName():
-                        rows = [self.addressbook()._groupForSharedAddressBookRow()]
-                elif self._uid:
-                    if self._uid == (yield self.addressbook()._groupForSharedAddressBookUID()):
-                        rows = [self.addressbook()._groupForSharedAddressBookRow()]
-                elif self._resourceID:
-                    if self.isGroupForSharedAddressBook():
-                        rows = [self.addressbook()._groupForSharedAddressBookRow()]
-
-                if rows:
-                    self._initFromRow(tuple(rows[0]))
-                    yield self._loadPropertyStore()
-                    abo = self
-
+        if row:
+            child = yield cls.makeClass(parent, row, groupBindRow)
+            returnValue(child)
         else:
-            acceptedGroupIDs = yield self.addressbook().acceptedGroupIDs()
-            allowedObjectIDs = yield self.addressbook().expandGroupIDs(self._txn, acceptedGroupIDs)
-            rows = None
-            if self._name:
-                if allowedObjectIDs:
-                    rows = (yield self._allColumnsWithResourceIDsAndName(allowedObjectIDs).on(
-                        self._txn, name=self._name,
-                        resourceIDs=allowedObjectIDs,
-                    ))
-            elif self._uid:
-                if allowedObjectIDs:
-                    rows = (yield self._allColumnsWithResourceIDsAndUID(allowedObjectIDs).on(
-                        self._txn, uid=self._uid,
-                        resourceIDs=allowedObjectIDs,
-                    ))
-            elif self._resourceID:
-                if self._resourceID not in allowedObjectIDs:
-                    # allow invited groups
-                    allowedObjectIDs = yield self.addressbook().unacceptedGroupIDs()
-                if self._resourceID in allowedObjectIDs:
-                    rows = (yield self._allColumnsWithResourceID.on(
-                        self._txn, resourceID=self._resourceID,
-                    ))
-            if rows:
-                self._initFromRow(tuple(rows[0]))
-                yield self._loadPropertyStore()
-                abo = self
-
-        if abo is not None:
-            if self._kind == _ABO_KIND_GROUP:
-
-                groupBindRows = yield AddressBookObject._bindForResourceIDAndHomeID.on(
-                    self._txn, resourceID=self._resourceID, homeID=self._home._resourceID
-                )
-
-                if groupBindRows:
-                    groupBindRow = groupBindRows[0]
-                    bindMode, homeID, resourceID, externalID, bindName, bindStatus, bindRevision, bindMessage = groupBindRow[:AddressBookObject.bindColumnCount] #@UnusedVariable
-                    self._bindMode = bindMode
-                    self._bindStatus = bindStatus
-                    self._bindMessage = bindMessage
-                    self._bindName = bindName
-                else:
-                    invites = yield self.sharingInvites()
-                    if len(invites):
-                        self._bindMessage = "shared"
-
-            returnValue(self)
-        else:
             returnValue(None)
 
 
-    @classproperty
+    @classmethod
     def _allColumns(cls): #@NoSelf
         """
         Full set of columns in the object table that need to be loaded to
@@ -1710,24 +1790,23 @@
             obj.MD5,
             Len(obj.TEXT),
             obj.CREATED,
-            obj.MODIFIED,
+            obj.MODIFIED
         ]
 
 
-    def _initFromRow(self, row):
-        """
-        Given a select result using the columns from L{_allColumns}, initialize
-        the object resource state.
-        """
-        (self._ownerAddressBookResourceID,
-         self._resourceID,
-         self._name,
-         self._uid,
-         self._kind,
-         self._md5,
-         self._size,
-         self._created,
-         self._modified,) = tuple(row)
+    @classmethod
+    def _rowAttributes(cls): #@NoSelf
+        return (
+            "_ownerAddressBookResourceID",
+            "_resourceID",
+            "_name",
+            "_uid",
+            "_kind",
+            "_md5",
+            "_size",
+            "_created",
+            "_modified",
+         )
 
 
     @classmethod
@@ -1750,7 +1829,7 @@
         else:
             acceptedGroupIDs = yield addressbook.acceptedGroupIDs()
             allowedObjectIDs = yield addressbook.expandGroupIDs(addressbook._txn, acceptedGroupIDs)
-            rows = yield cls._columnsWithResourceIDsQuery(cls._allColumns, allowedObjectIDs).on(
+            rows = yield cls._columnsWithResourceIDsQuery(cls._allColumns(), allowedObjectIDs).on(
                 addressbook._txn, resourceIDs=allowedObjectIDs
             )
         returnValue(rows)
@@ -1759,7 +1838,7 @@
     @classmethod
     def _allColumnsWithResourceIDsAndNamesQuery(cls, resourceIDs, names):
         obj = cls._objectSchema
-        return Select(cls._allColumns, From=obj,
+        return Select(cls._allColumns(), From=obj,
                       Where=(obj.RESOURCE_ID.In(Parameter("resourceIDs", len(resourceIDs))).And(
                           obj.RESOURCE_NAME.In(Parameter("names", len(names))))),)
 
@@ -2336,4 +2415,10 @@
         )
 
 
+# Hook-up class relationships at the end after they have all been defined
+from txdav.carddav.datastore.sql_external import AddressBookHomeExternal, AddressBookExternal, AddressBookObjectExternal
+AddressBookHome._externalClass = AddressBookHomeExternal
+AddressBookHome._childClass = AddressBook
+AddressBook._externalClass = AddressBookExternal
 AddressBook._objectResourceClass = AddressBookObject
+AddressBookObject._externalClass = AddressBookObjectExternal

Added: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql_external.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql_external.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/sql_external.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -0,0 +1,81 @@
+# -*- test-case-name: txdav.caldav.datastore.test.test_sql -*-
+##
+# Copyright (c) 2013 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.
+##
+"""
+SQL backend for CardDAV storage when resources are external.
+"""
+
+from twisted.internet.defer import succeed
+
+from twext.python.log import Logger
+
+from txdav.carddav.datastore.sql import AddressBookHome, AddressBook, \
+    AddressBookObject
+from txdav.common.datastore.sql_external import CommonHomeExternal, CommonHomeChildExternal, \
+    CommonObjectResourceExternal
+
+log = Logger()
+
+class AddressBookHomeExternal(CommonHomeExternal, AddressBookHome):
+
+    def __init__(self, transaction, ownerUID, resourceID):
+
+        AddressBookHome.__init__(self, transaction, ownerUID)
+        CommonHomeExternal.__init__(self, transaction, ownerUID, resourceID)
+
+
+    def hasAddressBookResourceUIDSomewhereElse(self, uid, ok_object, mode):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def getAddressBookResourcesForUID(self, uid):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def createdHome(self):
+        """
+        No children - make this a no-op.
+        """
+        return succeed(None)
+
+
+    def addressbook(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+
+class AddressBookExternal(CommonHomeChildExternal, AddressBook):
+    """
+    SQL-based implementation of L{IAddressBook}.
+    """
+    pass
+
+
+
+class AddressBookObjectExternal(CommonObjectResourceExternal, AddressBookObject):
+    """
+    SQL-based implementation of L{ICalendar}.
+    """
+    pass

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/test/test_sql.py	2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/carddav/datastore/test/test_sql.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -41,7 +41,7 @@
 from txdav.common.icommondatastore import NoSuchObjectResourceError
 from txdav.common.datastore.sql import EADDRESSBOOKTYPE, CommonObjectResource
 from txdav.common.datastore.sql_tables import  _ABO_KIND_PERSON, _ABO_KIND_GROUP, schema
-from txdav.common.datastore.test.util import buildStore
+from txdav.common.datastore.test.util import buildStore, cleanStore
 
 from txdav.xml.rfc2518 import GETContentLanguage, ResourceType
 
@@ -55,7 +55,22 @@
     @inlineCallbacks
     def setUp(self):
         yield super(AddressBookSQLStorageTests, self).setUp()
-        self._sqlStore = yield buildStore(self, self.notifierFactory)
+        self._sqlStore = yield buildStore(
+            self,
+            self.notifierFactory,
+            homes=(
+                "home1",
+                "home2",
+                "home3",
+                "home_bad",
+                "home_empty",
+                "homeNew",
+                "new-home",
+                "uid1",
+                "uid2",
+                "xyzzy",
+            )
+        )
         yield self.populate()
 
 
@@ -288,7 +303,7 @@
         Test that two concurrent attempts to PUT different address book object resources to the
         same address book home does not cause a deadlock.
         """
-        addressbookStore = yield buildStore(self, self.notifierFactory)
+        addressbookStore = self._sqlStore
 
         # Provision the home and addressbook now
         txn = addressbookStore.newTransaction()
@@ -394,7 +409,7 @@
         """
         Test that kind property UID is stored correctly in database
         """
-        addressbookStore = yield buildStore(self, self.notifierFactory)
+        addressbookStore = self._sqlStore
 
         # Provision the home and addressbook, one user and one group
         txn = addressbookStore.newTransaction()
@@ -440,7 +455,7 @@
         """
         Test that kind property vCard is stored correctly in database
         """
-        addressbookStore = yield buildStore(self, self.notifierFactory)
+        addressbookStore = self._sqlStore
 
         # Provision the home and addressbook, one user and one group
         txn = addressbookStore.newTransaction()
@@ -531,7 +546,8 @@
         """
         Test that kind property vCard is stored correctly in database
         """
-        addressbookStore = yield buildStore(self, self.notifierFactory)
+        addressbookStore = self._sqlStore
+        cleanStore(self, addressbookStore)
 
         # Provision the home and addressbook, one user and one group
         txn = addressbookStore.newTransaction()

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/conduit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/conduit.py	2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/conduit.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -14,6 +14,8 @@
 # limitations under the License.
 ##
 
+from twext.python.log import Logger
+
 from twisted.internet.defer import inlineCallbacks, returnValue
 
 from txdav.common.datastore.podding.request import ConduitRequest
@@ -25,21 +27,17 @@
     "PoddingConduitResource",
 ]
 
-class BadMessageError(Exception):
-    pass
+log = Logger()
 
 
-
-class InvalidCrossPodRequestError(Exception):
+class FailedCrossPodRequestError(RuntimeError):
+    """
+    Request returned an error.
+    """
     pass
 
 
 
-class FailedCrossPodRequestError(Exception):
-    pass
-
-
-
 class PoddingConduit(object):
     """
     This class is the API/RPC bridge between cross-pod requests and the store.
@@ -51,7 +49,7 @@
     the other keys are arguments to that call.
 
     Each response C{dict} has a "result" key that indicates the call result, and other
-    optional keys for any parameters returned by the call.
+    optional keys for any values returned by the call.
 
     The conduit provides two methods for each action: one for the sending side and one for
     the receiving side, called "send_{action}" and "recv_{action}", respectively, where
@@ -63,10 +61,14 @@
     The "recv_{action}" calls take a single C{dict} argument that is the deserialized JSON
     data from the incoming request. The return value is a C{dict} with the result.
 
+    Some simple forms of send_/recv_ methods can be auto-generated to simplify coding.
+
     Right now this conduit is used for cross-pod sharing operations. In the future we will
     likely use it for cross-pod migration.
     """
 
+    conduitRequestClass = ConduitRequest
+
     def __init__(self, store):
         """
         @param store: the L{CommonDataStore} in use.
@@ -91,18 +93,73 @@
         if source is None:
             raise DirectoryRecordNotFoundError("Cross-pod source: {}".format(source_guid))
         if not source.thisServer():
-            raise InvalidCrossPodRequestError("Cross-pod source not on this server: {}".format(source_guid))
+            raise FailedCrossPodRequestError("Cross-pod source not on this server: {}".format(source_guid))
 
         destination = self.store.directoryService().recordWithUID(destination_guid)
         if destination is None:
             raise DirectoryRecordNotFoundError("Cross-pod destination: {}".format(destination_guid))
         if destination.thisServer():
-            raise InvalidCrossPodRequestError("Cross-pod destination on this server: {}".format(destination_guid))
+            raise FailedCrossPodRequestError("Cross-pod destination on this server: {}".format(destination_guid))
 
         return (source, destination,)
 
 
     @inlineCallbacks
+    def sendRequest(self, txn, recipient, data):
+
+        request = self.conduitRequestClass(recipient.server(), data)
+        try:
+            response = (yield request.doRequest(txn))
+        except Exception as e:
+            raise FailedCrossPodRequestError("Failed cross-pod request: {}".format(e))
+        returnValue(response)
+
+
+    @inlineCallbacks
+    def processRequest(self, data):
+        """
+        Process the request.
+
+        @param data: the JSON data to process
+        @type data: C{dict}
+        """
+        # Must have a dict with an "action" key
+        try:
+            action = data["action"]
+        except (KeyError, TypeError) as e:
+            log.error("JSON data must have an object as its root with an 'action' attribute: {ex}\n{json}", ex=e, json=data)
+            raise FailedCrossPodRequestError("JSON data must have an object as its root with an 'action' attribute: {}\n{}".format(e, data,))
+
+        if action == "ping":
+            result = {"result": "ok"}
+            returnValue(result)
+
+        method = "recv_{}".format(action)
+        if not hasattr(self, method):
+            log.error("Unsupported action: {action}", action=action)
+            raise FailedCrossPodRequestError("Unsupported action: {}".format(action))
+
+        # Need a transaction to work with
+        txn = self.store.newTransaction(repr("Conduit request"))
+
+        # Do the actual request processing
+        try:
+            result = (yield getattr(self, method)(txn, data))
+        except Exception as e:
+            yield txn.abort()
+            log.error("Failed action: {action}, {ex}", action=action, ex=e)
+            raise FailedCrossPodRequestError("Failed action: {}, {}".format(action, e))
+
+        yield txn.commit()
+
+        returnValue(result)
+
+
+    #
+    # Invite related apis
+    #
+
+    @inlineCallbacks
     def send_shareinvite(self, txn, homeType, ownerUID, ownerID, ownerName, shareeUID, shareUID, bindMode, summary, supported_components):
         """
         Send a sharing invite cross-pod message.
@@ -127,7 +184,7 @@
         @type supported_components: C{str}
         """
 
-        _ignore_owner, sharee = self.validRequst(ownerUID, shareeUID)
+        _ignore_sender, recipient = self.validRequst(ownerUID, shareeUID)
 
         action = {
             "action": "shareinvite",
@@ -143,10 +200,8 @@
         if supported_components is not None:
             action["supported-components"] = supported_components
 
-        request = ConduitRequest(sharee.server(), action)
-        response = (yield request.doRequest(txn))
-        if response["result"] != "ok":
-            raise FailedCrossPodRequestError(response["description"])
+        result = yield self.sendRequest(txn, recipient, action)
+        returnValue(result)
 
 
     @inlineCallbacks
@@ -159,15 +214,12 @@
         """
 
         if message["action"] != "shareinvite":
-            raise BadMessageError("Wrong action '{}' for recv_shareinvite".format(message["action"]))
+            raise FailedCrossPodRequestError("Wrong action '{}' for recv_shareinvite".format(message["action"]))
 
         # Create a share
         shareeHome = yield txn.homeWithUID(message["type"], message["sharee"], create=True)
         if shareeHome is None or shareeHome.external():
-            returnValue({
-                "result": "bad",
-                "description": "Invalid sharee UID specified",
-            })
+            raise FailedCrossPodRequestError("Invalid sharee UID specified")
 
         try:
             yield shareeHome.processExternalInvite(
@@ -180,14 +232,10 @@
                 supported_components=message.get("supported-components")
             )
         except ExternalShareFailed as e:
-            returnValue({
-                "result": "bad",
-                "description": str(e),
-            })
+            raise FailedCrossPodRequestError(str(e))
 
         returnValue({
             "result": "ok",
-            "description": "Success"
         })
 
 
@@ -208,7 +256,7 @@
         @type shareUID: C{str}
         """
 
-        _ignore_owner, sharee = self.validRequst(ownerUID, shareeUID)
+        _ignore_sender, recipient = self.validRequst(ownerUID, shareeUID)
 
         action = {
             "action": "shareuninvite",
@@ -219,10 +267,8 @@
             "share_id": shareUID,
         }
 
-        request = ConduitRequest(sharee.server(), action)
-        response = (yield request.doRequest(txn))
-        if response["result"] != "ok":
-            raise FailedCrossPodRequestError(response["description"])
+        result = yield self.sendRequest(txn, recipient, action)
+        returnValue(result)
 
 
     @inlineCallbacks
@@ -235,15 +281,12 @@
         """
 
         if message["action"] != "shareuninvite":
-            raise BadMessageError("Wrong action '{}' for recv_shareuninvite".format(message["action"]))
+            raise FailedCrossPodRequestError("Wrong action '{}' for recv_shareuninvite".format(message["action"]))
 
         # Create a share
         shareeHome = yield txn.homeWithUID(message["type"], message["sharee"], create=True)
         if shareeHome is None or shareeHome.external():
-            returnValue({
-                "result": "bad",
-                "description": "Invalid sharee UID specified",
-            })
+            FailedCrossPodRequestError("Invalid sharee UID specified")
 
         try:
             yield shareeHome.processExternalUninvite(
@@ -252,14 +295,10 @@
                 message["share_id"],
             )
         except ExternalShareFailed as e:
-            returnValue({
-                "result": "bad",
-                "description": str(e),
-            })
+            FailedCrossPodRequestError(str(e))
 
         returnValue({
             "result": "ok",
-            "description": "Success"
         })
 
 
@@ -272,9 +311,9 @@
         @type homeType: C{int}
         @param ownerUID: GUID of the sharer.
         @type ownerUID: C{str}
-        @param shareeUID: GUID of the sharee
+        @param shareeUID: GUID of the recipient
         @type shareeUID: C{str}
-        @param shareUID: Resource/invite ID for sharee
+        @param shareUID: Resource/invite ID for recipient
         @type shareUID: C{str}
         @param bindStatus: bind mode for the share
         @type bindStatus: C{str}
@@ -282,7 +321,7 @@
         @type summary: C{str}
         """
 
-        _ignore_owner, sharee = self.validRequst(shareeUID, ownerUID)
+        _ignore_sender, recipient = self.validRequst(shareeUID, ownerUID)
 
         action = {
             "action": "sharereply",
@@ -295,10 +334,8 @@
         if summary is not None:
             action["summary"] = summary
 
-        request = ConduitRequest(sharee.server(), action)
-        response = (yield request.doRequest(txn))
-        if response["result"] != "ok":
-            raise FailedCrossPodRequestError(response["description"])
+        result = yield self.sendRequest(txn, recipient, action)
+        returnValue(result)
 
 
     @inlineCallbacks
@@ -311,15 +348,12 @@
         """
 
         if message["action"] != "sharereply":
-            raise BadMessageError("Wrong action '{}' for recv_sharereply".format(message["action"]))
+            raise FailedCrossPodRequestError("Wrong action '{}' for recv_sharereply".format(message["action"]))
 
         # Create a share
         ownerHome = yield txn.homeWithUID(message["type"], message["owner"])
         if ownerHome is None or ownerHome.external():
-            returnValue({
-                "result": "bad",
-                "description": "Invalid owner UID specified",
-            })
+            FailedCrossPodRequestError("Invalid owner UID specified")
 
         try:
             yield ownerHome.processExternalReply(
@@ -330,12 +364,126 @@
                 summary=message.get("summary")
             )
         except ExternalShareFailed as e:
-            returnValue({
-                "result": "bad",
-                "description": str(e),
-            })
+            FailedCrossPodRequestError(str(e))
 
         returnValue({
             "result": "ok",
-            "description": "Success"
         })
+
+
+    #
+    # Sharer data access related apis
+    #
+
+    def _send(self, action, shareeView):
+        """
+        Base behavior for an operation on a sharee resource.
+
+        @param shareeView: sharee resource being operated on.
+        @type shareeView: L{CommonHomeChildExternal}
+        """
+
+        homeType = shareeView.ownerHome()._homeType
+        ownerUID = shareeView.ownerHome().uid()
+        ownerID = shareeView.external_id()
+        shareeUID = shareeView.viewerHome().uid()
+
+        _ignore_sender, recipient = self.validRequst(shareeUID, ownerUID)
+
+        result = {
+            "action": action,
+            "type": homeType,
+            "owner": ownerUID,
+            "owner_id": ownerID,
+            "sharee": shareeUID,
+        }
+        return result, recipient
+
+
+    @inlineCallbacks
+    def _recv(self, txn, message, expected_action):
+        """
+        Base behavior for sharer data access.
+
+        @param message: message arguments
+        @type message: C{dict}
+        """
+
+        if message["action"] != expected_action:
+            raise FailedCrossPodRequestError("Wrong action '{}' for recv_{}".format(message["action"], expected_action))
+
+        # Create a share
+        ownerHome = yield txn.homeWithUID(message["type"], message["owner"], create=True)
+        if ownerHome is None or ownerHome.external():
+            FailedCrossPodRequestError("Invalid owner UID specified")
+
+        ownerHomeChild = yield ownerHome.childWithID(message["owner_id"])
+        if ownerHomeChild is None:
+            FailedCrossPodRequestError("Invalid owner shared resource specified")
+
+        returnValue((ownerHome, ownerHomeChild))
+
+
+    #
+    # Simple calls are ones where there is no argument and a single return value. We can simplify
+    # code generation for these by dynamically generating the appropriate class methods.
+    #
+
+    @inlineCallbacks
+    def _simple_send(self, actionName, shareeView, args=None, kwargs=None):
+        """
+        A simple send operation that returns a value.
+
+        @param actionName: name of the action.
+        @type actionName: C{str}
+        @param shareeView: sharee resource being operated on.
+        @type shareeView: L{CommonHomeChildExternal}
+        @param args: list of optional arguments.
+        @type args: C{list}
+        @param kwargs: optional keyword arguments.
+        @type kwargs: C{dict}
+        """
+
+        action, recipient = self._send(actionName, shareeView)
+        if args is not None:
+            action["arguments"] = args
+        if kwargs is not None:
+            action["keywords"] = kwargs
+        result = yield self.sendRequest(shareeView._txn, recipient, action)
+        returnValue(result["value"])
+
+
+    @inlineCallbacks
+    def _simple_recv(self, txn, actionName, message, method):
+        """
+        A simple recv operation that returns a value. We also look for an optional set of arguments/keywords
+        and include those only if present.
+
+        @param actionName: name of the action.
+        @type actionName: C{str}
+        @param message: message arguments
+        @type message: C{dict}
+        @param method: name of the method to execute on the shared resource to get the result.
+        @type method: C{str}
+        """
+
+        _ignore_ownerHome, ownerHomeChild = yield self._recv(txn, message, actionName)
+        value = yield getattr(ownerHomeChild, method)(*message.get("arguments", ()), **message.get("keywords", {}))
+        returnValue({
+            "result": "ok",
+            "value": value,
+        })
+
+
+    @classmethod
+    def _make_simple_action(cls, action, method):
+        setattr(cls, "send_{}".format(action), lambda self, shareeView, *args, **kwargs: self._simple_send(action, shareeView, args, kwargs))
+        setattr(cls, "recv_{}".format(action), lambda self, txn, message: self._simple_recv(txn, action, message, method))
+
+
+PoddingConduit._make_simple_action("countobjects", "countObjectResources")
+PoddingConduit._make_simple_action("listobjects", "listObjectResources")
+PoddingConduit._make_simple_action("synctoken", "syncToken")
+PoddingConduit._make_simple_action("resourcenamessincerevision", "resourceNamesSinceRevision")
+PoddingConduit._make_simple_action("resourceuidforname", "resourceUIDForName")
+PoddingConduit._make_simple_action("resourcenameforuid", "resourceNameForUID")

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/request.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/request.py	2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/request.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -73,7 +73,7 @@
         except Exception as e:
             # Request failed
             log.error("Could not do cross-pod request : {request} {ex}", request=self, ex=e)
-            raise ValueError("Failed cross-pod request: {}".format(response.code))
+            raise ValueError("Failed cross-pod request: {}".format(e))
 
         returnValue(data)
 

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/resource.py	2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/resource.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -32,6 +32,7 @@
 
 from txdav.xml import element as davxml
 from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers
+from txdav.common.datastore.podding.conduit import FailedCrossPodRequestError
 
 __all__ = [
     "ConduitResource",
@@ -138,36 +139,14 @@
             self.log.error("Invalid JSON data in request: {ex}\n{body}", ex=e, body=body)
             raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid JSON data in request: {}\n{}".format(e, body)))
 
-        # Must have a dict with an "action" key
+        # Get the conduit to process the data
         try:
-            action = j["action"]
-        except (KeyError, TypeError) as e:
-            self.log.error("JSON data must have an object as its root with an 'action' attribute: {ex}\n{json}", ex=e, json=j)
-            raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "JSON data must have an object as its root with an 'action' attribute: {}\n{}".format(e, j,)))
-
-        if action == "ping":
-            result = {"result": "ok"}
-            response = JSONResponse(responsecode.OK, result)
-            returnValue(response)
-
-        method = "recv_{}".format(action)
-        if not hasattr(self.store.conduit, method):
-            self.log.error("Unsupported action: {action}", action=action)
-            raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Unsupported action: {}".format(action)))
-
-        # Need a transaction to work with
-        txn = self.store.newTransaction(repr(request))
-
-        # Do the POST processing treating this as a non-local schedule
-        try:
-            result = (yield getattr(self.store.conduit, method)(txn, j))
+            result = yield self.store.conduit.processRequest(j)
+        except FailedCrossPodRequestError as e:
+            raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
         except Exception as e:
-            yield txn.abort()
-            self.log.error("Failed action: {action}, {ex}", action=action, ex=e)
-            raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "Failed action: {}, {}".format(action, e)))
+            raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, str(e)))
 
-        yield txn.commit()
-
         response = JSONResponse(responsecode.OK, result)
         returnValue(response)
 

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_conduit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_conduit.py	2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_conduit.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -16,17 +16,22 @@
 
 from twext.python.clsprop import classproperty
 import twext.web2.dav.test.util
-from twisted.internet.defer import inlineCallbacks, succeed
+from twisted.internet.defer import inlineCallbacks, succeed, returnValue
 from txdav.caldav.datastore.scheduling.ischedule.localservers import Servers, Server
 from txdav.caldav.datastore.test.util import buildCalendarStore, \
     TestCalendarStoreDirectoryRecord
 from txdav.common.datastore.podding.resource import ConduitResource
 from txdav.common.datastore.test.util import populateCalendarsFrom, CommonCommonTests
 from txdav.common.datastore.podding.conduit import PoddingConduit, \
-    InvalidCrossPodRequestError
+    FailedCrossPodRequestError
 from txdav.common.idirectoryservice import DirectoryRecordNotFoundError
+from txdav.common.datastore.podding.test.util import MultiStoreConduitTest, \
+    FakeConduitRequest
+from txdav.common.datastore.sql_tables import _BIND_STATUS_ACCEPTED
+from pycalendar.datetime import DateTime
+from twistedcaldav.ical import Component
 
-class Conduit (CommonCommonTests, twext.web2.dav.test.util.TestCase):
+class TestConduit (CommonCommonTests, twext.web2.dav.test.util.TestCase):
 
     class FakeConduit(object):
 
@@ -40,7 +45,7 @@
 
     @inlineCallbacks
     def setUp(self):
-        yield super(Conduit, self).setUp()
+        yield super(TestConduit, self).setUp()
         self._sqlCalendarStore = yield buildCalendarStore(self, self.notifierFactory)
         self.directory = self._sqlCalendarStore.directoryService()
 
@@ -113,4 +118,378 @@
 
         self.assertRaises(DirectoryRecordNotFoundError, conduit.validRequst, "bogus01", "user02")
         self.assertRaises(DirectoryRecordNotFoundError, conduit.validRequst, "user01", "bogus02")
-        self.assertRaises(InvalidCrossPodRequestError, conduit.validRequst, "user01", "user02")
+        self.assertRaises(FailedCrossPodRequestError, conduit.validRequst, "user01", "user02")
+
+
+
+class TestConduitToConduit(MultiStoreConduitTest):
+
+    class FakeConduit(PoddingConduit):
+
+        @inlineCallbacks
+        def send_fake(self, txn, ownerUID, shareeUID):
+            _ignore_owner, sharee = self.validRequst(ownerUID, shareeUID)
+            action = {
+                "action": "fake",
+                "echo": "bravo"
+            }
+
+            result = yield self.sendRequest(txn, sharee, action)
+            returnValue(result)
+
+
+        def recv_fake(self, txn, j):
+            return succeed({
+                "result": "ok",
+                "back2u": j["echo"],
+                "more": "bits",
+            })
+
+
+    def makeConduit(self, store):
+        """
+        Use our own variant.
+        """
+        conduit = self.FakeConduit(store)
+        conduit.conduitRequestClass = FakeConduitRequest
+        return conduit
+
+
+    @inlineCallbacks
+    def test_fake_action(self):
+        """
+        Cross-pod request works when conduit does support the action.
+        """
+
+        txn = self.transactionUnderTest()
+        store1 = self.storeUnderTest()
+        response = yield store1.conduit.send_fake(txn, "user01", "puser01")
+        self.assertTrue("result" in response)
+        self.assertEqual(response["result"], "ok")
+        self.assertTrue("back2u" in response)
+        self.assertEqual(response["back2u"], "bravo")
+        self.assertTrue("more" in response)
+        self.assertEqual(response["more"], "bits")
+        yield txn.commit()
+
+        store2 = self.otherStoreUnderTest()
+        txn = store2.newTransaction()
+        response = yield store2.conduit.send_fake(txn, "puser01", "user01")
+        self.assertTrue("result" in response)
+        self.assertEqual(response["result"], "ok")
+        self.assertTrue("back2u" in response)
+        self.assertEqual(response["back2u"], "bravo")
+        self.assertTrue("more" in response)
+        self.assertEqual(response["more"], "bits")
+        yield txn.commit()
+
+
+
+class TestConduitAPI(MultiStoreConduitTest):
+    """
+    Test that the conduit api works.
+    """
+
+    nowYear = {"now": DateTime.getToday().getYear()}
+
+    caldata1 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:uid1
+DTSTART:{now:04d}0102T140000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+    caldata2 = """BEGIN:VCALENDAR
+VERSION:2.0
+CALSCALE:GREGORIAN
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:ui2
+DTSTART:{now:04d}0102T160000Z
+DURATION:PT1H
+CREATED:20060102T190000Z
+DTSTAMP:20051222T210507Z
+RRULE:FREQ=WEEKLY
+SUMMARY:instance
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n").format(**nowYear)
+
+    @inlineCallbacks
+    def test_basic_share(self):
+        """
+        Test that basic invite/uninvite works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        shared = yield  calendar1.shareeView("puser01")
+        self.assertEqual(shared.shareStatus(), _BIND_STATUS_ACCEPTED)
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        self.assertTrue(shared is not None)
+        self.assertTrue(shared.external())
+        yield self.otherCommit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield calendar1.uninviteUserFromShare("puser01")
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        self.assertTrue(shared is None)
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_countobjects(self):
+        """
+        Test that action=countobjects works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        count = yield shared.countObjectResources()
+        self.assertEqual(count, 0)
+        yield self.otherCommit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        count = yield calendar1.countObjectResources()
+        self.assertEqual(count, 1)
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        count = yield shared.countObjectResources()
+        self.assertEqual(count, 1)
+        yield self.otherCommit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        yield  object1.remove()
+        count = yield calendar1.countObjectResources()
+        self.assertEqual(count, 0)
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        count = yield shared.countObjectResources()
+        self.assertEqual(count, 0)
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_listobjects(self):
+        """
+        Test that action=listobjects works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        objects = yield shared.listObjectResources()
+        self.assertEqual(set(objects), set())
+        yield self.otherCommit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield  calendar1.createCalendarObjectWithName("2.ics", Component.fromString(self.caldata2))
+        objects = yield calendar1.listObjectResources()
+        self.assertEqual(set(objects), set(("1.ics", "2.ics",)))
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        objects = yield shared.listObjectResources()
+        self.assertEqual(set(objects), set(("1.ics", "2.ics",)))
+        yield self.otherCommit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        yield  object1.remove()
+        objects = yield calendar1.listObjectResources()
+        self.assertEqual(set(objects), set(("2.ics",)))
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        objects = yield shared.listObjectResources()
+        self.assertEqual(set(objects), set(("2.ics",)))
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_synctoken(self):
+        """
+        Test that action=synctoken works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        token1_1 = yield calendar1.syncToken()
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        token2_1 = yield shared.syncToken()
+        yield self.otherCommit()
+
+        self.assertEqual(token1_1, token2_1)
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        token1_2 = yield calendar1.syncToken()
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        token2_2 = yield shared.syncToken()
+        yield self.otherCommit()
+
+        self.assertNotEqual(token1_1, token1_2)
+        self.assertEqual(token1_2, token2_2)
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        yield  object1.remove()
+        count = yield calendar1.countObjectResources()
+        self.assertEqual(count, 0)
+        yield self.commit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        token1_3 = yield calendar1.syncToken()
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        token2_3 = yield shared.syncToken()
+        yield self.otherCommit()
+
+        self.assertNotEqual(token1_1, token1_3)
+        self.assertNotEqual(token1_2, token1_3)
+        self.assertEqual(token1_3, token2_3)
+
+
+    @inlineCallbacks
+    def test_resourcenamessincerevision(self):
+        """
+        Test that action=synctoken works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        token1_1 = yield calendar1.syncToken()
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        token2_1 = yield shared.syncToken()
+        yield self.otherCommit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        token1_2 = yield calendar1.syncToken()
+        names1 = yield calendar1.resourceNamesSinceToken(token1_1)
+        self.assertEqual(names1, (["1.ics"], [],))
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        token2_2 = yield shared.syncToken()
+        names2 = yield shared.resourceNamesSinceToken(token2_1)
+        self.assertEqual(names2, (["1.ics"], [],))
+        yield self.otherCommit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        object1 = yield self.calendarObjectUnderTest(home="user01", calendar_name="calendar", name="1.ics")
+        yield  object1.remove()
+        count = yield calendar1.countObjectResources()
+        self.assertEqual(count, 0)
+        yield self.commit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        token1_3 = yield calendar1.syncToken()
+        names1 = yield calendar1.resourceNamesSinceToken(token1_2)
+        self.assertEqual(names1, ([], ["1.ics"],))
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        token2_3 = yield shared.syncToken()
+        names2 = yield shared.resourceNamesSinceToken(token2_2)
+        self.assertEqual(names2, ([], ["1.ics"],))
+        yield self.otherCommit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        names1 = yield calendar1.resourceNamesSinceToken(token1_3)
+        self.assertEqual(names1, ([], [],))
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        names2 = yield shared.resourceNamesSinceToken(token2_3)
+        self.assertEqual(names2, ([], [],))
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_resourceuidforname(self):
+        """
+        Test that action=resourceuidforname works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        uid = yield calendar1.resourceUIDForName("1.ics")
+        self.assertEqual(uid, "uid1")
+        uid = yield calendar1.resourceUIDForName("2.ics")
+        self.assertTrue(uid is None)
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        uid = yield shared.resourceUIDForName("1.ics")
+        self.assertEqual(uid, "uid1")
+        uid = yield shared.resourceUIDForName("2.ics")
+        self.assertTrue(uid is None)
+        yield self.otherCommit()
+
+
+    @inlineCallbacks
+    def test_resourcenameforuid(self):
+        """
+        Test that action=resourcenameforuid works.
+        """
+
+        yield self.createShare("user01", "puser01")
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        yield  calendar1.createCalendarObjectWithName("1.ics", Component.fromString(self.caldata1))
+        yield self.commit()
+
+        calendar1 = yield self.calendarUnderTest(home="user01", name="calendar")
+        uid = yield calendar1.resourceNameForUID("uid1")
+        self.assertEqual(uid, "1.ics")
+        uid = yield calendar1.resourceNameForUID("uid2")
+        self.assertTrue(uid is None)
+        yield self.commit()
+
+        shared = yield self.calendarUnderTest(txn=self.newOtherTransaction(), home="puser01", name="shared-calendar")
+        uid = yield shared.resourceNameForUID("uid1")
+        self.assertEqual(uid, "1.ics")
+        uid = yield shared.resourceNameForUID("uid2")
+        self.assertTrue(uid is None)
+        yield self.otherCommit()

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_external_home.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_external_home.py	2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_external_home.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -86,7 +86,8 @@
             home = yield self.transactionUnderTest().calendarHomeWithUID("puser{:02d}".format(i), create=True)
             self.assertTrue(home is not None)
             self.assertEqual(home._status, _HOME_STATUS_EXTERNAL)
-            self.assertRaises(AssertionError, home.childWithName, "calendar")
+            calendar = yield home.childWithName("calendar")
+            self.assertTrue(calendar is None)
 
 
     @inlineCallbacks

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_resource.py	2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/test_resource.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -25,12 +25,13 @@
 from txdav.common.datastore.podding.resource import ConduitResource
 from txdav.common.datastore.test.util import populateCalendarsFrom, CommonCommonTests
 import json
+from txdav.common.datastore.podding.conduit import PoddingConduit
 
 class ConduitPOST (CommonCommonTests, twext.web2.dav.test.util.TestCase):
 
-    class FakeConduit(object):
+    class FakeConduit(PoddingConduit):
 
-        def recv_fake(self, j):
+        def recv_fake(self, txn, j):
             return succeed({
                 "result": "ok",
                 "back2u": j["echo"],
@@ -214,7 +215,8 @@
         Cross-pod request fails when conduit does not support the action.
         """
 
-        self.patch(self.storeUnderTest(), "conduit", self.FakeConduit())
+        store = self.storeUnderTest()
+        self.patch(store, "conduit", self.FakeConduit(store))
 
         request = SimpleRequest(
             self.site,
@@ -242,7 +244,8 @@
         Cross-pod request works when conduit does support the action.
         """
 
-        self.patch(self.storeUnderTest(), "conduit", self.FakeConduit())
+        store = self.storeUnderTest()
+        self.patch(store, "conduit", self.FakeConduit(store))
 
         request = SimpleRequest(
             self.site,

Added: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/util.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/podding/test/util.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -0,0 +1,202 @@
+##
+# Copyright (c) 2013 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 twisted.internet.defer import inlineCallbacks, returnValue
+
+from txdav.caldav.datastore.scheduling.ischedule.localservers import Server, \
+    Servers
+from txdav.caldav.datastore.test.util import \
+    TestCalendarStoreDirectoryRecord, TestCalendarStoreDirectoryService
+from txdav.common.datastore.podding.conduit import PoddingConduit
+from txdav.common.datastore.test.util import CommonCommonTests, SQLStoreBuilder,\
+    theStoreBuilder
+
+import twext.web2.dav.test.util
+from txdav.common.datastore.sql_tables import _BIND_MODE_WRITE
+
+class FakeConduitRequest(object):
+    """
+    A conduit request that sends messages internally rather than using HTTP
+    """
+
+    storeMap = {}
+
+    @classmethod
+    def addServerStore(cls, server, store):
+        """
+        Add a store mapped to a server. These mappings are used to "deliver" conduit
+        requests to the appropriate store.
+
+        @param uri: the server
+        @type uri: L{Server}
+        @param store: the store
+        @type store: L{ICommonDataStore}
+        """
+
+        cls.storeMap[server.details()] = store
+
+
+    def __init__(self, server, data):
+
+        self.server = server
+        self.data = data
+
+
+    @inlineCallbacks
+    def doRequest(self, txn):
+
+        # Generate an HTTP client request
+        try:
+            response = (yield self._processRequest())
+        except Exception as e:
+            raise ValueError("Failed cross-pod request: {}".format(e))
+
+        returnValue(response)
+
+
+    @inlineCallbacks
+    def _processRequest(self):
+        """
+        Process the request by sending it to the relevant server.
+
+        @return: the HTTP response.
+        @rtype: L{Response}
+        """
+
+        store = self.storeMap[self.server.details()]
+        result = yield store.conduit.processRequest(self.data)
+        returnValue(result)
+
+
+
+class MultiStoreConduitTest(CommonCommonTests, twext.web2.dav.test.util.TestCase):
+
+    theStoreBuilder2 = SQLStoreBuilder(secondary=True)
+    otherTransaction = None
+
+    @inlineCallbacks
+    def setUp(self):
+        yield super(MultiStoreConduitTest, self).setUp()
+
+        server1 = Server("A", "http://127.0.0.1:8008", "A", True)
+        Servers.addServer(server1)
+
+        server2 = Server("B", "http://127.0.0.1:8108", "B", False)
+        Servers.addServer(server2)
+
+        self._sqlCalendarStore1 = yield self.makeStore(theStoreBuilder, True, server1, server2)
+        self._sqlCalendarStore2 = yield self.makeStore(self.theStoreBuilder2, False, server1, server2)
+
+        FakeConduitRequest.addServerStore(server1, self._sqlCalendarStore1)
+        FakeConduitRequest.addServerStore(server2, self._sqlCalendarStore2)
+
+
+    def storeUnderTest(self):
+        """
+        Return a store for testing.
+        """
+        return self._sqlCalendarStore1
+
+
+    def otherStoreUnderTest(self):
+        """
+        Return a store for testing.
+        """
+        return self._sqlCalendarStore2
+
+
+    def newOtherTransaction(self):
+        assert self.otherTransaction is None
+        store2 = self.otherStoreUnderTest()
+        self.otherTransaction = store2.newTransaction()
+        return self.otherTransaction
+
+
+    @inlineCallbacks
+    def otherCommit(self):
+        assert self.otherTransaction is not None
+        yield self.otherTransaction.commit()
+        self.otherTransaction = None
+
+
+    @inlineCallbacks
+    def otherAbort(self):
+        assert self.otherTransaction is not None
+        yield self.otherTransaction.abort()
+        self.otherTransaction = None
+
+
+    @inlineCallbacks
+    def makeStore(self, builder, internal, server1, server2):
+
+        directory = self.makeDirectory(internal, server1, server2)
+        store = yield builder.buildStore(self, self.notifierFactory, directory)
+        store.queryCacher = None     # Cannot use query caching
+        store.conduit = self.makeConduit(store)
+        returnValue(store)
+
+
+    def makeDirectory(self, internal, server1, server2):
+
+        directory = TestCalendarStoreDirectoryService()
+
+        # User accounts
+        for ctr in range(1, 100):
+            directory.addRecord(TestCalendarStoreDirectoryRecord(
+                "user%02d" % (ctr,),
+                ("user%02d" % (ctr,),),
+                "User %02d" % (ctr,),
+                frozenset((
+                    "urn:uuid:user%02d" % (ctr,),
+                    "mailto:user%02d at example.com" % (ctr,),
+                )),
+                thisServer=internal,
+                server=server1
+            ))
+
+        for ctr in range(1, 100):
+            directory.addRecord(TestCalendarStoreDirectoryRecord(
+                "puser{:02d}".format(ctr),
+                ("puser{:02d}".format(ctr),),
+                "Puser {:02d}".format(ctr),
+                frozenset((
+                    "urn:uuid:puser{:02d}".format(ctr),
+                    "mailto:puser{:02d}@example.com".format(ctr),
+                )),
+                thisServer=not internal,
+                server=server2
+            ))
+
+        return directory
+
+
+    def makeConduit(self, store):
+        conduit = PoddingConduit(store)
+        conduit.conduitRequestClass = FakeConduitRequest
+        return conduit
+
+
+    @inlineCallbacks
+    def createShare(self, ownerGUID, shareeGUID, name="calendar"):
+
+        home = yield self.homeUnderTest(name=ownerGUID, create=True)
+        calendar = yield home.calendarWithName("calendar")
+        yield calendar.inviteUserToShare(shareeGUID, _BIND_MODE_WRITE, "shared", shareName="shared-calendar")
+        yield self.commit()
+
+        home2 = yield self.homeUnderTest(txn=self.newOtherTransaction(), name=shareeGUID)
+        yield home2.acceptShare("shared-calendar")
+        yield self.otherCommit()

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql.py	2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -67,7 +67,7 @@
     _BIND_MODE_INDIRECT, _HOME_STATUS_NORMAL, _HOME_STATUS_EXTERNAL
 from txdav.common.datastore.sql_tables import schema, splitSQLString
 from txdav.common.icommondatastore import ConcurrentModification, \
-    RecordNotAllowedError, ExternalShareFailed
+    RecordNotAllowedError, ExternalShareFailed, ShareNotAllowed
 from txdav.common.icommondatastore import HomeChildNameNotAllowedError, \
     HomeChildNameAlreadyExistsError, NoSuchHomeChildError, \
     ObjectResourceNameNotAllowedError, ObjectResourceNameAlreadyExistsError, \
@@ -477,7 +477,7 @@
         self._label = label
         self._migrating = migrating
         self._primaryHomeType = None
-        self._disableCache = disableCache
+        self._disableCache = disableCache or not store.queryCachingEnabled()
         if disableCache:
             self._queryCacher = None
         else:
@@ -1562,6 +1562,25 @@
 
     _cacher = None  # Initialize in derived classes
 
+    @classmethod
+    @inlineCallbacks
+    def makeClass(cls, transaction, ownerUID, no_cache=False):
+        """
+        Build the actual home class taking into account the possibility that we might need to
+        switch in the external version of the class.
+
+        @param transaction: transaction
+        @type transaction: L{CommonStoreTransaction}
+        @param ownerUID: owner UID of home to load
+        @type ownerUID: C{str}
+        @param no_cache: should cached query be used
+        @type no_cache: C{bool}
+        """
+        home = cls(transaction, ownerUID)
+        actualHome = yield home.initFromStore(no_cache)
+        returnValue(actualHome)
+
+
     def __init__(self, transaction, ownerUID):
         self._txn = transaction
         self._ownerUID = ownerUID
@@ -1708,6 +1727,10 @@
                 actualHome = self
             yield actualHome.initMetaDataFromStore()
             yield actualHome._loadPropertyStore()
+
+            for factory_type, factory in self._txn._notifierFactories.items():
+                actualHome.addNotifier(factory_type, factory.newNotifier(actualHome))
+
             returnValue(actualHome)
         else:
             returnValue(None)
@@ -1756,10 +1779,7 @@
     @classmethod
     @inlineCallbacks
     def homeWithUID(cls, txn, uid, create=False):
-        homeObject = cls(txn, uid)
-        for factory_type, factory in txn._notifierFactories.items():
-            homeObject.addNotifier(factory_type, factory.newNotifier(homeObject))
-        homeObject = (yield homeObject.initFromStore())
+        homeObject = yield cls.makeClass(txn, uid)
         if homeObject is not None:
             returnValue(homeObject)
         else:
@@ -1769,7 +1789,7 @@
             # Determine if the user is local or external
             record = txn.directoryService().recordWithUID(uid)
             if record is None:
-                raise DirectoryRecordNotFoundError("Cannot create home for UID since no directory record exists: {uid}".format(uid=uid))
+                raise DirectoryRecordNotFoundError("Cannot create home for UID since no directory record exists: {}".format(uid))
 
             state = _HOME_STATUS_NORMAL if record.thisServer() else _HOME_STATUS_EXTERNAL
 
@@ -1794,10 +1814,7 @@
                 yield savepoint.rollback(txn)
 
                 # Retry the query - row may exist now, if not re-raise
-                homeObject = cls(txn, uid)
-                for factory_type, factory in txn._notifierFactories.items():
-                    homeObject.addNotifier(factory_type, factory.newNotifier(homeObject))
-                homeObject = (yield homeObject.initFromStore())
+                homeObject = yield cls.makeClass(txn, uid)
                 if homeObject:
                     returnValue(homeObject)
                 else:
@@ -1808,10 +1825,7 @@
                 # Note that we must not cache the owner_uid->resource_id
                 # mapping in _cacher when creating as we don't want that to appear
                 # until AFTER the commit
-                home = cls(txn, uid)
-                for factory_type, factory in txn._notifierFactories.items():
-                    home.addNotifier(factory_type, factory.newNotifier(home))
-                home = (yield home.initFromStore(no_cache=True))
+                home = yield cls.makeClass(txn, uid, no_cache=True)
                 yield home.createdHome()
                 returnValue(home)
 
@@ -2533,165 +2547,6 @@
 
 
 
-class CommonHomeExternal(CommonHome):
-    """
-    A CommonHome for a user not hosted on this system, but on another pod. This is needed to provide a
-    "reference" to the external user so we can share with them. Actual operations to list child resources, etc
-    are all stubbed out since no data for the user is actually hosted in this store.
-    """
-
-    def __init__(self, transaction, ownerUID, resourceID):
-        super(CommonHomeExternal, self).__init__(transaction, ownerUID)
-        self._resourceID = resourceID
-        self._status = _HOME_STATUS_EXTERNAL
-
-
-    def initFromStore(self, no_cache=False):
-        """
-        Never called - this should be done by CommonHome.initFromStore only.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def external(self):
-        """
-        Is this an external home.
-
-        @return: a string.
-        """
-        return True
-
-
-    def children(self):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def loadChildren(self):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def listChildren(self):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def objectWithShareUID(self, shareUID):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def invitedObjectWithShareUID(self, shareUID):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    @memoizedKey("name", "_children")
-    @inlineCallbacks
-    def createChildWithName(self, name, externalID=None):
-        """
-        No real children - only external ones.
-        """
-        if externalID is None:
-            raise AssertionError("CommonHomeExternal: not supported")
-        child = yield super(CommonHomeExternal, self).createChildWithName(name, externalID)
-        returnValue(child)
-
-
-    def removeChildWithName(self, name):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def syncToken(self):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def resourceNamesSinceRevision(self, revision, depth):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    @inlineCallbacks
-    def _loadPropertyStore(self):
-        """
-        No property store - stub to a NonePropertyStore.
-        """
-        props = yield PropertyStore.load(
-            self.uid(),
-            self.uid(),
-            self._txn,
-            self._resourceID,
-            notifyCallback=self.notifyChanged
-        )
-        self._propertyStore = props
-
-
-    def properties(self):
-        return self._propertyStore
-
-
-    def objectResourcesWithUID(self, uid, ignore_children=[], allowShared=True):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def objectResourceWithID(self, rid):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-    def notifyChanged(self):
-        """
-        Notifications are not handled for external homes - make this a no-op.
-        """
-        return succeed(None)
-
-
-    def bumpModified(self):
-        """
-        No changes recorded for external homes - make this a no-op.
-        """
-        return succeed(None)
-
-
-    def removeUnacceptedShares(self):
-        """
-        No children.
-        """
-        raise AssertionError("CommonHomeExternal: not supported")
-
-
-#    def ownerHomeAndChildNameForChildID(self, resourceID):
-#        """
-#        No children.
-#        """
-#        raise AssertionError("CommonHomeExternal: not supported")
-
-
-
 class _SharedSyncLogic(object):
     """
     Logic for maintaining sync-token shared between notification collections and
@@ -3507,7 +3362,7 @@
                 subt,
                 homeID=shareeHome._resourceID,
                 resourceID=self._resourceID,
-                externalID=None,
+                externalID=self._externalID,
                 name=newName,
                 mode=mode,
                 bindStatus=status,
@@ -3997,6 +3852,7 @@
         "_resourceID",
     )
 
+    _externalClass = None
     _objectResourceClass = None
 
     _bindSchema = None
@@ -4007,6 +3863,142 @@
     _objectSchema = None
 
 
+    @classmethod
+    @inlineCallbacks
+    def makeClass(cls, home, bindData, additionalBindData, metadataData, propstore=None, ownerHome=None):
+        """
+        Given the various database rows, build the actual class.
+
+        @param home: the parent home object
+        @type home: L{CommonHome}
+        @param bindData: the standard set of bind columns
+        @type bindData: C{list}
+        @param additionalBindData: additional bind data specific to sub-classes
+        @type additionalBindData: C{list}
+        @param metadataData: metadata data
+        @type metadataData: C{list}
+        @param propstore: a property store to use, or C{None} to load it automatically
+        @type propstore: L{PropertyStore}
+        @param ownerHome: the home of the owner, or C{None} to figure it out automatically
+        @type ownerHome: L{CommonHome}
+
+        @return: the constructed child class
+        @rtype: L{CommonHomeChild}
+        """
+
+        bindMode, _ignore_homeID, resourceID, externalID, name, bindStatus, bindRevision, bindMessage = bindData
+
+        if ownerHome is None:
+            if bindMode == _BIND_MODE_OWN:
+                ownerHome = home
+                ownerName = name
+            else:
+                ownerHome, ownerName = yield home.ownerHomeAndChildNameForChildID(resourceID)
+        else:
+            ownerName = None
+
+        c = cls._externalClass if ownerHome.external() else cls
+        child = c(
+            home=home,
+            name=name,
+            resourceID=resourceID,
+            mode=bindMode,
+            status=bindStatus,
+            revision=bindRevision,
+            message=bindMessage,
+            ownerHome=ownerHome,
+            ownerName=ownerName,
+            externalID=externalID,
+        )
+
+        if additionalBindData:
+            for attr, value in zip(child.additionalBindAttributes(), additionalBindData):
+                setattr(child, attr, value)
+
+        if metadataData:
+            for attr, value in zip(child.metadataAttributes(), metadataData):
+                setattr(child, attr, value)
+
+        # We have to re-adjust the property store object to account for possible shared
+        # collections as previously we loaded them all as if they were owned
+        if propstore and bindMode != _BIND_MODE_OWN:
+            propstore._setDefaultUserUID(ownerHome.uid())
+        yield child._loadPropertyStore(propstore)
+
+        returnValue(child)
+
+
+    @classmethod
+    @inlineCallbacks
+    def _getDBData(cls, home, name, resourceID, externalID):
+        """
+        Given a set of identifying information, load the data rows for the object. Only one of
+        L{name}, L{resourceID} or L{externalID} is specified - others are C{None}.
+
+        @param home: the parent home object
+        @type home: L{CommonHome}
+        @param name: the resource name
+        @type name: C{str}
+        @param resourceID: the resource ID
+        @type resourceID: C{int}
+        @param externalID: the resource ID of the external (cross-pod) referenced item
+        @type externalID: C{int}
+        """
+
+        # Get the bind row data
+        row = None
+        queryCacher = home._txn._queryCacher
+
+        if queryCacher:
+            # Retrieve data from cache
+            if name:
+                cacheKey = queryCacher.keyForObjectWithName(home._resourceID, name)
+            elif resourceID:
+                cacheKey = queryCacher.keyForObjectWithResourceID(home._resourceID, resourceID)
+            elif externalID:
+                cacheKey = queryCacher.keyForObjectWithExternalID(home._resourceID, externalID)
+            row = yield queryCacher.get(cacheKey)
+
+        if row is None:
+            # No cached copy
+            if name:
+                rows = yield cls._bindForNameAndHomeID.on(home._txn, name=name, homeID=home._resourceID)
+            elif resourceID:
+                rows = yield cls._bindForResourceIDAndHomeID.on(home._txn, resourceID=resourceID, homeID=home._resourceID)
+            elif externalID:
+                rows = yield cls._bindForExternalIDAndHomeID.on(home._txn, externalID=externalID, homeID=home._resourceID)
+            row = rows[0] if rows else None
+
+        if not row:
+            returnValue(None)
+
+        if queryCacher:
+            # Cache the result
+            queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithName(home._resourceID, name), row)
+            queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithResourceID(home._resourceID, resourceID), row)
+            queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithExternalID(home._resourceID, externalID), row)
+
+        bindData = row[:cls.bindColumnCount]
+        additionalBindData = row[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
+        resourceID = bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_ID)]
+
+        # Get the matching metadata data
+        metadataData = None
+        if queryCacher:
+            # Retrieve from cache
+            cacheKey = queryCacher.keyForHomeChildMetaData(resourceID)
+            metadataData = yield queryCacher.get(cacheKey)
+
+        if metadataData is None:
+            # No cached copy
+            metadataData = (yield cls._metadataByIDQuery.on(home._txn, resourceID=resourceID))[0]
+            if queryCacher:
+                # Cache the results
+                yield queryCacher.setAfterCommit(home._txn, cacheKey, metadataData)
+
+        returnValue((bindData, additionalBindData, metadataData,))
+
+
     def __init__(self, home, name, resourceID, mode, status, revision=0, message=None, ownerHome=None, ownerName=None, externalID=None):
 
         self._home = home
@@ -4062,7 +4054,7 @@
         rows = yield cls._acceptedBindForHomeID.on(
             home._txn, homeID=home._resourceID
         )
-        names = [row[3] for row in rows]
+        names = [row[cls.bindColumns().index(cls._bindSchema.RESOURCE_NAME)] for row in rows]
         returnValue(names)
 
 
@@ -4095,41 +4087,16 @@
 
         # Create the actual objects merging in properties
         for dataRow in dataRows:
-            bindMode, homeID, resourceID, externalID, bindName, bindStatus, bindRevision, bindMessage = dataRow[:cls.bindColumnCount] #@UnusedVariable
-            additionalBind = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
-            metadata = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
+            bindData = dataRow[:cls.bindColumnCount] #@UnusedVariable
+            resourceID = bindData[cls.bindColumns().index(cls._bindSchema.RESOURCE_ID)]
+            additionalBindData = dataRow[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
+            metadataData = dataRow[cls.bindColumnCount + len(cls.additionalBindColumns()):]
+            propstore = propertyStores.get(resourceID, None)
 
-            if bindMode == _BIND_MODE_OWN:
-                ownerHome = home
-                ownerName = bindName
-            else:
-                #TODO: get all ownerHomeIDs at once
-                ownerHome, ownerName = yield home.ownerHomeAndChildNameForChildID(resourceID)
-
-            child = cls(
-                home=home,
-                name=bindName,
-                resourceID=resourceID,
-                mode=bindMode,
-                status=bindStatus,
-                revision=bindRevision,
-                message=bindMessage,
-                ownerHome=ownerHome,
-                ownerName=ownerName,
-                externalID=externalID,
-            )
-            for attr, value in zip(cls.additionalBindAttributes(), additionalBind):
-                setattr(child, attr, value)
-            for attr, value in zip(cls.metadataAttributes(), metadata):
-                setattr(child, attr, value)
+            child = yield cls.makeClass(home, bindData, additionalBindData, metadataData, propstore)
             child._syncTokenRevision = revisions[resourceID]
-            propstore = propertyStores.get(resourceID, None)
-            # We have to re-adjust the property store object to account for possible shared
-            # collections as previously we loaded them all as if they were owned
-            if propstore and bindMode != _BIND_MODE_OWN:
-                propstore._setDefaultUserUID(ownerHome.uid())
-            yield child._loadPropertyStore(propstore)
             results.append(child)
+
         returnValue(results)
 
 
@@ -4163,63 +4130,17 @@
         @return: an L{CommonHomeChild} or C{None} if no such child
             exists.
         """
-        rows = None
-        queryCacher = home._txn._queryCacher
 
-        if queryCacher:
-            # Retrieve data from cache
-            if name:
-                cacheKey = queryCacher.keyForObjectWithName(home._resourceID, name)
-            elif resourceID:
-                cacheKey = queryCacher.keyForObjectWithResourceID(home._resourceID, resourceID)
-            elif externalID:
-                cacheKey = queryCacher.keyForObjectWithExternalID(home._resourceID, externalID)
-            rows = yield queryCacher.get(cacheKey)
-
-        if rows is None:
-            # No cached copy
-            if name:
-                rows = yield cls._bindForNameAndHomeID.on(home._txn, name=name, homeID=home._resourceID)
-            elif resourceID:
-                rows = yield cls._bindForResourceIDAndHomeID.on(home._txn, resourceID=resourceID, homeID=home._resourceID)
-            elif resourceID:
-                rows = yield cls._bindForExternalIDAndHomeID.on(home._txn, externalID=externalID, homeID=home._resourceID)
-
-        if not rows:
+        dbData = yield cls._getDBData(home, name, resourceID, externalID)
+        if dbData is None:
             returnValue(None)
+        bindData, additionalBindData, metadataData = dbData
 
-        row = rows[0]
-        bindMode, homeID, resourceID, externalID, name, bindStatus, bindRevision, bindMessage = row[:cls.bindColumnCount] #@UnusedVariable
-
-        if queryCacher:
-            # Cache the result
-            queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithName(home._resourceID, name), rows)
-            queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithResourceID(home._resourceID, resourceID), rows)
-            queryCacher.setAfterCommit(home._txn, queryCacher.keyForObjectWithExternalID(home._resourceID, externalID), rows)
-
+        bindStatus = bindData[cls.bindColumns().index(cls._bindSchema.BIND_STATUS)]
         if accepted is not None and (bindStatus == _BIND_STATUS_ACCEPTED) != bool(accepted):
             returnValue(None)
-        additionalBind = row[cls.bindColumnCount:cls.bindColumnCount + len(cls.additionalBindColumns())]
 
-        if bindMode == _BIND_MODE_OWN:
-            ownerHome = home
-            ownerName = name
-        else:
-            ownerHome, ownerName = yield home.ownerHomeAndChildNameForChildID(resourceID)
-
-        child = cls(
-            home=home,
-            name=name,
-            resourceID=resourceID,
-            mode=bindMode,
-            status=bindStatus,
-            revision=bindRevision,
-            message=bindMessage,
-            ownerHome=ownerHome,
-            ownerName=ownerName,
-            externalID=externalID,
-        )
-        yield child.initFromStore(additionalBind)
+        child = yield cls.makeClass(home, bindData, additionalBindData, metadataData)
         returnValue(child)
 
 
@@ -4289,36 +4210,6 @@
                       Where=child.RESOURCE_ID == Parameter("resourceID"))
 
 
-    @inlineCallbacks
-    def initFromStore(self, additionalBind=None):
-        """
-        Initialise this object from the store, based on its already-populated
-        resource ID. We read in and cache all the extra metadata from the DB to
-        avoid having to do DB queries for those individually later.
-        """
-        queryCacher = self._txn._queryCacher
-        if queryCacher:
-            # Retrieve from cache
-            cacheKey = queryCacher.keyForHomeChildMetaData(self._resourceID)
-            dataRows = yield queryCacher.get(cacheKey)
-        else:
-            dataRows = None
-        if dataRows is None:
-            # No cached copy
-            dataRows = (yield self._metadataByIDQuery.on(self._txn, resourceID=self._resourceID))[0]
-            if queryCacher:
-                # Cache the results
-                yield queryCacher.setAfterCommit(self._txn, cacheKey, dataRows)
-
-        if additionalBind:
-            for attr, value in zip(self.additionalBindAttributes(), additionalBind):
-                setattr(self, attr, value)
-
-        for attr, value in zip(self.metadataAttributes(), dataRows):
-            setattr(self, attr, value)
-        yield self._loadPropertyStore()
-
-
     def id(self):
         """
         Retrieve the store identifier for this collection.
@@ -4329,6 +4220,15 @@
         return self._resourceID
 
 
+    def external_id(self):
+        """
+        Retrieve the external store identifier for this collection.
+
+        @return: a string.
+        """
+        return self._externalID
+
+
     def external(self):
         """
         Is this an external home.
@@ -4385,6 +4285,10 @@
 
         @return: a L{Deferred} which fires when the modification is complete.
         """
+
+        if self.isShared() or self.external():
+            raise ShareNotAllowed("Cannot rename a shared collection")
+
         oldName = self._name
 
         yield self.invalidateQueryCache()
@@ -4521,8 +4425,7 @@
     @inlineCallbacks
     def countObjectResources(self):
         if self._objectNames is None:
-            rows = yield self._objectCountQuery.on(
-                self._txn, resourceID=self._resourceID)
+            rows = yield self._objectCountQuery.on(self._txn, resourceID=self._resourceID)
             returnValue(rows[0][0])
         returnValue(len(self._objectNames))
 
@@ -4612,7 +4515,7 @@
         obj = cls._objectSchema
         return Select(
             [obj.UID], From=obj,
-            Where=(obj.UID == Parameter("name")
+            Where=(obj.RESOURCE_NAME == Parameter("name")
                   ).And(obj.PARENT_RESOURCE_ID == Parameter("resourceID")))
 
 
@@ -4971,10 +4874,84 @@
         "_parentCollection",
     )
 
+    _externalClass = None
     _objectSchema = None
 
     BATCH_LOAD_SIZE = 50
 
+
+    @classmethod
+    @inlineCallbacks
+    def makeClass(cls, parent, objectData, propstore=None):
+        """
+        Given the various database rows, build the actual class.
+
+        @param parent: the parent collection object
+        @type parent: L{CommonHomeChild}
+        @param objectData: the standard set of object columns
+        @type objectData: C{list}
+        @param propstore: a property store to use, or C{None} to load it automatically
+        @type propstore: L{PropertyStore}
+
+        @return: the constructed child class
+        @rtype: L{CommonHomeChild}
+        """
+
+        c = cls._externalClass if parent.external() else cls
+        child = c(
+            parent,
+            objectData[cls._allColumns().index(cls._objectSchema.RESOURCE_NAME)],
+            objectData[cls._allColumns().index(cls._objectSchema.UID)],
+        )
+
+        for attr, value in zip(child._rowAttributes(), objectData):
+            setattr(child, attr, value)
+
+        yield child._loadPropertyStore(propstore)
+
+        returnValue(child)
+
+
+    @classmethod
+    @inlineCallbacks
+    def _getDBData(cls, parent, name, uid, resourceID):
+        """
+        Given a set of identifying information, load the data rows for the object. Only one of
+        L{name}, L{uid} or L{resourceID} is specified - others are C{None}.
+
+        @param parent: the parent collection object
+        @type parent: L{CommonHomeChild}
+        @param name: the resource name
+        @type name: C{str}
+        @param uid: the UID of the data
+        @type uid: C{str}
+        @param resourceID: the resource ID
+        @type resourceID: C{int}
+        """
+
+        rows = None
+        if name:
+            rows = yield cls._allColumnsWithParentAndName.on(
+                parent._txn,
+                name=name,
+                parentID=parent._resourceID
+            )
+        elif uid:
+            rows = yield cls._allColumnsWithParentAndUID.on(
+                parent._txn,
+                uid=uid,
+                parentID=parent._resourceID
+            )
+        elif resourceID:
+            rows = yield cls._allColumnsWithParentAndID.on(
+                parent._txn,
+                resourceID=resourceID,
+                parentID=parent._resourceID
+            )
+
+        returnValue(rows[0] if rows else None)
+
+
     def __init__(self, parent, name, uid, resourceID=None, options=None): #@UnusedVariable
         self._parentCollection = parent
         self._resourceID = resourceID
@@ -4992,7 +4969,7 @@
     @classproperty
     def _allColumnsWithParentQuery(cls): #@NoSelf
         obj = cls._objectSchema
-        return Select(cls._allColumns, From=obj,
+        return Select(cls._allColumns(), From=obj,
                       Where=obj.PARENT_RESOURCE_ID == Parameter("parentID"))
 
 
@@ -5033,11 +5010,10 @@
 
         # Create the actual objects merging in properties
         for row in dataRows:
-            child = cls(parent, "", None)
-            child._initFromRow(tuple(row))
-            yield child._loadPropertyStore(
-                props=propertyStores.get(child._resourceID, None)
-            )
+            resourceID = row[cls._allColumns().index(cls._objectSchema.RESOURCE_ID)]
+            propstore = propertyStores.get(resourceID, None)
+
+            child = yield cls.makeClass(parent, row, propstore=propstore)
             results.append(child)
 
         returnValue(results)
@@ -5062,7 +5038,7 @@
     @classmethod
     def _allColumnsWithParentAndNamesQuery(cls, names):
         obj = cls._objectSchema
-        return Select(cls._allColumns, From=obj,
+        return Select(cls._allColumns(), From=obj,
                       Where=(obj.PARENT_RESOURCE_ID == Parameter("parentID")).And(
                           obj.RESOURCE_NAME.In(Parameter("names", len(names)))))
 
@@ -5107,11 +5083,10 @@
 
         # Create the actual objects merging in properties
         for row in dataRows:
-            child = cls(parent, "", None)
-            child._initFromRow(tuple(row))
-            yield child._loadPropertyStore(
-                props=propertyStores.get(child._resourceID, None)
-            )
+            resourceID = row[cls._allColumns().index(cls._objectSchema.RESOURCE_ID)]
+            propstore = propertyStores.get(resourceID, None)
+
+            child = yield cls.makeClass(parent, row, propstore=propstore)
             results.append(child)
 
         returnValue(results)
@@ -5119,18 +5094,29 @@
 
     @classmethod
     def objectWithName(cls, parent, name, uid):
-        objectResource = cls(parent, name, uid, None)
-        return objectResource.initFromStore()
+        return cls._objectWithNameOrID(parent, name, uid, None)
 
 
     @classmethod
     def objectWithID(cls, parent, resourceID):
-        objectResource = cls(parent, None, None, resourceID)
-        return objectResource.initFromStore()
+        return cls._objectWithNameOrID(parent, None, None, resourceID)
 
 
     @classmethod
     @inlineCallbacks
+    def _objectWithNameOrID(cls, parent, name, uid, resourceID):
+
+        row = yield cls._getDBData(parent, name, uid, resourceID)
+
+        if row:
+            child = yield cls.makeClass(parent, row)
+            returnValue(child)
+        else:
+            returnValue(None)
+
+
+    @classmethod
+    @inlineCallbacks
     def create(cls, parent, name, component, options=None):
 
         child = (yield parent.objectResourceWithName(name))
@@ -5140,13 +5126,13 @@
         if name.startswith("."):
             raise ObjectResourceNameNotAllowedError(name)
 
-        objectResource = cls(parent, name, None, None, options=options)
+        c = cls._externalClass if parent.external() else cls
+        objectResource = c(parent, name, None, None, options=options)
         yield objectResource.setComponent(component, inserting=True)
         yield objectResource._loadPropertyStore(created=True)
 
         # Note: setComponent triggers a notification, so we don't need to
         # call notify( ) here like we do for object removal.
-
         returnValue(objectResource)
 
 
@@ -5157,7 +5143,7 @@
         parameter and a given instance column matches a given parameter name.
         """
         return Select(
-            cls._allColumns, From=cls._objectSchema,
+            cls._allColumns(), From=cls._objectSchema,
             Where=(column == Parameter(paramName)).And(
                 cls._objectSchema.PARENT_RESOURCE_ID == Parameter("parentID"))
         )
@@ -5178,38 +5164,7 @@
         return cls._allColumnsWithParentAnd(cls._objectSchema.RESOURCE_ID, "resourceID")
 
 
-    @inlineCallbacks
-    def initFromStore(self):
-        """
-        Initialise this object from the store. We read in and cache all the
-        extra metadata from the DB to avoid having to do DB queries for those
-        individually later. Either the name or uid is present, so we have to
-        tweak the query accordingly.
-
-        @return: L{self} if object exists in the DB, else C{None}
-        """
-
-        if self._name:
-            rows = yield self._allColumnsWithParentAndName.on(
-                self._txn, name=self._name,
-                parentID=self._parentCollection._resourceID)
-        elif self._uid:
-            rows = yield self._allColumnsWithParentAndUID.on(
-                self._txn, uid=self._uid,
-                parentID=self._parentCollection._resourceID)
-        elif self._resourceID:
-            rows = yield self._allColumnsWithParentAndID.on(
-                self._txn, resourceID=self._resourceID,
-                parentID=self._parentCollection._resourceID)
-        if rows:
-            self._initFromRow(tuple(rows[0]))
-            yield self._loadPropertyStore()
-            returnValue(self)
-        else:
-            returnValue(None)
-
-
-    @classproperty
+    @classmethod
     def _allColumns(cls): #@NoSelf
         """
         Full set of columns in the object table that need to be loaded to
@@ -5227,18 +5182,17 @@
         ]
 
 
-    def _initFromRow(self, row):
-        """
-        Given a select result using the columns from L{_allColumns}, initialize
-        the object resource state.
-        """
-        (self._resourceID,
-         self._name,
-         self._uid,
-         self._md5,
-         self._size,
-         self._created,
-         self._modified,) = tuple(row)
+    @classmethod
+    def _rowAttributes(cls): #@NoSelf
+        return (
+            "_resourceID",
+            "_name",
+            "_uid",
+            "_md5",
+            "_size",
+            "_created",
+            "_modified",
+        )
 
 
     @inlineCallbacks
@@ -5542,11 +5496,11 @@
             # Determine if the user is local or external
             record = txn.directoryService().recordWithUID(uid)
             if record is None:
-                raise DirectoryRecordNotFoundError("Cannot create home for UID since no directory record exists: {uid}".format(uid=uid))
+                raise DirectoryRecordNotFoundError("Cannot create home for UID since no directory record exists: {}".format(uid))
 
             state = _HOME_STATUS_NORMAL if record.thisServer() else _HOME_STATUS_EXTERNAL
             if state == _HOME_STATUS_EXTERNAL:
-                raise RecordNotAllowedError("Cannot store notifications for external user: {uid}".format(uid=uid))
+                raise RecordNotAllowedError("Cannot store notifications for external user: {}".format(uid))
 
             # Use savepoint so we can do a partial rollback if there is a race
             # condition where this row has already been inserted

Added: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql_external.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql_external.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/sql_external.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -0,0 +1,354 @@
+# -*- test-case-name: txdav.caldav.datastore.test.test_sql,txdav.carddav.datastore.test.test_sql -*-
+##
+# Copyright (c) 2013 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 txdav.common.icommondatastore import NonExistentExternalShare, \
+    ExternalShareFailed
+"""
+SQL data store.
+"""
+
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+
+from twext.internet.decorate import memoizedKey
+from twext.python.log import Logger
+
+from txdav.base.propertystore.sql import PropertyStore
+from txdav.common.datastore.sql import CommonHome, CommonHomeChild, \
+    CommonObjectResource
+from txdav.common.datastore.sql_tables import _HOME_STATUS_EXTERNAL
+
+
+log = Logger()
+
+class CommonHomeExternal(CommonHome):
+    """
+    A CommonHome for a user not hosted on this system, but on another pod. This is needed to provide a
+    "reference" to the external user so we can share with them. Actual operations to list child resources, etc
+    are all stubbed out since no data for the user is actually hosted in this store.
+    """
+
+    def __init__(self, transaction, ownerUID, resourceID):
+        super(CommonHomeExternal, self).__init__(transaction, ownerUID)
+        self._resourceID = resourceID
+        self._status = _HOME_STATUS_EXTERNAL
+
+
+    def initFromStore(self, no_cache=False):
+        """
+        Never called - this should be done by CommonHome.initFromStore only.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def external(self):
+        """
+        Is this an external home.
+
+        @return: a string.
+        """
+        return True
+
+
+    def children(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def loadChildren(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def listChildren(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def objectWithShareUID(self, shareUID):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def invitedObjectWithShareUID(self, shareUID):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    @memoizedKey("name", "_children")
+    @inlineCallbacks
+    def createChildWithName(self, name, externalID=None):
+        """
+        No real children - only external ones.
+        """
+        if externalID is None:
+            raise AssertionError("CommonHomeExternal: not supported")
+        child = yield super(CommonHomeExternal, self).createChildWithName(name, externalID)
+        returnValue(child)
+
+
+    def removeChildWithName(self, name):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def syncToken(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def resourceNamesSinceRevision(self, revision, depth):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    @inlineCallbacks
+    def _loadPropertyStore(self):
+        """
+        No property store - stub to a NonePropertyStore.
+        """
+        props = yield PropertyStore.load(
+            self.uid(),
+            self.uid(),
+            self._txn,
+            self._resourceID,
+            notifyCallback=self.notifyChanged
+        )
+        self._propertyStore = props
+
+
+    def properties(self):
+        return self._propertyStore
+
+
+    def objectResourcesWithUID(self, uid, ignore_children=[], allowShared=True):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def objectResourceWithID(self, rid):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+    def notifyChanged(self):
+        """
+        Notifications are not handled for external homes - make this a no-op.
+        """
+        return succeed(None)
+
+
+    def bumpModified(self):
+        """
+        No changes recorded for external homes - make this a no-op.
+        """
+        return succeed(None)
+
+
+    def removeUnacceptedShares(self):
+        """
+        No children.
+        """
+        raise AssertionError("CommonHomeExternal: not supported")
+
+
+#    def ownerHomeAndChildNameForChildID(self, resourceID):
+#        """
+#        No children.
+#        """
+#        raise AssertionError("CommonHomeExternal: not supported")
+
+
+
+class CommonHomeChildExternal(CommonHomeChild):
+    """
+    A CommonHomeChild for a collection not hosted on this system, but on another pod. This will forward
+    specific apis to the other pod using cross-pod requests.
+    """
+
+    def external(self):
+        """
+        Is this an external home.
+
+        @return: a string.
+        """
+        return True
+
+
+    def fixNonExistentExternalShare(self):
+        """
+        An external request has returned and indicates the external share no longer exists. That
+        means this shared resource is an "orphan" and needs to be remove (uninvited) to clean things up.
+        """
+        log.error("Non-existent share detected and removed for {share}", share=self)
+        ownerView = yield self.ownerView()
+        yield ownerView.removeShare(self)
+
+
+    def remove(self, rid):
+        """
+        External shares are never removed directly - instead they must be "uninvited".
+        """
+        raise AssertionError("CommonHomeChildExternal: not supported")
+
+
+    @inlineCallbacks
+    def objectResources(self):
+        raise NotImplementedError("TODO: external resource")
+
+
+    @inlineCallbacks
+    def objectResourcesWithNames(self, names):
+        raise NotImplementedError("TODO: external resource")
+
+
+    @inlineCallbacks
+    def listObjectResources(self):
+        if self._objectNames is None:
+            try:
+                self._objectNames = yield self._txn.store().conduit.send_listobjects(self)
+            except NonExistentExternalShare:
+                yield self.fixNonExistentExternalShare()
+                raise ExternalShareFailed("External share does not exist")
+
+        returnValue(self._objectNames)
+
+
+    @inlineCallbacks
+    def countObjectResources(self):
+        if self._objectNames is None:
+            try:
+                count = yield self._txn.store().conduit.send_countobjects(self)
+            except NonExistentExternalShare:
+                yield self.fixNonExistentExternalShare()
+                raise ExternalShareFailed("External share does not exist")
+            returnValue(count)
+        returnValue(len(self._objectNames))
+
+
+    def objectResourceWithName(self, name):
+        raise NotImplementedError("TODO: external resource")
+
+
+    def objectResourceWithUID(self, uid):
+        raise NotImplementedError("TODO: external resource")
+
+
+    def objectResourceWithID(self, resourceID):
+        raise NotImplementedError("TODO: external resource")
+
+
+    @inlineCallbacks
+    def resourceNameForUID(self, uid):
+        try:
+            resource = self._objects[uid]
+            returnValue(resource.name() if resource else None)
+        except KeyError:
+            pass
+
+        try:
+            name = yield self._txn.store().conduit.send_resourcenameforuid(self, uid)
+        except NonExistentExternalShare:
+            yield self.fixNonExistentExternalShare()
+            raise ExternalShareFailed("External share does not exist")
+
+        if name:
+            returnValue(name)
+        else:
+            self._objects[uid] = None
+            returnValue(None)
+
+
+    @inlineCallbacks
+    def resourceUIDForName(self, name):
+        try:
+            resource = self._objects[name]
+            returnValue(resource.uid() if resource else None)
+        except KeyError:
+            pass
+
+        try:
+            uid = yield self._txn.store().conduit.send_resourceuidforname(self, name)
+        except NonExistentExternalShare:
+            yield self.fixNonExistentExternalShare()
+            raise ExternalShareFailed("External share does not exist")
+
+        if uid:
+            returnValue(uid)
+        else:
+            self._objects[name] = None
+            returnValue(None)
+
+
+    @inlineCallbacks
+    def createObjectResourceWithName(self, name, component, options=None):
+        raise NotImplementedError("TODO: external resource")
+
+
+    @inlineCallbacks
+    def moveObjectResource(self, child, newparent, newname=None):
+        raise NotImplementedError("TODO: external resource")
+
+
+    @inlineCallbacks
+    def syncToken(self):
+        if self._syncTokenRevision is None:
+            try:
+                token = yield self._txn.store().conduit.send_synctoken(self)
+                self._syncTokenRevision = self.revisionFromToken(token)
+            except NonExistentExternalShare:
+                yield self.fixNonExistentExternalShare()
+                raise ExternalShareFailed("External share does not exist")
+        returnValue(("%s_%s" % (self._externalID, self._syncTokenRevision,)))
+
+
+    @inlineCallbacks
+    def resourceNamesSinceRevision(self, revision):
+        try:
+            names = yield self._txn.store().conduit.send_resourcenamessincerevision(self, revision)
+        except NonExistentExternalShare:
+            yield self.fixNonExistentExternalShare()
+            raise ExternalShareFailed("External share does not exist")
+
+        returnValue(names)
+
+
+
+class CommonObjectResourceExternal(CommonObjectResource):
+    """
+    A CommonObjectResource for a resource not hosted on this system, but on another pod. This will forward
+    specific apis to the other pod using cross-pod requests.
+    """
+    pass

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/test/test_sql.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/test/test_sql.py	2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/test/test_sql.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -13,6 +13,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
+from txdav.caldav.datastore.test.util import buildDirectoryRecord
 
 """
 Tests for L{txdav.common.datastore.sql}.
@@ -34,6 +35,11 @@
 from twext.enterprise.dal.syntax import Insert
 from txdav.common.datastore.sql import fixUUIDNormalization
 
+from uuid import UUID
+exampleUID = UUID("a" * 32)
+denormalizedUID = str(exampleUID)
+normalizedUID = denormalizedUID.upper()
+
 class CommonSQLStoreTests(CommonCommonTests, TestCase):
     """
     Tests for shared functionality in L{txdav.common.datastore.sql}.
@@ -46,6 +52,9 @@
         """
         yield super(CommonSQLStoreTests, self).setUp()
         self._sqlStore = yield buildStore(self, self.notifierFactory)
+        self._sqlStore.directoryService().addRecord(buildDirectoryRecord(denormalizedUID))
+        self._sqlStore.directoryService().addRecord(buildDirectoryRecord(normalizedUID))
+        self._sqlStore.directoryService().addRecord(buildDirectoryRecord("uid"))
 
 
     def storeUnderTest(self):
@@ -421,10 +430,3 @@
         yield fixUUIDNormalization(self.storeUnderTest())
         self.assertEqual((yield self.allHomeUIDs(schema.ADDRESSBOOK_HOME)),
                          [[normalizedUID]])
-
-
-
-from uuid import UUID
-exampleUID = UUID("a" * 32)
-denormalizedUID = str(exampleUID)
-normalizedUID = denormalizedUID.upper()

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/test/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/test/util.py	2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/test/util.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -122,30 +122,68 @@
 
     implements(IStoreDirectoryRecord)
 
-    def __init__(self, uid, shortNames, fullName):
+    def __init__(self, uid, shortNames, fullName, thisServer=True, server=None):
         self.uid = uid
         self.shortNames = shortNames
         self.fullName = fullName
         self.displayName = self.fullName if self.fullName else self.shortNames[0]
+        self._thisServer = thisServer
+        self._server = server
 
 
+    def thisServer(self):
+        return self._thisServer
 
+
+    def server(self):
+        return self._server
+
+
+
+def buildDirectory(homes=None):
+
+    directory = TestStoreDirectoryService()
+
+    # User accounts
+    for ctr in range(1, 100):
+        directory.addRecord(TestStoreDirectoryRecord(
+            "user%02d" % (ctr,),
+            ("user%02d" % (ctr,),),
+            "User %02d" % (ctr,),
+        ))
+
+    homes = set(homes) if homes is not None else set()
+    for uid in homes:
+        directory.addRecord(buildDirectoryRecord(uid))
+
+    return directory
+
+
+
+def buildDirectoryRecord(uid):
+    return TestStoreDirectoryRecord(
+        uid,
+        (uid,),
+        uid.capitalize(),
+    )
+
+
+
 class SQLStoreBuilder(object):
     """
     Test-fixture-builder which can construct a PostgresStore.
     """
-    sharedService = None
-    currentTestID = None
+    def __init__(self, secondary=False):
+        self.sharedService = None
+        self.currentTestID = None
+        self.sharedDBPath = "_test_sql_db" + str(os.getpid()) + ("-2" if secondary else "")
 
-    SHARED_DB_PATH = "_test_sql_db" + str(os.getpid())
 
-
-    @classmethod
-    def createService(cls, serviceFactory):
+    def createService(self, serviceFactory):
         """
         Create a L{PostgresService} to use for building a store.
         """
-        dbRoot = CachingFilePath(cls.SHARED_DB_PATH)
+        dbRoot = CachingFilePath(self.sharedDBPath)
         return PostgresService(
             dbRoot, serviceFactory, current_sql_schema, resetSchema=True,
             databaseName="caldav",
@@ -161,17 +199,15 @@
         )
 
 
-    @classmethod
-    def childStore(cls):
+    def childStore(self):
         """
         Create a store suitable for use in a child process, that is hooked up
         to the store that a parent test process is managing.
         """
         disableMemcacheForTest(TestCase())
         staticQuota = 3000
-        attachmentRoot = (CachingFilePath(cls.SHARED_DB_PATH)
-                          .child("attachments"))
-        stubsvc = cls.createService(lambda cf: Service())
+        attachmentRoot = (CachingFilePath(self.sharedDBPath).child("attachments"))
+        stubsvc = self.createService(lambda cf: Service())
 
         cp = ConnectionPool(stubsvc.produceConnection, maxConnections=1)
         # Attach the service to the running reactor.
@@ -187,17 +223,17 @@
         return cds
 
 
-    def buildStore(self, testCase, notifierFactory, directoryService=None):
+    def buildStore(self, testCase, notifierFactory, directoryService=None, homes=None):
         """
         Do the necessary work to build a store for a particular test case.
 
         @return: a L{Deferred} which fires with an L{IDataStore}.
         """
         disableMemcacheForTest(testCase)
-        dbRoot = CachingFilePath(self.SHARED_DB_PATH)
+        dbRoot = CachingFilePath(self.sharedDBPath)
         attachmentRoot = dbRoot.child("attachments")
         if directoryService is None:
-            directoryService = TestStoreDirectoryService()
+            directoryService = buildDirectory(homes=homes)
         if self.sharedService is None:
             ready = Deferred()
             def getReady(connectionFactory, storageService):
@@ -244,8 +280,7 @@
         attachmentRoot.createDirectory()
 
         currentTestID = testCase.id()
-        cp = ConnectionPool(self.sharedService.produceConnection,
-                            maxConnections=5)
+        cp = ConnectionPool(self.sharedService.produceConnection, maxConnections=5)
         quota = deriveQuota(testCase)
         store = CommonDataStore(
             cp.connection,
@@ -307,6 +342,7 @@
 
 theStoreBuilder = SQLStoreBuilder()
 buildStore = theStoreBuilder.buildStore
+cleanStore = theStoreBuilder.cleanStore
 
 
 _notSet = object()
@@ -678,13 +714,13 @@
 
 
     @inlineCallbacks
-    def homeUnderTest(self, txn=None, name="home1"):
+    def homeUnderTest(self, txn=None, name="home1", create=False):
         """
         Get the calendar home detailed by C{requirements['home1']}.
         """
         if txn is None:
             txn = self.transactionUnderTest()
-        returnValue((yield txn.calendarHomeWithUID(name)))
+        returnValue((yield txn.calendarHomeWithUID(name, create=create)))
 
 
     @inlineCallbacks

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/upgrade/test/test_migrate.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/upgrade/test/test_migrate.py	2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/datastore/upgrade/test/test_migrate.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -143,7 +143,17 @@
             self.filesPath, {"push": StubNotifierFactory()}, TestStoreDirectoryService(), True, True
         )
         self.sqlStore = yield theStoreBuilder.buildStore(
-            self, StubNotifierFactory()
+            self,
+            StubNotifierFactory(),
+            homes=(
+                "home1",
+                "home2",
+                "home3",
+                "home_defaults",
+                "home_no_splits",
+                "home_splits",
+                "home_splits_shared",
+            )
         )
         self.upgrader = UpgradeToDatabaseStep(self.fileStore, self.sqlStore)
 

Modified: CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/icommondatastore.py
===================================================================
--- CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/icommondatastore.py	2013-12-11 15:40:55 UTC (rev 12050)
+++ CalendarServer/branches/users/cdaboo/cross-pod-sharing/txdav/common/icommondatastore.py	2013-12-11 22:16:01 UTC (rev 12051)
@@ -213,12 +213,28 @@
 
 
 
+class ShareNotAllowed(CommonStoreError):
+    """
+    An operation on a shared resource is not allowed.
+    """
+
+
+
 class ExternalShareFailed(CommonStoreError):
     """
     An external sharee operation failed.
     """
 
 
+
+class NonExistentExternalShare(CommonStoreError):
+    """
+    An external sharee operation failed because the share does not exist on the
+    other pod. The caller of the external request receiving this exception should
+    remove the local external share to "heal" this mismatch.
+    """
+
+
 # Indexing / sync tokens
 
 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20140312/088bfe01/attachment.html>


More information about the calendarserver-changes mailing list