[CalendarServer-changes] [11148] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Wed May 8 11:03:06 PDT 2013


Revision: 11148
          http://trac.calendarserver.org//changeset/11148
Author:   cdaboo at apple.com
Date:     2013-05-08 11:03:05 -0700 (Wed, 08 May 2013)
Log Message:
-----------
Allow default calendar to be deleted and auto-restored when needed. Also required re-factoring store-based notifications
to fix an issue where app-layer propfind cache would not be invalidated by internal-to-the-store object changes.

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/provision/root.py
    CalendarServer/trunk/calendarserver/push/notifier.py
    CalendarServer/trunk/calendarserver/push/test/test_notifier.py
    CalendarServer/trunk/calendarserver/tap/util.py
    CalendarServer/trunk/twistedcaldav/cache.py
    CalendarServer/trunk/twistedcaldav/directory/principal.py
    CalendarServer/trunk/twistedcaldav/extensions.py
    CalendarServer/trunk/twistedcaldav/resource.py
    CalendarServer/trunk/twistedcaldav/scheduling_store/caldav/resource.py
    CalendarServer/trunk/twistedcaldav/storebridge.py
    CalendarServer/trunk/twistedcaldav/test/test_link.py
    CalendarServer/trunk/twistedcaldav/test/test_resource.py
    CalendarServer/trunk/txdav/caldav/datastore/file.py
    CalendarServer/trunk/txdav/caldav/datastore/sql.py
    CalendarServer/trunk/txdav/caldav/datastore/test/common.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_file.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_implicit.py
    CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py
    CalendarServer/trunk/txdav/carddav/datastore/file.py
    CalendarServer/trunk/txdav/carddav/datastore/sql.py
    CalendarServer/trunk/txdav/carddav/datastore/test/common.py
    CalendarServer/trunk/txdav/carddav/datastore/test/test_file.py
    CalendarServer/trunk/txdav/common/datastore/file.py
    CalendarServer/trunk/txdav/common/datastore/sql.py
    CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_18_to_19.sql
    CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_18_to_19.sql
    CalendarServer/trunk/txdav/common/datastore/test/util.py
    CalendarServer/trunk/txdav/common/datastore/upgrade/sql/others/test/test_attachment_migration.py
    CalendarServer/trunk/txdav/common/datastore/upgrade/sql/test/test_upgrade.py
    CalendarServer/trunk/txdav/common/datastore/upgrade/test/test_migrate.py
    CalendarServer/trunk/txdav/idav.py

Modified: CalendarServer/trunk/calendarserver/provision/root.py
===================================================================
--- CalendarServer/trunk/calendarserver/provision/root.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/calendarserver/provision/root.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -40,10 +40,7 @@
 from twistedcaldav.extensions import DirectoryPrincipalPropertySearchMixIn
 from twistedcaldav.extensions import ReadOnlyResourceMixIn
 from twistedcaldav.resource import CalDAVComplianceMixIn
-from twistedcaldav.resource import CalendarHomeResource, AddressBookHomeResource
 from twistedcaldav.directory.principal import DirectoryPrincipalResource
-from twistedcaldav.storebridge import CalendarCollectionResource,\
-    AddressBookCollectionResource, StoreNotificationCollectionResource
 from calendarserver.platform.darwin.wiki import usernameForAuthToken
 
 log = Logger()
@@ -89,12 +86,7 @@
             self.responseCache = MemcacheResponseCache(self.fp)
 
             # These class attributes need to be setup with our memcache notifier
-            CalendarHomeResource.cacheNotifierFactory = MemcacheChangeNotifier
-            AddressBookHomeResource.cacheNotifierFactory = MemcacheChangeNotifier
             DirectoryPrincipalResource.cacheNotifierFactory = MemcacheChangeNotifier
-            CalendarCollectionResource.cacheNotifierFactory = MemcacheChangeNotifier
-            AddressBookCollectionResource.cacheNotifierFactory = MemcacheChangeNotifier
-            StoreNotificationCollectionResource.cacheNotifierFactory = MemcacheChangeNotifier
         else:
             self.responseCache = DisabledCache()
 
@@ -110,6 +102,7 @@
                 return response
             self.contentFilters.append((addConnectionClose, True))
 
+
     def deadProperties(self):
         if not hasattr(self, "_dead_properties"):
             # Get the property store from super
@@ -123,9 +116,11 @@
 
         return self._dead_properties
 
+
     def defaultAccessControlList(self):
         return config.RootResourceACL
 
+
     @inlineCallbacks
     def checkSacl(self, request):
         """
@@ -146,7 +141,6 @@
             ))
             raise HTTPError(response)
 
-
         # SACLs are enabled in the plist, but there may not actually
         # be a SACL group assigned to this service.  Let's see if
         # unauthenticated users are allowed by calling CheckSACL
@@ -202,6 +196,7 @@
         log.warn("User %r is not enabled with the %r SACL(s)" % (username, saclServices,))
         raise HTTPError(responsecode.FORBIDDEN)
 
+
     @inlineCallbacks
     def locateChild(self, request, segments):
 
@@ -311,7 +306,6 @@
                     )
                 raise HTTPError(response)
 
-
         # We don't want the /inbox resource to pay attention to SACLs because
         # we just want it to use the hard-coded ACL for the imip reply user.
         # The /timezones resource is used by the wiki web calendar, so open
@@ -378,7 +372,7 @@
                     if response is None:
                         request.notInCache = True
                         raise KeyError("Not found in cache.")
-        
+
                     returnValue((_CachedResponseResource(response), []))
             except KeyError:
                 pass
@@ -386,10 +380,18 @@
         child = (yield super(RootResource, self).locateChild(request, segments))
         returnValue(child)
 
-    def http_COPY       (self, request): return responsecode.FORBIDDEN
-    def http_MOVE       (self, request): return responsecode.FORBIDDEN
-    def http_DELETE     (self, request): return responsecode.FORBIDDEN
 
+    def http_COPY(self, request):
+        return responsecode.FORBIDDEN
+
+
+    def http_MOVE(self, request):
+        return responsecode.FORBIDDEN
+
+
+    def http_DELETE(self, request):
+        return responsecode.FORBIDDEN
+
 # So CheckSACL will be parameterized
 # We do this after RootResource is defined
 try:

Modified: CalendarServer/trunk/calendarserver/push/notifier.py
===================================================================
--- CalendarServer/trunk/calendarserver/push/notifier.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/calendarserver/push/notifier.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -18,15 +18,21 @@
 Notification framework for Calendar Server
 """
 
-import datetime
-from twext.python.log import LoggingMixIn, Logger
-from twisted.internet.defer import inlineCallbacks, succeed
 from twext.enterprise.dal.record import fromTable
+from twext.enterprise.dal.syntax import Delete
 from twext.enterprise.queue import WorkItem
+from twext.python.log import LoggingMixIn, Logger
+
+from twisted.internet.defer import inlineCallbacks
+
 from txdav.common.datastore.sql_tables import schema
-from twext.enterprise.dal.syntax import Delete
+from txdav.idav import IStoreNotifierFactory, IStoreNotifier
 
+from zope.interface.declarations import implements
 
+import datetime
+
+
 log = Logger()
 
 
@@ -58,23 +64,14 @@
     L{NotifierFactory}.
     """
 
-    def __init__(self, notifierFactory, label="default", id=None, prefix=None):
+    implements(IStoreNotifier)
+
+    def __init__(self, notifierFactory, storeObject):
         self._notifierFactory = notifierFactory
-        self._ids = {label : self.normalizeID(id) }
+        self._storeObject = storeObject
         self._notify = True
-        self._prefix = prefix
 
 
-    def normalizeID(self, id):
-        urn = "urn:uuid:"
-        try:
-            if id.startswith(urn):
-                return id[len(urn):]
-        except AttributeError:
-            pass
-        return id
-
-
     def enableNotify(self, arg):
         self.log_debug("enableNotify: %s" % (self._ids['default'][1],))
         self._notify = True
@@ -87,43 +84,46 @@
 
     @inlineCallbacks
     def notify(self):
-        for label in self._ids.iterkeys():
-            id = self.getID(label=label)
-            if id is not None:
-                if self._notify:
-                    self.log_debug("Notifications are enabled: %s %s" %
-                        (label, id))
-                    yield self._notifierFactory.send(id)
-                else:
-                    self.log_debug("Skipping notification for: %s" % (id,))
+        """
+        Send the notification. For a home object we just push using the home id. For a home
+        child we push both the owner home id and the owned home child id.
+        """
+        # Push ids from the store objects are a tuple of (prefix, name,) and we need to compose that
+        # into a single token.
+        ids = (self._storeObject.notifierID(),)
 
+        # For resources that are children of a home, we need to add the home id too.
+        if hasattr(self._storeObject, "parentNotifierID"):
+            ids += (self._storeObject.parentNotifierID(),)
 
-    def clone(self, label="default", id=None):
-        newNotifier = self.__class__(self._notifierFactory)
-        newNotifier._ids = self._ids.copy()
-        newNotifier._ids[label] = id
-        newNotifier._prefix = self._prefix
-        return newNotifier
+        for prefix, id in ids:
+            if self._notify:
+                self.log_debug("Notifications are enabled: %s %s/%s" % (self._storeObject, prefix, id,))
+                yield self._notifierFactory.send(prefix, id)
+            else:
+                self.log_debug("Skipping notification for: %s %s/%s" % (self._storeObject, prefix, id,))
 
 
-    def addID(self, label="default", id=None):
-        self._ids[label] = self.normalizeID(id)
+    def clone(self, storeObject):
+        return self.__class__(self._notifierFactory, storeObject)
 
 
-    def getID(self, label="default"):
-        id = self._ids.get(label, None)
-        if self._prefix is None:
-            return id
+    def nodeName(self):
+        """
+        The pushkey is the notifier id of the home collection for home and owned home child objects. For
+        a shared home child, the push key is the notifier if of the owner's home child.
+        """
+        if hasattr(self._storeObject, "ownerHome"):
+            if self._storeObject.owned():
+                prefix, id = self._storeObject.ownerHome().notifierID()
+            else:
+                prefix, id = self._storeObject.notifierID()
         else:
-            return "%s|%s" % (self._prefix, id)
+            prefix, id = self._storeObject.notifierID()
+        return self._notifierFactory.pushKeyForId(prefix, id)
 
 
-    def nodeName(self, label="default"):
-        id = self.getID(label=label)
-        return succeed(self._notifierFactory.pushKeyForId(id))
 
-
-
 class NotifierFactory(LoggingMixIn):
     """
     Notifier Factory
@@ -132,8 +132,10 @@
     work queue.
     """
 
-    def __init__(self, store, hostname, coalesceSeconds, reactor=None):
-        self.store = store
+    implements(IStoreNotifierFactory)
+
+    def __init__(self, hostname, coalesceSeconds, reactor=None):
+        self.store = None   # Initialized after the store is created
         self.hostname = hostname
         self.coalesceSeconds = coalesceSeconds
 
@@ -143,42 +145,28 @@
 
 
     @inlineCallbacks
-    def send(self, id):
+    def send(self, prefix, id):
         txn = self.store.newTransaction()
-        notBefore = datetime.datetime.utcnow() + datetime.timedelta(
-            seconds=self.coalesceSeconds)
-        yield txn.enqueue(PushNotificationWork, pushID=self.pushKeyForId(id),
-            notBefore=notBefore)
+        notBefore = datetime.datetime.utcnow() + datetime.timedelta(seconds=self.coalesceSeconds)
+        yield txn.enqueue(PushNotificationWork, pushID=self.pushKeyForId(prefix, id), notBefore=notBefore)
         yield txn.commit()
 
 
-    def newNotifier(self, label="default", id=None, prefix=None):
-        return Notifier(self, label=label, id=id, prefix=prefix)
+    def newNotifier(self, storeObject):
+        return Notifier(self, storeObject)
 
 
-    def pushKeyForId(self, id):
-        path = "/"
+    def pushKeyForId(self, prefix, id):
+        return "/%s/%s/%s/" % (prefix, self.hostname, id)
 
-        try:
-            prefix, id = id.split("|", 1)
-            path += "%s/" % (prefix,)
-        except ValueError:
-            # id has no prefix
-            pass
 
-        path += "%s/" % (self.hostname,)
-        if id:
-            path += "%s/" % (id,)
-        return path
 
-
-
-def getPubSubAPSConfiguration(pushKey, config):
+def getPubSubAPSConfiguration(notifierID, config):
     """
     Returns the Apple push notification settings specific to the pushKey
     """
     try:
-        protocol, ignored = pushKey.split("|", 1)
+        protocol, ignored = notifierID
     except ValueError:
         # id has no protocol, so we can't look up APS config
         return None

Modified: CalendarServer/trunk/calendarserver/push/test/test_notifier.py
===================================================================
--- CalendarServer/trunk/calendarserver/push/test/test_notifier.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/calendarserver/push/test/test_notifier.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -20,6 +20,8 @@
 from calendarserver.push.notifier import PushNotificationWork
 from twisted.internet.defer import inlineCallbacks, succeed
 from twistedcaldav.config import ConfigDict
+from txdav.common.datastore.test.util import populateCalendarsFrom
+from txdav.common.datastore.sql_tables import _BIND_MODE_WRITE
 
 
 class StubService(object):
@@ -67,7 +69,7 @@
                 },
             },
         })
-        result = getPubSubAPSConfiguration("CalDAV|foo", config)
+        result = getPubSubAPSConfiguration(("CalDAV", "foo",), config)
         self.assertEquals(
             result,
             {
@@ -135,3 +137,109 @@
         yield wp.whenExecuted()
         self.assertEquals(pushDistributor.history,
             ["/CalDAV/localhost/bar/", "/CalDAV/localhost/baz/"])
+
+
+
+class NotifierFactory(StoreTestCase):
+
+    requirements = {
+        "home1" : {
+            "calendar_1" : {}
+        },
+        "home2" : {
+            "calendar_1" : {}
+        },
+    }
+
+    @inlineCallbacks
+    def populate(self):
+
+        # Need to bypass normal validation inside the store
+        yield populateCalendarsFrom(self.requirements, self.storeUnderTest())
+        self.notifierFactory.reset()
+
+
+    def test_storeInit(self):
+
+        self.assertTrue("push" in self._sqlCalendarStore._notifierFactories)
+
+
+    @inlineCallbacks
+    def test_homeNotifier(self):
+
+        home = yield self.homeUnderTest()
+        yield home.notifyChanged()
+        yield self.commit()
+        self.assertEquals(self.notifierFactory.history, ["/CalDAV/example.com/home1/"])
+
+
+    @inlineCallbacks
+    def test_calendarNotifier(self):
+
+        calendar = yield self.calendarUnderTest()
+        yield calendar.notifyChanged()
+        yield self.commit()
+        self.assertEquals(
+            set(self.notifierFactory.history),
+            set(["/CalDAV/example.com/home1/", "/CalDAV/example.com/home1/calendar_1/"])
+        )
+
+
+    @inlineCallbacks
+    def test_shareWithNotifier(self):
+
+        calendar = yield self.calendarUnderTest()
+        home2 = yield self.homeUnderTest(name="home2")
+        yield calendar.shareWith(home2, _BIND_MODE_WRITE)
+        yield self.commit()
+        self.assertEquals(
+            set(self.notifierFactory.history),
+            set([
+                "/CalDAV/example.com/home1/",
+                "/CalDAV/example.com/home1/calendar_1/",
+                "/CalDAV/example.com/home2/"
+            ])
+        )
+
+        calendar = yield self.calendarUnderTest()
+        home2 = yield self.homeUnderTest(name="home2")
+        yield calendar.unshareWith(home2)
+        yield self.commit()
+        self.assertEquals(
+            set(self.notifierFactory.history),
+            set([
+                "/CalDAV/example.com/home1/",
+                "/CalDAV/example.com/home1/calendar_1/",
+                "/CalDAV/example.com/home2/"
+            ])
+        )
+
+
+    @inlineCallbacks
+    def test_sharedCalendarNotifier(self):
+
+        calendar = yield self.calendarUnderTest()
+        home2 = yield self.homeUnderTest(name="home2")
+        shareName = yield calendar.shareWith(home2, _BIND_MODE_WRITE)
+        yield self.commit()
+        self.notifierFactory.reset()
+
+        shared = yield self.calendarUnderTest(home="home2", name=shareName)
+        yield shared.notifyChanged()
+        yield self.commit()
+        self.assertEquals(
+            set(self.notifierFactory.history),
+            set(["/CalDAV/example.com/home1/", "/CalDAV/example.com/home1/calendar_1/"])
+        )
+
+
+    @inlineCallbacks
+    def test_notificationNotifier(self):
+
+        notifications = yield self.transactionUnderTest().notificationsWithUID("home1")
+        yield notifications.notifyChanged()
+        yield self.commit()
+        self.assertEquals(
+            set(self.notifierFactory.history),
+            set(["/CalDAV/example.com/home1/", "/CalDAV/example.com/home1/notification/"])
+        )

Modified: CalendarServer/trunk/calendarserver/tap/util.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/util.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/calendarserver/tap/util.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -49,6 +49,7 @@
 # from twisted.python.failure import Failure
 
 from twistedcaldav.bind import doBind
+from twistedcaldav.cache import CacheStoreNotifierFactory
 from twistedcaldav.directory import calendaruserproxy
 from twistedcaldav.directory.addressbook import DirectoryAddressBookHomeProvisioningResource
 from twistedcaldav.directory.aggregate import AggregateDirectoryService
@@ -226,14 +227,13 @@
     #
     # Configure NotifierFactory
     #
+    notifierFactories = {}
     if config.Notifications.Enabled:
-        # FIXME: NotifierFactory needs reference to the store in order
-        # to get a txn in order to create a Work item
-        notifierFactory = NotifierFactory(None, config.ServerHostName,
-            config.Notifications.CoalesceSeconds)
-    else:
-        notifierFactory = None
+        notifierFactories["push"] = NotifierFactory(config.ServerHostName, config.Notifications.CoalesceSeconds)
 
+    if config.EnableResponseCache and config.Memcached.Pools.Default.ClientEnabled:
+        notifierFactories["cache"] = CacheStoreNotifierFactory()
+
     if directoryService is None:
         directoryService = directoryFromConfig(config)
 
@@ -247,7 +247,7 @@
             uri = "http://%s:%s" % (config.ServerHostName, config.HTTPPort,)
         attachments_uri = uri + "/calendars/__uids__/%(home)s/dropbox/%(dropbox_id)s/%(name)s"
         store = CommonSQLDataStore(
-            txnFactory, notifierFactory,
+            txnFactory, notifierFactories,
             directoryService,
             FilePath(config.AttachmentsRoot), attachments_uri,
             config.EnableCalDAV, config.EnableCardDAV,
@@ -266,11 +266,14 @@
     else:
         store = CommonFileDataStore(
             FilePath(config.DocumentRoot),
-            notifierFactory, directoryService,
+            notifierFactories, directoryService,
             config.EnableCalDAV, config.EnableCardDAV,
             quota=quota
         )
-    if notifierFactory is not None:
+
+    # FIXME: NotifierFactories need a reference to the store in order
+    # to get a txn in order to possibly create a Work item
+    for notifierFactory in notifierFactories.values():
         notifierFactory.store = store
     return store
 

Modified: CalendarServer/trunk/twistedcaldav/cache.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/cache.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/twistedcaldav/cache.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -14,23 +14,26 @@
 # limitations under the License.
 ##
 
-import cPickle
-import hashlib
-import uuid
-
-from zope.interface import implements
-
-from twisted.internet.defer import succeed, inlineCallbacks, returnValue
+from twext.python.log import LoggingMixIn
 from twext.web2.dav.util import allDataFromStream
 from twext.web2.http import Response
 from twext.web2.iweb import IResource
 from twext.web2.stream import MemoryStream
 
-from twext.python.log import LoggingMixIn
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue
 
+from twistedcaldav.config import config
 from twistedcaldav.memcachepool import CachePoolUserMixIn, defaultCachePool
-from twistedcaldav.config import config
 
+from txdav.idav import IStoreNotifierFactory, IStoreNotifier
+
+from zope.interface import implements
+
+import cPickle
+import hashlib
+import urllib
+import uuid
+
 """
 The basic principals of the PROPFIND cache are this:
 
@@ -51,7 +54,6 @@
   - childTokens - tokens for any child resources the request uri depends on (for depth:1)
 
   The current principalToken, uriToken and childTokens values are themselves stored in the cache using the key prefix 'cacheToken:'.
-When the 'changeCache' api is called the cached value for the matching token is updated.
 
 (4) When a request is being checked in the cache, the response cache entry key is first computed and any value extracted. The
 tokens in the value are then checked against the current set of tokens in the cache. If there is any mismatch between tokens, the
@@ -100,6 +102,9 @@
 
 
 class MemcacheChangeNotifier(LoggingMixIn, CachePoolUserMixIn):
+    """
+    A change notifier used by resources (not store objects).
+    """
 
     def __init__(self, resource, cachePool=None, cacheHandle="Default"):
         self._resource = resource
@@ -119,10 +124,7 @@
         """
 
         # For shared resources we use the owner URL as the cache key
-        if hasattr(self._resource, "owner_url"):
-            url = self._resource.owner_url()
-        else:
-            url = self._resource.url()
+        url = self._resource.url()
 
         self.log_debug("Changing Cache Token for %r" % (url,))
         return self.getCachePool().set(
@@ -242,15 +244,19 @@
         self._cachePool = cachePool
 
 
+    @inlineCallbacks
     def _tokenForURI(self, uri, cachePoolHandle=None):
         """
         Get the current token for a particular URI.
         """
 
         if cachePoolHandle:
-            return defaultCachePool(cachePoolHandle).get('cacheToken:%s' % (uri,))
+            result = (yield defaultCachePool(cachePoolHandle).get('cacheToken:%s' % (uri,)))
         else:
-            return self.getCachePool().get('cacheToken:%s' % (uri,))
+            result = (yield self.getCachePool().get('cacheToken:%s' % (uri,)))
+        if result is not None:
+            _ignore_flags, result = result
+        returnValue(result)
 
 
     @inlineCallbacks
@@ -321,9 +327,14 @@
                 self.log_debug("Not in cache: %r" % (key,))
                 returnValue(None)
 
-            self.log_debug("Found in cache: %r = %r" % (key, value))
+            (principalToken, directoryToken, uriToken, childTokens, (code, headers, body)) = cPickle.loads(value)
+            self.log_debug("Found in cache: %r = %r" % (key, (
+                principalToken,
+                directoryToken,
+                uriToken,
+                childTokens,
+            )))
 
-            (principalToken, directoryToken, uriToken, childTokens, (code, headers, body)) = cPickle.loads(value)
             currentTokens = (yield self._getTokens(request))
 
             if currentTokens[0] != principalToken:
@@ -361,8 +372,8 @@
                             token))
                     returnValue(None)
 
-            r = Response(code,
-                         stream=MemoryStream(body))
+            self.log_debug("Response cache matched")
+            r = Response(code, stream=MemoryStream(body))
 
             for key, value in headers.iteritems():
                 r.headers.setRawHeaders(key, value)
@@ -404,7 +415,12 @@
                     responseBody
                 )
             ))
-            self.log_debug("Adding to cache: %r = %r" % (key, cacheEntry))
+            self.log_debug("Adding to cache: %r = tokens - %r" % (key, (
+                pToken,
+                dToken,
+                uToken,
+                cTokens,
+            )))
             yield self.getCachePool().set(key, cacheEntry,
                 expireTime=config.ResponseCacheTimeout * 60)
 
@@ -454,45 +470,66 @@
         returnValue(response)
 
 
-    def changeCache(self):
-        if hasattr(self, 'cacheNotifier'):
-            return self.cacheNotifier.changed()
-        else:
-            self.log_debug("%r does not have a cacheNotifier but was changed" % (self,))
 
+class CacheStoreNotifierFactory(LoggingMixIn, CachePoolUserMixIn):
+    """
+    A notifier factory specifically for store object notifications. This is handed of to
+    the data store object, which calls .newNotifier() each time a home object is created
+    and gives the new notifier to the home. That object is also inherited by home child
+    objects created from the home.
 
-
-class ResponseCacheMixin(object):
+    This object uses a memcachepool for setting new cache tokens.
     """
-    This is a mixin for a child resource that does not itself cache PROPFINDs, but needs to invalidate a parent
-    resource's PROPFIND cache by virtue of a change to its own childToken.
-    """
 
-    def changeCache(self):
-        if hasattr(self, 'cacheNotifier'):
-            return self.cacheNotifier.changed()
-        else:
-            self.log_debug("%r does not have a cacheNotifier but was changed" % (self,))
+    implements(IStoreNotifierFactory)
 
+    def newNotifier(self, storeObject):
+        return CacheStoreNotifier(self, storeObject)
 
 
-class CacheStoreNotifier(object):
+    def _newCacheToken(self):
+        return str(uuid.uuid4())
 
-    def __init__(self, resource):
-        self.resource = resource
 
+    def changed(self, cache_id):
+        """
+        Change the cache token for a store object.
 
-    def notify(self, op="update"):
-        self.resource.changeCache()
+        return: A L{Deferred} that fires when the token has been changed.
+        """
 
+        self.log_debug("Changing Cache Token for %r" % (cache_id,))
+        return self.getCachePool().set(
+            'cacheToken:%s' % (cache_id,),
+            self._newCacheToken(), expireTime=config.ResponseCacheTimeout * 60)
 
-    def clone(self, label="default", id=None):
-        return self
 
 
-    def getID(self, label="default"):
-        return None
+class CacheStoreNotifier(object):
+    """
+    A notifier for store objects. Store objects will call .notify() when they change.
+    """
 
+    implements(IStoreNotifier)
 
-    def nodeName(self, label="default"):
-        return succeed(None)
+    def __init__(self, notifierFactory, storeObject):
+        self._notifierFactory = notifierFactory
+        self._storeObject = storeObject
+
+
+    def notify(self):
+        """
+        We need to convert the store object notifier ID into a URI, since the cache uses URIs.
+        """
+
+        prefix, id = self._storeObject.notifierID()
+        if prefix == "CalDAV":
+            uri = "/calendars/__uids__/%s/" % (id,)
+        elif prefix == "CardDAV":
+            uri = "/addressbooks/__uids__/%s/" % (id,)
+        uri = urllib.quote(uri)
+        return self._notifierFactory.changed(uri)
+
+
+    def clone(self, storeObject):
+        return self.__class__(self._notifierFactory, storeObject)

Modified: CalendarServer/trunk/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/principal.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/twistedcaldav/directory/principal.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -899,6 +899,10 @@
         return self.principalURL()
 
 
+    def notifierID(self):
+        return self.principalURL()
+
+
     @inlineCallbacks
     def isProxyFor(self, principal):
         """

Modified: CalendarServer/trunk/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/extensions.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/twistedcaldav/extensions.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -211,7 +211,7 @@
                 resource = principalCollection.principalForRecord(record)
                 if resource:
                     matchingResources.append(resource)
-    
+
                     # We've determined this is a matching resource
                     matchcount += 1
                     if clientLimit is not None and matchcount >= clientLimit:
@@ -429,7 +429,7 @@
             whenAllProperties = gatherResults([
                 maybeDeferred(self.resource.readProperty, qn, request)
                 .addCallback(lambda p, iqn=qn: (p.sname(), p.toxml())
-                             if p is not None else (encodeXMLName(*iqn), None) )
+                             if p is not None else (encodeXMLName(*iqn), None))
                 .addErrback(gotError, encodeXMLName(*qn))
                 for qn in sorted(qnames)
             ])
@@ -537,29 +537,28 @@
 
 def updateCacheTokenOnCallback(f):
     def wrapper(self, *args, **kwargs):
-        if hasattr(self, "cacheNotifier"):
+        if hasattr(self, "notifyChanged"):
             def updateToken(response):
-                d = self.cacheNotifier.changed()
+                d = self.notifyChanged()
                 d.addCallback(lambda _: response)
                 return d
 
             d = maybeDeferred(f, self, *args, **kwargs)
-
-            if hasattr(self, "cacheNotifier"):
-                d.addCallback(updateToken)
-
+            d.addCallback(updateToken)
             return d
         else:
             return f(self, *args, **kwargs)
 
     return wrapper
 
+
+
 class DAVResource (DirectoryPrincipalPropertySearchMixIn,
                    SuperDAVResource, LoggingMixIn,
                    DirectoryRenderingMixIn, StaticRenderMixin):
     """
     Extended L{twext.web2.dav.resource.DAVResource} implementation.
-    
+
     Note we add StaticRenderMixin as a base class because we need all the etag etc behavior
     that is currently in static.py but is actually applicable to any type of resource.
     """
@@ -578,12 +577,12 @@
     def http_ACL(self, request):
         return super(DAVResource, self).http_ACL(request)
 
-    
     http_REPORT = http_REPORT
 
     def davComplianceClasses(self):
         return ("1", "access-control") # Add "2" when we have locking
 
+
     def render(self, request):
         if not self.exists():
             return responsecode.NOT_FOUND
@@ -592,15 +591,19 @@
             return self.renderDirectory(request)
         return super(DAVResource, self).render(request)
 
+
     def resourceType(self):
         # Allow live property to be overridden by dead property
         if self.deadProperties().contains((dav_namespace, "resourcetype")):
             return self.deadProperties().get((dav_namespace, "resourcetype"))
         return element.ResourceType(element.Collection()) if self.isCollection() else element.ResourceType()
 
+
     def contentType(self):
         return MimeType("httpd", "unix-directory") if self.isCollection() else None
 
+
+
 class DAVResourceWithChildrenMixin (object):
     """
     Bits needed from twext.web2.static
@@ -679,11 +682,14 @@
         self.putChildren = {}
         super(DAVResourceWithChildrenMixin, self).__init__(principalCollections=principalCollections)
 
+
     def findChildren(
         self, depth, request, callback,
         privileges=None, inherited_aces=None
     ):
         return succeed(None)
+
+
     def locateChild(self, request, segments):
         return self, server.StopTraversal
 
@@ -713,6 +719,7 @@
             return self.renderDirectory(request)
         return super(DAVResource, self).render(request)
 
+
     @inlineCallbacks
     def readProperty(self, property, request):
         if type(property) is tuple:
@@ -749,23 +756,26 @@
                         "Property %s does not exist." % (qname,)
                     ))
 
-
-
         result = (yield super(DAVPrincipalResource, self).readProperty(property, request))
         returnValue(result)
 
+
     def groupMembers(self):
         return succeed(())
 
+
     def expandedGroupMembers(self):
         return succeed(())
 
+
     def groupMemberships(self):
         return succeed(())
 
+
     def expandedGroupMemberships(self):
         return succeed(())
 
+
     def resourceType(self):
         # Allow live property to be overridden by dead property
         if self.deadProperties().contains((dav_namespace, "resourcetype")):
@@ -790,6 +800,7 @@
             return element.ResourceType.collection #@UndefinedVariable
         return element.ResourceType.empty #@UndefinedVariable
 
+
     def render(self, request):
         if not self.fp.exists():
             return responsecode.NOT_FOUND
@@ -797,7 +808,7 @@
         if self.fp.isdir():
             if request.path[-1] != "/":
                 # Redirect to include trailing '/' in URI
-                return RedirectResponse(request.unparseURL(path=urllib.quote(urllib.unquote(request.path), safe=':/')+'/'))
+                return RedirectResponse(request.unparseURL(path=urllib.quote(urllib.unquote(request.path), safe=':/') + '/'))
             else:
                 ifp = self.fp.childSearchPreauth(*self.indexNames)
                 if ifp:
@@ -844,9 +855,11 @@
         return self.readOnlyResponse
 
     http_DELETE = _forbidden
-    http_MOVE   = _forbidden
-    http_PUT    = _forbidden
+    http_MOVE = _forbidden
+    http_PUT = _forbidden
 
+
+
 class ReadOnlyResourceMixIn (ReadOnlyWritePropertiesResourceMixIn):
     """
     Read only resource.
@@ -856,13 +869,16 @@
     def writeProperty(self, property, request):
         raise HTTPError(self.readOnlyResponse)
 
+
     def accessControlList(
         self, request, inheritance=True, expanding=False, inherited_aces=None
     ):
-        # Permissions here are fixed, and are not subject to                    
-        # inheritance rules, etc.                                               
+        # Permissions here are fixed, and are not subject to
+        # inheritance rules, etc.
         return succeed(self.defaultAccessControlList())
 
+
+
 class PropertyNotFoundError (HTTPError):
     def __init__(self, qname):
         HTTPError.__init__(self,
@@ -872,6 +888,8 @@
             )
         )
 
+
+
 class CachingPropertyStore (LoggingMixIn):
     """
     DAV property store using a dict in memory on top of another
@@ -881,11 +899,12 @@
         self.propertyStore = propertyStore
         self.resource = propertyStore.resource
 
+
     def get(self, qname, uid=None):
         #self.log_debug("Get: %r, %r" % (self.resource.fp.path, qname))
 
         cache = self._cache()
-        
+
         cachedQname = qname + (uid,)
 
         if cachedQname in cache:
@@ -903,6 +922,7 @@
         else:
             raise PropertyNotFoundError(qname)
 
+
     def set(self, property, uid=None):
         #self.log_debug("Set: %r, %r" % (self.resource.fp.path, property))
 
@@ -914,6 +934,7 @@
         self.propertyStore.set(property, uid)
         cache[cachedQname] = property
 
+
     def contains(self, qname, uid=None):
         #self.log_debug("Contains: %r, %r" % (self.resource.fp.path, qname))
 
@@ -933,6 +954,7 @@
         else:
             return False
 
+
     def delete(self, qname, uid=None):
         #self.log_debug("Delete: %r, %r" % (self.resource.fp.path, qname))
 
@@ -943,11 +965,12 @@
 
         self.propertyStore.delete(qname, uid)
 
+
     def list(self, uid=None, filterByUID=True):
         #self.log_debug("List: %r" % (self.resource.fp.path,))
         keys = self._cache().iterkeys()
         if filterByUID:
-            return [ 
+            return [
                 (namespace, name)
                 for namespace, name, propuid in keys
                 if propuid == uid
@@ -955,6 +978,7 @@
         else:
             return keys
 
+
     def _cache(self):
         if not hasattr(self, "_data"):
             #self.log_debug("Cache init: %r" % (self.resource.fp.path,))
@@ -964,6 +988,8 @@
             )
         return self._data
 
+
+
 def extractCalendarServerPrincipalSearchData(doc):
     """
     Extract relevant info from a CalendarServerPrincipalSearch document
@@ -1001,4 +1027,3 @@
                 raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))
 
     return tokens, context, applyTo, clientLimit, propElement
-

Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -59,8 +59,7 @@
 
 from twistedcaldav import caldavxml, customxml
 from twistedcaldav import carddavxml
-from twistedcaldav.cache import PropfindCacheMixin, DisabledCacheNotifier, \
-    CacheStoreNotifier
+from twistedcaldav.cache import PropfindCacheMixin
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.carddavxml import carddav_namespace
 from twistedcaldav.config import config
@@ -206,12 +205,11 @@
 def updateCacheTokenOnCallback(f):
     def fun(self, *args, **kwargs):
         def _updateToken(response):
-            return self.cacheNotifier.changed().addCallback(
-                lambda _: response)
+            return self.notifyChanged().addCallback(lambda _: response)
 
         d = maybeDeferred(f, self, *args, **kwargs)
 
-        if hasattr(self, 'cacheNotifier'):
+        if hasattr(self, 'notifyChanged'):
             d.addCallback(_updateToken)
 
         return d
@@ -579,14 +577,11 @@
 
                 # FIXME: is there a better way to get back to the associated
                 # datastore object?
-                dataObject = None
-                if hasattr(self, "_newStoreObject"):
-                    dataObject = getattr(self, "_newStoreObject")
-                if dataObject:
-                    label = "collection" if self.isShareeCollection() else "default"
-                    nodeName = (yield dataObject.nodeName(label=label))
-                    if nodeName:
-                        propVal = customxml.PubSubXMPPPushKeyProperty(nodeName)
+                dataObject = getattr(self, "_newStoreObject")
+                if dataObject is not None:
+                    notifier = dataObject.getNotifier("push")
+                    if notifier is not None:
+                        propVal = customxml.PubSubXMPPPushKeyProperty(notifier.nodeName())
                         returnValue(propVal)
 
                 returnValue(customxml.PubSubXMPPPushKeyProperty())
@@ -2009,7 +2004,6 @@
         server has inserted into this L{CommonHomeResource}.
     @type _provisionedLinks: L{dict} mapping L{bytes} to L{Resource}
     """
-    cacheNotifierFactory = DisabledCacheNotifier
 
     def __init__(self, parent, name, transaction, home):
         self.parent = parent
@@ -2019,8 +2013,6 @@
         self._provisionedLinks = {}
         self._setupProvisions()
         self._newStoreHome = home
-        self.cacheNotifier = self.cacheNotifierFactory(self)
-        self._newStoreHome.addNotifier(CacheStoreNotifier(self))
         CalDAVResource.__init__(self)
 
         from twistedcaldav.storebridge import _NewStorePropertiesWrapper
@@ -2242,7 +2234,8 @@
 
             if config.Notifications.Services.APNS.Enabled:
 
-                nodeName = (yield self._newStoreHome.nodeName())
+                notifier = self._newStoreHome.getNotifier("push")
+                nodeName = notifier.nodeName() if notifier is not None else None
                 if nodeName:
                     notifierID = self._newStoreHome.notifierID()
                     if notifierID:
@@ -2277,9 +2270,9 @@
         elif qname == (customxml.calendarserver_namespace, "pushkey"):
             if (config.Notifications.Services.AMP.Enabled or
                 config.Notifications.Services.APNS.Enabled):
-                nodeName = (yield self._newStoreHome.nodeName())
-                if nodeName:
-                    returnValue(customxml.PubSubXMPPPushKeyProperty(nodeName))
+                notifier = self._newStoreHome.getNotifier("push")
+                if notifier is not None:
+                    returnValue(customxml.PubSubXMPPPushKeyProperty(notifier.nodeName()))
             returnValue(None)
 
         returnValue((yield super(CommonHomeResource, self).readProperty(property, request)))
@@ -2368,8 +2361,8 @@
         return self._newStoreHome.created() if self._newStoreHome else None
 
 
-    def notifierID(self, label="default"):
-        self._newStoreHome.notifierID(label)
+    def notifierID(self):
+        return "%s/%s" % self._newStoreHome.notifierID()
 
 
     def notifyChanged(self):

Modified: CalendarServer/trunk/twistedcaldav/scheduling_store/caldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling_store/caldav/resource.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/twistedcaldav/scheduling_store/caldav/resource.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -263,9 +263,12 @@
         prop_to_set = customxml.ScheduleDefaultTasksURL if tasks else caldavxml.ScheduleDefaultCalendarURL
 
         # This property now comes direct from the calendar home new store object
-        default = (yield self.parent._newStoreHome.defaultCalendar(componentType))
-        defaultURL = joinURL(self.parent.url(), default.name())
-        returnValue(prop_to_set(davxml.HRef(defaultURL)))
+        default = (yield self.parent._newStoreHome.defaultCalendar(componentType, create=False))
+        if default is None:
+            returnValue(prop_to_set())
+        else:
+            defaultURL = joinURL(self.parent.url(), default.name())
+            returnValue(prop_to_set(davxml.HRef(defaultURL)))
 
 
     @inlineCallbacks
@@ -315,10 +318,11 @@
         """
 
         # This property now comes direct from the calendar home new store object
-        default = (yield self.parent._newStoreHome.defaultCalendar(componentType))
+        default = (yield self.parent._newStoreHome.defaultCalendar(componentType, create=False))
 
         # Need L{DAVResource} object to return not new store object
-        default = (yield request.locateResource(joinURL(self.parent.url(), default.name())))
+        if default is not None:
+            default = (yield request.locateResource(joinURL(self.parent.url(), default.name())))
 
         returnValue(default)
 

Modified: CalendarServer/trunk/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/storebridge.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/twistedcaldav/storebridge.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -38,8 +38,6 @@
 from twisted.python.util import FancyEqMixin
 
 from twistedcaldav import customxml, carddavxml, caldavxml
-from twistedcaldav.cache import CacheStoreNotifier, ResponseCacheMixin, \
-    DisabledCacheNotifier
 from twistedcaldav.caldavxml import caldav_namespace, MaxAttendeesPerInstance, \
     MaxInstances, NoUIDConflict
 from twistedcaldav.carddavxml import carddav_namespace, NoUIDConflict as NovCardUIDConflict
@@ -218,13 +216,12 @@
 
 
 
-class _CommonHomeChildCollectionMixin(ResponseCacheMixin):
+class _CommonHomeChildCollectionMixin(object):
     """
     Methods for things which are like calendars.
     """
 
     _childClass = None
-    cacheNotifierFactory = DisabledCacheNotifier
 
     def _initializeWithHomeChild(self, child, home):
         """
@@ -242,9 +239,6 @@
         self._dead_properties = _NewStorePropertiesWrapper(
             self._newStoreObject.properties()
         ) if self._newStoreObject else NonePropertyStore(self)
-        if self._newStoreObject:
-            self.cacheNotifier = self.cacheNotifierFactory(self)
-            self._newStoreObject.addNotifier(CacheStoreNotifier(self))
 
 
     def liveProperties(self):
@@ -972,8 +966,8 @@
             )
 
 
-    def notifierID(self, label="default"):
-        self._newStoreObject.notifierID(label)
+    def notifierID(self):
+        return "%s/%s" % self._newStoreObject.notifierID()
 
 
     def notifyChanged(self):
@@ -1231,44 +1225,6 @@
             returnValue(None)
 
 
-    @inlineCallbacks
-    def storeRemove(self, request):
-        """
-        Delete this calendar collection resource, first deleting each contained
-        calendar resource.
-
-        This has to emulate the behavior in fileop.delete in that any errors
-        need to be reported back in a multistatus response.
-
-        @param request: The request used to locate child resources.  Note that
-            this is the request which I{triggered} the C{DELETE}, but which may
-            not actually be a C{DELETE} request itself.
-
-        @type request: L{twext.web2.iweb.IRequest}
-
-        @return: an HTTP response suitable for sending to a client (or
-            including in a multi-status).
-
-        @rtype: something adaptable to L{twext.web2.iweb.IResponse}
-        """
-
-        # Not allowed to delete the default calendar
-        default = (yield self.isDefaultCalendar(request))
-        if default:
-            log.err("Cannot DELETE default calendar: %s" % (self,))
-            raise HTTPError(ErrorResponse(
-                FORBIDDEN,
-                (caldav_namespace, "default-calendar-delete-allowed",),
-                "Cannot delete default calendar",
-            ))
-
-        response = (
-            yield super(CalendarCollectionResource, self).storeRemove(request)
-        )
-
-        returnValue(response)
-
-
     # FIXME: access control
     @inlineCallbacks
     def http_MOVE(self, request):
@@ -3227,15 +3183,11 @@
 
 
 
-class StoreNotificationCollectionResource(_NotificationChildHelper,
-                                          NotificationCollectionResource,
-                                          ResponseCacheMixin):
+class StoreNotificationCollectionResource(_NotificationChildHelper, NotificationCollectionResource):
     """
     Wrapper around a L{txdav.caldav.icalendar.ICalendar}.
     """
 
-    cacheNotifierFactory = DisabledCacheNotifier
-
     def __init__(self, notifications, homeResource, home, *args, **kw):
         """
         Create a CalendarCollectionResource from a L{txdav.caldav.icalendar.ICalendar}
@@ -3244,9 +3196,6 @@
         super(StoreNotificationCollectionResource, self).__init__(*args, **kw)
         self._initializeWithNotifications(notifications, home)
         self._parentResource = homeResource
-        if self._newStoreNotifications:
-            self.cacheNotifier = self.cacheNotifierFactory(self)
-            self._newStoreNotifications.addNotifier(CacheStoreNotifier(self))
 
 
     def name(self):

Modified: CalendarServer/trunk/twistedcaldav/test/test_link.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_link.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/twistedcaldav/test/test_link.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -26,30 +26,45 @@
 
 
 class StubProperty(object):
+
     def qname(self):
         return "StubQnamespace", "StubQname"
 
+
+
 class StubHome(object):
+
     def properties(self):
         return []
 
+
     def calendarWithName(self, name):
         return succeed(None)
-    
-    def addNotifier(self, notifier):
+
+
+    def addNotifier(self, factory_name, notifier):
         pass
 
+
+
 class StubCalendarHomeResource(CalendarHomeResource):
+
     def principalForRecord(self):
         return None
 
+
+
 class StubShare(object):
+
     def __init__(self, link):
         self.hosturl = link
 
+
     def url(self):
         return self.hosturl
 
+
+
 class LinkResourceTests(TestCase):
 
     @inlineCallbacks
@@ -60,9 +75,10 @@
         resource.putChild("link", link)
 
         request = SimpleRequest(self.site, "GET", "/home/link/")
-        linked_to, _ignore = (yield resource.locateChild(request, ["link",]))
+        linked_to, _ignore = (yield resource.locateChild(request, ["link", ]))
         self.assertTrue(linked_to is resource.getChild("outbox"))
 
+
     @inlineCallbacks
     def test_badLink(self):
         resource = CalendarHomeResource(self.site.resource, "home", object(), StubHome())
@@ -72,12 +88,13 @@
 
         request = SimpleRequest(self.site, "GET", "/home/link/")
         try:
-            yield resource.locateChild(request, ["link",])
+            yield resource.locateChild(request, ["link", ])
         except HTTPError, e:
             self.assertEqual(e.response.code, responsecode.NOT_FOUND)
         else:
             self.fail("HTTPError exception not raised")
 
+
     @inlineCallbacks
     def test_recursiveLink(self):
         resource = CalendarHomeResource(self.site.resource, "home", object(), StubHome())
@@ -89,7 +106,7 @@
 
         request = SimpleRequest(self.site, "GET", "/home/link1/")
         try:
-            yield resource.locateChild(request, ["link1",])
+            yield resource.locateChild(request, ["link1", ])
         except HTTPError, e:
             self.assertEqual(e.response.code, responsecode.LOOP_DETECTED)
         else:

Modified: CalendarServer/trunk/twistedcaldav/test/test_resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_resource.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/twistedcaldav/test/test_resource.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -41,7 +41,7 @@
         return []
 
 
-    def addNotifier(self, notifier):
+    def addNotifier(self, factory_name, notifier):
         pass
 
 

Modified: CalendarServer/trunk/txdav/caldav/datastore/file.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/file.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/caldav/datastore/file.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -84,9 +84,8 @@
     _topPath = "calendars"
     _notifierPrefix = "CalDAV"
 
-    def __init__(self, uid, path, calendarStore, transaction, notifiers):
-        super(CalendarHome, self).__init__(uid, path, calendarStore,
-                                           transaction, notifiers)
+    def __init__(self, uid, path, calendarStore, transaction):
+        super(CalendarHome, self).__init__(uid, path, calendarStore, transaction)
 
         self._childClass = Calendar
 

Modified: CalendarServer/trunk/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/sql.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/caldav/datastore/sql.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -339,10 +339,10 @@
 
     _cacher = Memcacher("SQL.calhome", pickle=True, key_normalization=False)
 
-    def __init__(self, transaction, ownerUID, notifiers):
+    def __init__(self, transaction, ownerUID):
 
         self._childClass = Calendar
-        super(CalendarHome, self).__init__(transaction, ownerUID, notifiers)
+        super(CalendarHome, self).__init__(transaction, ownerUID)
 
 
     @classmethod
@@ -554,11 +554,16 @@
         # Check whether components type must be separate
         if config.RestrictCalendarsToOneComponentType:
             yield defaultCal.setSupportedComponents("VEVENT")
+            yield self.setDefaultCalendar(defaultCal, False)
 
             # Default tasks
             defaultTasks = yield self.createCalendarWithName("tasks")
             yield defaultTasks.setSupportedComponents("VTODO")
             yield defaultTasks.setUsedForFreeBusy(False)
+            yield self.setDefaultCalendar(defaultTasks, True)
+        else:
+            yield self.setDefaultCalendar(defaultCal, False)
+            yield self.setDefaultCalendar(defaultCal, True)
 
         inbox = yield self.createCalendarWithName("inbox")
         yield inbox.setUsedForFreeBusy(False)
@@ -662,6 +667,7 @@
             Where=chm.RESOURCE_ID == self._resourceID,
         ).on(self._txn)
         yield self.invalidateQueryCache()
+        yield self.notifyChanged()
 
         returnValue(defaultCalendar)
 
@@ -697,13 +703,23 @@
             Where=chm.RESOURCE_ID == self._resourceID,
         ).on(self._txn)
         yield self.invalidateQueryCache()
+        yield self.notifyChanged()
 
 
     @inlineCallbacks
-    def defaultCalendar(self, componentType):
+    def defaultCalendar(self, componentType, create=True):
         """
         Find the default calendar for the supplied iCalendar component type. If one does
         not exist, automatically provision it.
+
+        @param componentType: the name of the iCalendar component for the default, i.e. "VEVENT" or "VTODO"
+        @type componentType: C{str}
+        @param create: if C{True} and no default is found, create a calendar and make it the default, if C{False}
+            and there is no default, then return C{None}
+        @type create: C{bool}
+
+        @return: the default calendar or C{None} if none found and creation not requested
+        @rtype: L{Calendar} or C{None}
         """
 
         # Check any default calendar property first - this will create if none exists
@@ -741,9 +757,12 @@
 
             # If none can be found, provision one
             if default is None:
-                new_name = "%ss" % (componentType.lower()[1:],)
-                default = yield self.createCalendarWithName(new_name)
-                yield default.setSupportedComponents(componentType.upper())
+                if not create:
+                    returnValue(None)
+                else:
+                    new_name = "%ss" % (componentType.lower()[1:],)
+                    default = yield self.createCalendarWithName(new_name)
+                    yield default.setSupportedComponents(componentType.upper())
 
             # Update the metadata
             chm = self._homeMetaDataSchema
@@ -788,6 +807,7 @@
         return str(prop) if prop is not None else None
 
 
+    @inlineCallbacks
     def setDefaultAlarm(self, alarm, vevent, timed):
         """
         Set default alarm of the specified type.
@@ -806,6 +826,7 @@
             prop = caldavxml.DefaultAlarmVToDoDateTime if timed else caldavxml.DefaultAlarmVToDoDate
 
         self.properties()[PropertyName.fromElement(prop)] = prop.fromString(alarm)
+        yield self.notifyChanged()
 
 
 CalendarHome._register(ECALENDARTYPE)
@@ -975,6 +996,7 @@
         ).on(self._txn)
         self._supportedComponents = supported_components
         yield self.invalidateQueryCache()
+        yield self.notifyChanged()
 
 
     def getSupportedComponents(self):
@@ -1013,6 +1035,7 @@
             return str(prop)
 
 
+    @inlineCallbacks
     def setDefaultAlarm(self, alarm, vevent, timed):
         """
         Set default alarm of the specified type.
@@ -1031,6 +1054,7 @@
             prop = caldavxml.DefaultAlarmVToDoDateTime if timed else caldavxml.DefaultAlarmVToDoDate
 
         self.properties()[PropertyName.fromElement(prop)] = prop.fromString(alarm)
+        yield self.notifyChanged()
 
 
     def isInbox(self):

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/common.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/common.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/common.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -453,11 +453,12 @@
         yield self.commit()
 
         # Make sure notification fired after commit
-        self.assertEquals(self.notifierFactory.history,
-            [
-                "CalDAV|home1",
-                "CalDAV|home1/notification",
-            ]
+        self.assertEquals(
+            set(self.notifierFactory.history),
+            set([
+                "/CalDAV/example.com/home1/",
+                "/CalDAV/example.com/home1/notification/",
+            ])
         )
 
         notifications = yield self.transactionUnderTest().notificationsWithUID(
@@ -471,11 +472,12 @@
         yield self.commit()
 
         # Make sure notification fired after commit
-        self.assertEquals(self.notifierFactory.history,
-            [
-                "CalDAV|home1",
-                "CalDAV|home1/notification",
-            ]
+        self.assertEquals(
+            set(self.notifierFactory.history),
+            set([
+                "/CalDAV/example.com/home1/",
+                "/CalDAV/example.com/home1/notification/",
+            ])
         )
 
 
@@ -533,20 +535,12 @@
     @inlineCallbacks
     def test_notifierID(self):
         home = yield self.homeUnderTest()
-        self.assertEquals(home.notifierID(), "CalDAV|home1")
+        self.assertEquals(home.notifierID(), ("CalDAV", "home1",))
         calendar = yield home.calendarWithName("calendar_1")
-        self.assertEquals(calendar.notifierID(), "CalDAV|home1")
-        self.assertEquals(calendar.notifierID(label="collection"), "CalDAV|home1/calendar_1")
+        self.assertEquals(calendar.notifierID(), ("CalDAV", "home1/calendar_1",))
 
 
     @inlineCallbacks
-    def test_nodeNameSuccess(self):
-        home = yield self.homeUnderTest()
-        name = yield home.nodeName()
-        self.assertEquals(name, "/CalDAV/example.com/home1/")
-
-
-    @inlineCallbacks
     def test_displayNameNone(self):
         """
         L{ICalendarHome.calendarWithName} returns C{None} for calendars which
@@ -706,7 +700,7 @@
         yield self.commit()
 
         # Make sure notification fired after commit
-        self.assertTrue("CalDAV|home1" in self.notifierFactory.history)
+        self.assertTrue("/CalDAV/example.com/home1/" in self.notifierFactory.history)
 
         # Make sure it's available in a new transaction; i.e. test the commit.
         home = yield self.homeUnderTest()
@@ -746,15 +740,13 @@
 
         # Make sure notification fired after commit
         self.assertEquals(
-            self.notifierFactory.history,
-            [
-                "CalDAV|home1",
-                "CalDAV|home1/calendar_1",
-                "CalDAV|home1",
-                "CalDAV|home1/calendar_2",
-                "CalDAV|home1",
-                "CalDAV|home1/calendar_empty",
-            ]
+            set(self.notifierFactory.history),
+            set([
+                "/CalDAV/example.com/home1/",
+                "/CalDAV/example.com/home1/calendar_1/",
+                "/CalDAV/example.com/home1/calendar_2/",
+                "/CalDAV/example.com/home1/calendar_empty/",
+            ])
         )
 
 
@@ -926,11 +918,11 @@
         # Make sure notifications are fired after commit
         yield self.commit()
         self.assertEquals(
-            self.notifierFactory.history,
-            [
-                "CalDAV|home1",
-                "CalDAV|home1/calendar_1",
-            ]
+            set(self.notifierFactory.history),
+            set([
+                "/CalDAV/example.com/home1/",
+                "/CalDAV/example.com/home1/calendar_1/",
+            ])
         )
 
 
@@ -1096,17 +1088,15 @@
         yield self.commit()
 
         home = yield self.homeUnderTest()
-        self.assertEquals(home.notifierID(), "CalDAV|home1")
+        self.assertEquals(home.notifierID(), ("CalDAV", "home1",))
         calendar = yield home.calendarWithName("calendar_1")
-        self.assertEquals(calendar.notifierID(), "CalDAV|home1")
-        self.assertEquals(calendar.notifierID(label="collection"), "CalDAV|home1/calendar_1")
+        self.assertEquals(calendar.notifierID(), ("CalDAV", "home1/calendar_1",))
         yield self.commit()
 
         home = yield self.homeUnderTest(name=OTHER_HOME_UID)
-        self.assertEquals(home.notifierID(), "CalDAV|%s" % (OTHER_HOME_UID,))
+        self.assertEquals(home.notifierID(), ("CalDAV", OTHER_HOME_UID,))
         calendar = yield home.calendarWithName(self.sharedName)
-        self.assertEquals(calendar.notifierID(), "CalDAV|home1")
-        self.assertEquals(calendar.notifierID(label="collection"), "CalDAV|home1/calendar_1")
+        self.assertEquals(calendar.notifierID(), ("CalDAV", "home1/calendar_1",))
         yield self.commit()
 
 
@@ -1486,11 +1476,11 @@
 
         # Make sure notifications fire after commit
         self.assertEquals(
-            self.notifierFactory.history,
-            [
-                "CalDAV|home1",
-                "CalDAV|home1/calendar_1",
-            ]
+            set(self.notifierFactory.history),
+            set([
+                "/CalDAV/example.com/home1/",
+                "/CalDAV/example.com/home1/calendar_1/",
+            ])
         )
 
 
@@ -1606,11 +1596,11 @@
 
         # Make sure notification fired after commit
         self.assertEquals(
-            self.notifierFactory.history,
-            [
-                "CalDAV|home1",
-                "CalDAV|home1/calendar_1",
-            ]
+            set(self.notifierFactory.history),
+            set([
+                "/CalDAV/example.com/home1/",
+                "/CalDAV/example.com/home1/calendar_1/",
+            ])
         )
 
 

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_file.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_file.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_file.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -78,9 +78,12 @@
                             resource.setContent(resource.getContent() % {"now": nowYear})
 
     testID = test.id()
-    test.calendarStore = CalendarStore(storeRootPath, test.notifierFactory,
-                                       buildDirectory(),
-                                       quota=deriveQuota(test))
+    test.calendarStore = CalendarStore(
+        storeRootPath,
+        {"push": test.notifierFactory} if test.notifierFactory else {},
+        buildDirectory(),
+        quota=deriveQuota(test),
+    )
     test.txn = test.calendarStore.newTransaction(testID + "(old)")
     assert test.calendarStore is not None, "No calendar store?"
 

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_implicit.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_implicit.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_implicit.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -766,7 +766,7 @@
 """
 
         home = (yield self.homeUnderTest(name="user01"))
-        home.setDefaultAlarm(alarm, True, True)
+        yield home.setDefaultAlarm(alarm, True, True)
         yield self.commit()
 
         data1 = """BEGIN:VCALENDAR
@@ -810,7 +810,7 @@
 """
 
         home = (yield self.homeUnderTest(name="user02"))
-        home.setDefaultAlarm(alarm, True, True)
+        yield home.setDefaultAlarm(alarm, True, True)
         yield self.commit()
 
         # user01 shares calendar with user02

Modified: CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py
===================================================================
--- CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/caldav/datastore/test/test_sql.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -13,9 +13,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
-from txdav.caldav.datastore.test.util import buildCalendarStore
-from txdav.caldav.icalendarstore import ComponentUpdateState, \
-    InvalidDefaultCalendar
 
 """
 Tests for txdav.caldav.datastore.postgres, mostly based on
@@ -44,7 +41,9 @@
 from txdav.caldav.datastore.test.common import CommonTests as CalendarCommonTests, \
     test_event_text
 from txdav.caldav.datastore.test.test_file import setUpCalendarStore
+from txdav.caldav.datastore.test.util import buildCalendarStore
 from txdav.caldav.datastore.util import _migrateCalendar, migrateHome
+from txdav.caldav.icalendarstore import ComponentUpdateState, InvalidDefaultCalendar
 from txdav.common.datastore.sql import ECALENDARTYPE, CommonObjectResource
 from txdav.common.datastore.sql_legacy import PostgresLegacyIndexEmulator
 from txdav.common.datastore.sql_tables import schema, _BIND_MODE_DIRECT, \
@@ -1154,7 +1153,7 @@
 
         home = yield self.transactionUnderTest().calendarHomeWithUID("home_defaults")
         self.assertEqual(home._default_events, default_events._resourceID)
-        self.assertEqual(home._default_tasks, default_tasks._resourceID)
+        self.assertEqual(home._default_tasks, None)
 
         default_tasks2 = yield home.defaultCalendar("VTODO")
         self.assertTrue(default_tasks2 is not None)
@@ -1212,6 +1211,44 @@
 
 
     @inlineCallbacks
+    def test_defaultCalendar_delete(self):
+        """
+        Make sure a default_events calendar is assigned after existing one is deleted.
+        """
+
+        home = yield self.homeUnderTest(name="home_defaults")
+        calendar1 = yield home.calendarWithName("calendar_1")
+        default_events = yield home.defaultCalendar("VEVENT")
+        self.assertTrue(default_events is not None)
+        self.assertEqual(home._default_events, calendar1._resourceID)
+        yield self.commit()
+
+        home = yield self.homeUnderTest(name="home_defaults")
+        calendar1 = yield home.calendarWithName("calendar_1")
+        yield calendar1.remove()
+        yield self.commit()
+
+        home = yield self.homeUnderTest(name="home_defaults")
+        self.assertEqual(home._default_events, None)
+        self.assertEqual(home._default_tasks, None)
+        calendars = yield home.listCalendars()
+        self.assertEqual(calendars, ["inbox", ])
+        yield self.commit()
+
+        home = yield self.homeUnderTest(name="home_defaults")
+        default_events = yield home.defaultCalendar("VEVENT")
+        self.assertTrue(default_events is not None)
+        yield self.commit()
+
+        home = yield self.homeUnderTest(name="home_defaults")
+        calendar1 = yield home.calendarWithName(default_events.name())
+        default_events = yield home.defaultCalendar("VEVENT")
+        self.assertTrue(default_events is not None)
+        self.assertEqual(home._default_events, calendar1._resourceID)
+        yield self.commit()
+
+
+    @inlineCallbacks
     def test_resourceLock(self):
         """
         Test CommonObjectResource.lock to make sure it locks, raises on missing resource,

Modified: CalendarServer/trunk/txdav/carddav/datastore/file.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/file.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/carddav/datastore/file.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -67,8 +67,8 @@
     _topPath = "addressbooks"
     _notifierPrefix = "CardDAV"
 
-    def __init__(self, uid, path, addressbookStore, transaction, notifiers):
-        super(AddressBookHome, self).__init__(uid, path, addressbookStore, transaction, notifiers)
+    def __init__(self, uid, path, addressbookStore, transaction):
+        super(AddressBookHome, self).__init__(uid, path, addressbookStore, transaction)
 
         self._childClass = AddressBook
 

Modified: CalendarServer/trunk/txdav/carddav/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/sql.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/carddav/datastore/sql.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -84,10 +84,10 @@
 
     _cacher = Memcacher("SQL.adbkhome", pickle=True, key_normalization=False)
 
-    def __init__(self, transaction, ownerUID, notifiers):
+    def __init__(self, transaction, ownerUID):
 
         self._childClass = AddressBook
-        super(AddressBookHome, self).__init__(transaction, ownerUID, notifiers)
+        super(AddressBookHome, self).__init__(transaction, ownerUID)
 
     addressbooks = CommonHome.children
     listAddressbooks = CommonHome.listChildren

Modified: CalendarServer/trunk/txdav/carddav/datastore/test/common.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/test/common.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/carddav/datastore/test/common.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -226,10 +226,9 @@
     @inlineCallbacks
     def test_notifierID(self):
         home = yield self.homeUnderTest()
-        self.assertEquals(home.notifierID(), "CardDAV|home1")
+        self.assertEquals(home.notifierID(), ("CardDAV", "home1",))
         addressbook = yield home.addressbookWithName("addressbook_1")
-        self.assertEquals(addressbook.notifierID(), "CardDAV|home1")
-        self.assertEquals(addressbook.notifierID(label="collection"), "CardDAV|home1/addressbook_1")
+        self.assertEquals(addressbook.notifierID(), ("CardDAV", "home1/addressbook_1",))
 
 
     @inlineCallbacks
@@ -320,7 +319,7 @@
         yield self.commit()
 
         # Make sure notification fired after commit
-        self.assertTrue("CardDAV|home1" in self.notifierFactory.history)
+        self.assertTrue("/CardDAV/example.com/home1/" in self.notifierFactory.history)
 
         # Make sure it's available in a new transaction; i.e. test the commit.
         home = yield self.homeUnderTest()
@@ -359,15 +358,15 @@
 
         # Make sure notification fired after commit
         self.assertEquals(
-            self.notifierFactory.history,
-            [
-                "CardDAV|home1",
-                "CardDAV|home1/addressbook_1",
-                "CardDAV|home1",
-                "CardDAV|home1/addressbook_2",
-                "CardDAV|home1",
-                "CardDAV|home1/addressbook_empty"
-            ]
+            set(self.notifierFactory.history),
+            set([
+                "/CardDAV/example.com/home1/",
+                "/CardDAV/example.com/home1/addressbook_1/",
+                "/CardDAV/example.com/home1/",
+                "/CardDAV/example.com/home1/addressbook_2/",
+                "/CardDAV/example.com/home1/",
+                "/CardDAV/example.com/home1/addressbook_empty/"
+            ])
         )
 
 
@@ -496,11 +495,11 @@
         # Make sure notifications are fired after commit
         yield self.commit()
         self.assertEquals(
-            self.notifierFactory.history,
-            [
-                "CardDAV|home1",
-                "CardDAV|home1/addressbook_1",
-            ]
+            set(self.notifierFactory.history),
+            set([
+                "/CardDAV/example.com/home1/",
+                "/CardDAV/example.com/home1/addressbook_1/",
+            ])
         )
 
 
@@ -674,11 +673,11 @@
 
         # Make sure notifications fire after commit
         self.assertEquals(
-            self.notifierFactory.history,
-            [
-                "CardDAV|home1",
-                "CardDAV|home1/addressbook_1",
-            ]
+            set(self.notifierFactory.history),
+            set([
+                "/CardDAV/example.com/home1/",
+                "/CardDAV/example.com/home1/addressbook_1/",
+            ])
         )
 
 
@@ -790,11 +789,11 @@
 
         # Make sure notification fired after commit
         self.assertEquals(
-            self.notifierFactory.history,
-            [
-                "CardDAV|home1",
-                "CardDAV|home1/addressbook_1",
-            ]
+            set(self.notifierFactory.history),
+            set([
+                "/CardDAV/example.com/home1/",
+                "/CardDAV/example.com/home1/addressbook_1/",
+            ])
         )
 
 

Modified: CalendarServer/trunk/txdav/carddav/datastore/test/test_file.py
===================================================================
--- CalendarServer/trunk/txdav/carddav/datastore/test/test_file.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/carddav/datastore/test/test_file.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -61,7 +61,7 @@
     storePath.copyTo(addressbookPath)
 
     test.notifierFactory = StubNotifierFactory()
-    test.addressbookStore = AddressBookStore(storeRootPath, test.notifierFactory, None)
+    test.addressbookStore = AddressBookStore(storeRootPath, {"push": test.notifierFactory}, None)
     test.txn = test.addressbookStore.newTransaction(test.id() + " (old)")
     assert test.addressbookStore is not None, "No addressbook store?"
 

Modified: CalendarServer/trunk/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/file.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/common/datastore/file.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -110,12 +110,16 @@
     """
     implements(ICalendarStore)
 
-    def __init__(self, path, notifierFactory,
-                 directoryService,
-                 enableCalendars=True,
-                 enableAddressBooks=True,
-                 quota=(2 ** 20),
-                 propertyStoreClass=XattrPropertyStore):
+    def __init__(
+        self,
+        path,
+        notifierFactories,
+        directoryService,
+        enableCalendars=True,
+        enableAddressBooks=True,
+        quota=(2 ** 20),
+        propertyStoreClass=XattrPropertyStore
+    ):
         """
         Create a store.
 
@@ -127,7 +131,7 @@
         self._directoryService = IStoreDirectoryService(directoryService) if directoryService is not None else None
         self.enableCalendars = enableCalendars
         self.enableAddressBooks = enableAddressBooks
-        self._notifierFactory = notifierFactory
+        self._notifierFactories = notifierFactories if notifierFactories is not None else {}
         self._transactionClass = CommonStoreTransaction
         self._propertyStoreClass = propertyStoreClass
         self.quota = quota
@@ -163,7 +167,7 @@
             name,
             self.enableCalendars,
             self.enableAddressBooks,
-            self._notifierFactory if self._enableNotifications else None,
+            self._notifierFactories if self._enableNotifications else None,
             self._migrating,
         )
         for callback in self._newTransactionCallbacks:
@@ -262,7 +266,7 @@
 
     _homeClass = {}
 
-    def __init__(self, dataStore, name, enableCalendars, enableAddressBooks, notifierFactory, migrating=False):
+    def __init__(self, dataStore, name, enableCalendars, enableAddressBooks, notifierFactories, migrating=False):
         """
         Initialize a transaction; do not call this directly, instead call
         L{DataStore.newTransaction}.
@@ -280,7 +284,7 @@
         self._calendarHomes = {}
         self._addressbookHomes = {}
         self._notificationHomes = {}
-        self._notifierFactory = notifierFactory
+        self._notifierFactories = notifierFactories
         self._notifiedAlready = set()
         self._bumpedAlready = set()
         self._migrating = migrating
@@ -596,12 +600,12 @@
     _topPath = None
     _notifierPrefix = None
 
-    def __init__(self, uid, path, dataStore, transaction, notifiers):
+    def __init__(self, uid, path, dataStore, transaction):
         self._dataStore = dataStore
         self._uid = uid
         self._path = path
         self._transaction = transaction
-        self._notifiers = notifiers
+        self._notifiers = None
         self._shares = SharedCollectionsDatabase(StubResource(self))
         self._newChildren = {}
         self._removedChildren = set()
@@ -683,13 +687,9 @@
         else:
             homePath = childPath
 
-        if txn._notifierFactory:
-            notifiers = (txn._notifierFactory.newNotifier(id=uid,
-                prefix=cls._notifierPrefix),)
-        else:
-            notifiers = None
-
-        home = cls(uid, homePath, txn._dataStore, txn, notifiers)
+        home = cls(uid, homePath, txn._dataStore, txn)
+        for factory_name, factory in txn._notifierFactories.items():
+            home.addNotifier(factory_name, factory.newNotifier(home))
         if creating:
             home.createdHome()
             if withNotifications:
@@ -928,28 +928,18 @@
         self.properties()[PropertyName.fromElement(TwistedQuotaUsedProperty)] = TwistedQuotaUsedProperty(str(new_used))
 
 
-    def addNotifier(self, notifier):
+    def addNotifier(self, factory_name, notifier):
         if self._notifiers is None:
-            self._notifiers = ()
-        self._notifiers += (notifier,)
+            self._notifiers = {}
+        self._notifiers[factory_name] = notifier
 
 
-    def notifierID(self, label="default"):
-        if self._notifiers:
-            return self._notifiers[0].getID(label)
-        else:
-            return None
+    def getNotifier(self, factory_name):
+        return self._notifiers.get(factory_name)
 
 
-    @inlineCallbacks
-    def nodeName(self, label="default"):
-        if self._notifiers:
-            for notifier in self._notifiers:
-                name = (yield notifier.nodeName(label=label))
-                if name is not None:
-                    returnValue(name)
-        else:
-            returnValue(None)
+    def notifierID(self):
+        return (self._notifierPrefix, self.uid(),)
 
 
     def notifyChanged(self):
@@ -959,7 +949,7 @@
 
         # Only send one set of change notifications per transaction
         if self._notifiers and not self._transaction.isNotifiedAlready(self):
-            for notifier in self._notifiers:
+            for notifier in self._notifiers.values():
                 self._transaction.postCommit(notifier.notify)
             self._transaction.notificationAddedForObject(self)
 
@@ -1004,12 +994,10 @@
         self._invites = None # Derived classes need to set this
         self._renamedName = realName
 
-        if home._notifiers:
-            childID = "%s/%s" % (home.uid(), name)
-            notifiers = [notifier.clone(label="collection", id=childID) for notifier in home._notifiers]
+        if self._home._notifiers:
+            self._notifiers = dict([(factory_name, notifier.clone(self),) for factory_name, notifier in self._home._notifiers.items()])
         else:
-            notifiers = None
-        self._notifiers = notifiers
+            self._notifiers = None
 
 
     @classmethod
@@ -1282,30 +1270,24 @@
         pass
 
 
-    def addNotifier(self, notifier):
+    def addNotifier(self, factory_name, notifier):
         if self._notifiers is None:
-            self._notifiers = ()
-        self._notifiers += (notifier,)
+            self._notifiers = {}
+        self._notifiers[factory_name] = notifier
 
 
-    def notifierID(self, label="default"):
-        if self._notifiers:
-            return self._notifiers[0].getID(label)
-        else:
-            return None
+    def getNotifier(self, factory_name):
+        return self._notifiers.get(factory_name)
 
 
-    @inlineCallbacks
-    def nodeName(self, label="default"):
-        if self._notifiers:
-            for notifier in self._notifiers:
-                name = (yield notifier.nodeName(label=label))
-                if name is not None:
-                    returnValue(name)
-        else:
-            returnValue(None)
+    def notifierID(self):
+        return (self.ownerHome()._notifierPrefix, "%s/%s" % (self.ownerHome().uid(), self.name(),),)
 
 
+    def parentNotifierID(self):
+        return self.ownerHome().notifierID()
+
+
     def notifyChanged(self):
         """
         Trigger a notification of a change
@@ -1313,7 +1295,7 @@
 
         # Only send one set of change notifications per transaction
         if self._notifiers and not self._transaction.isNotifiedAlready(self):
-            for notifier in self._notifiers:
+            for notifier in self._notifiers.values():
                 self._transaction.postCommit(notifier.notify)
             self._transaction.notificationAddedForObject(self)
 

Modified: CalendarServer/trunk/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/common/datastore/sql.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -117,8 +117,9 @@
     @ivar sqlTxnFactory: A 0-arg factory callable that produces an
         L{IAsyncTransaction}.
 
-    @ivar notifierFactory: a L{twistedcaldav.notify.NotifierFactory} (or
-        similar) that produces new notifiers for homes and collections.
+    @ivar _notifierFactories: a C{dict} of L{IStoreNotifierFactory} objects
+        from which the store can create notifiers for store objects. The keys
+        are "tokens" that determine the type of notifier.
 
     @ivar attachmentsPath: a L{FilePath} indicating a directory where
         attachments may be stored.
@@ -145,20 +146,31 @@
 
     implements(ICalendarStore)
 
-    def __init__(self, sqlTxnFactory, notifierFactory,
-                 directoryService,
-                 attachmentsPath, attachmentsURIPattern,
-                 enableCalendars=True, enableAddressBooks=True,
-                 enableManagedAttachments=True,
-                 label="unlabeled", quota=(2 ** 20),
-                 logLabels=False, logStats=False, logStatsLogFile=None, logSQL=False,
-                 logTransactionWaits=0, timeoutTransactions=0,
-                 cacheQueries=True, cachePool="Default",
-                 cacheExpireSeconds=3600):
+    def __init__(self,
+        sqlTxnFactory,
+        notifierFactories,
+        directoryService,
+        attachmentsPath,
+        attachmentsURIPattern,
+        enableCalendars=True,
+        enableAddressBooks=True,
+        enableManagedAttachments=True,
+        label="unlabeled",
+        quota=(2 ** 20),
+        logLabels=False,
+        logStats=False,
+        logStatsLogFile=None,
+        logSQL=False,
+        logTransactionWaits=0,
+        timeoutTransactions=0,
+        cacheQueries=True,
+        cachePool="Default",
+        cacheExpireSeconds=3600
+    ):
         assert enableCalendars or enableAddressBooks
 
         self.sqlTxnFactory = sqlTxnFactory
-        self.notifierFactory = notifierFactory
+        self._notifierFactories = notifierFactories if notifierFactories is not None else {}
         self._directoryService = IStoreDirectoryService(directoryService) if directoryService is not None else None
         self.attachmentsPath = attachmentsPath
         self.attachmentsURIPattern = attachmentsURIPattern
@@ -255,7 +267,7 @@
             self.sqlTxnFactory(),
             self.enableCalendars,
             self.enableAddressBooks,
-            self.notifierFactory if self._enableNotifications else None,
+            self._notifierFactories if self._enableNotifications else {},
             label,
             self._migrating,
             disableCache
@@ -433,12 +445,12 @@
 
     def __init__(self, store, sqlTxn,
                  enableCalendars, enableAddressBooks,
-                 notifierFactory, label, migrating=False, disableCache=False):
+                 notifierFactories, label, migrating=False, disableCache=False):
         self._store = store
         self._calendarHomes = {}
         self._addressbookHomes = {}
         self._notificationHomes = {}
-        self._notifierFactory = notifierFactory
+        self._notifierFactories = notifierFactories
         self._notifiedAlready = set()
         self._bumpedAlready = set()
         self._label = label
@@ -867,11 +879,11 @@
 
 
     def isNotifiedAlready(self, obj):
-        return obj in self._notifiedAlready
+        return obj.id() in self._notifiedAlready
 
 
     def notificationAddedForObject(self, obj):
-        self._notifiedAlready.add(obj)
+        self._notifiedAlready.add(obj.id())
 
 
     def isBumpedAlready(self, obj):
@@ -880,7 +892,7 @@
         called for the given object, in order to facilitate calling
         bumpModified only once per object.
         """
-        return obj in self._bumpedAlready
+        return obj.id() in self._bumpedAlready
 
 
     def bumpAddedForObject(self, obj):
@@ -889,7 +901,7 @@
         done, in order to facilitate calling bumpModified only once per
         object.
         """
-        self._bumpedAlready.add(obj)
+        self._bumpedAlready.add(obj.id())
 
     _savepointCounter = 0
 
@@ -1400,13 +1412,13 @@
 
     _cacher = None  # Initialize in derived classes
 
-    def __init__(self, transaction, ownerUID, notifiers):
+    def __init__(self, transaction, ownerUID):
         self._txn = transaction
         self._ownerUID = ownerUID
         self._resourceID = None
         self._childrenLoaded = False
         self._children = {}
-        self._notifiers = notifiers
+        self._notifiers = None
         self._quotaUsedBytes = None
         self._created = None
         self._modified = None
@@ -1551,13 +1563,9 @@
     @classmethod
     @inlineCallbacks
     def homeWithUID(cls, txn, uid, create=False):
-        if txn._notifierFactory:
-            notifiers = (txn._notifierFactory.newNotifier(
-                id=uid, prefix=cls._notifierPrefix
-            ),)
-        else:
-            notifiers = None
-        homeObject = cls(txn, uid, notifiers)
+        homeObject = cls(txn, uid)
+        for factory_type, factory in txn._notifierFactories.items():
+            homeObject.addNotifier(factory_type, factory.newNotifier(homeObject))
         homeObject = (yield homeObject.initFromStore())
         if homeObject is not None:
             returnValue(homeObject)
@@ -1585,7 +1593,9 @@
                 yield savepoint.rollback(txn)
 
                 # Retry the query - row may exist now, if not re-raise
-                homeObject = cls(txn, uid, notifiers)
+                homeObject = cls(txn, uid)
+                for factory_type, factory in txn._notifierFactories.items():
+                    homeObject.addNotifier(factory_type, factory.newNotifier(homeObject))
                 homeObject = (yield homeObject.initFromStore())
                 if homeObject:
                     returnValue(homeObject)
@@ -1597,7 +1607,9 @@
                 # 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, notifiers)
+                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))
                 yield home.createdHome()
                 returnValue(home)
@@ -2087,30 +2099,37 @@
             self._quotaUsedBytes = 0
 
 
-    def addNotifier(self, notifier):
+    def addNotifier(self, factory_name, notifier):
         if self._notifiers is None:
-            self._notifiers = ()
-        self._notifiers += (notifier,)
+            self._notifiers = {}
+        self._notifiers[factory_name] = notifier
 
 
-    def notifierID(self, label="default"):
-        if self._notifiers:
-            return self._notifiers[0].getID(label)
-        else:
-            return None
+    def getNotifier(self, factory_name):
+        return self._notifiers.get(factory_name)
 
 
+    def notifierID(self):
+        return (self._notifierPrefix, self.uid(),)
+
+
     @inlineCallbacks
-    def nodeName(self, label="default"):
-        if self._notifiers:
-            for notifier in self._notifiers:
-                name = (yield notifier.nodeName(label=label))
-                if name is not None:
-                    returnValue(name)
-        else:
-            returnValue(None)
+    def notifyChanged(self):
+        """
+        Trigger a notification of a change
+        """
 
+        # Update modified if object still exists
+        if self._resourceID:
+            yield self.bumpModified()
 
+        # Only send one set of change notifications per transaction
+        if self._notifiers and not self._txn.isNotifiedAlready(self):
+            for notifier in self._notifiers.values():
+                self._txn.postCommit(notifier.notify)
+            self._txn.notificationAddedForObject(self)
+
+
     @classproperty
     def _lockLastModifiedQuery(cls): #@NoSelf
         meta = cls._homeMetaDataSchema
@@ -2162,23 +2181,6 @@
 
 
     @inlineCallbacks
-    def notifyChanged(self):
-        """
-        Trigger a notification of a change
-        """
-
-        # Update modified if object still exists
-        if self._resourceID:
-            yield self.bumpModified()
-
-        # Only send one set of change notifications per transaction
-        if self._notifiers and not self._txn.isNotifiedAlready(self):
-            for notifier in self._notifiers:
-                self._txn.postCommit(notifier.notify)
-            self._txn.notificationAddedForObject(self)
-
-
-    @inlineCallbacks
     def removeUnacceptedShares(self):
         """
         Unbinds any collections that have been shared to this home but not yet
@@ -2544,9 +2546,7 @@
         # a different token to subscribe to and thus would each need a separate push - whereas a common
         # token only requires one push (to multiple subscribers).
         if self._ownerHome._notifiers:
-            childID = "%s/%s" % (self._ownerHome.uid(), self._ownerName)
-            self._notifiers = [notifier.clone(label="collection", id=childID)
-                         for notifier in self._ownerHome._notifiers]
+            self._notifiers = dict([(factory_name, notifier.clone(self),) for factory_name, notifier in self._ownerHome._notifiers.items()])
         else:
             self._notifiers = None
 
@@ -2758,6 +2758,7 @@
 
         # Must send notification to ensure cache invalidation occurs
         yield self.notifyChanged()
+        yield shareeHome.notifyChanged()
 
         returnValue(sharedName)
 
@@ -2832,6 +2833,7 @@
 
             # Must send notification to ensure cache invalidation occurs
             yield self.notifyChanged()
+            yield shareeView.viewerHome().notifyChanged()
 
         returnValue(shareeView._name)
 
@@ -2879,6 +2881,7 @@
 
         # Must send notification to ensure cache invalidation occurs
         yield self.notifyChanged()
+        yield shareeHome.notifyChanged()
 
         returnValue(resourceName)
 
@@ -3408,6 +3411,7 @@
 
         # Change notification for a create is on the home collection
         yield home.notifyChanged()
+        yield child.notifyChanged()
         returnValue(child)
 
 
@@ -3538,12 +3542,9 @@
         yield self._renameSyncToken()
 
         yield self.notifyChanged()
+        yield self._home.notifyChanged()
 
-        # Make sure home collection modified is changed - not that we do not use _home.notifiedChanged() here
-        # since we are sending the notification on the existing child collection object
-        yield self._home.bumpModified()
 
-
     @classproperty
     def _deleteQuery(cls): #@NoSelf
         """
@@ -3556,6 +3557,9 @@
     @inlineCallbacks
     def remove(self):
 
+        # Do before setting _resourceID making changes
+        yield self.notifyChanged()
+
         queryCacher = self._home._txn._queryCacher
         if queryCacher:
             cacheKey = queryCacher.keyForObjectWithName(self._home._resourceID, self._name)
@@ -3572,13 +3576,9 @@
         self._modified = None
         self._objects = {}
 
-        yield self.notifyChanged()
+        yield self._home.notifyChanged()
 
-        # Make sure home collection modified is changed - not that we do not use _home.notifiedChanged() here
-        # since we are sending the notification on the previously existing child collection object
-        yield self._home.bumpModified()
 
-
     def ownerHome(self):
         """
         @see: L{ICalendar.ownerCalendarHome}
@@ -3987,30 +3987,41 @@
         return datetimeMktime(parseSQLTimestamp(self._modified)) if self._modified else None
 
 
-    def addNotifier(self, notifier):
+    def addNotifier(self, factory_name, notifier):
         if self._notifiers is None:
-            self._notifiers = ()
-        self._notifiers += (notifier,)
+            self._notifiers = {}
+        self._notifiers[factory_name] = notifier
 
 
-    def notifierID(self, label="default"):
-        if self._notifiers:
-            return self._notifiers[0].getID(label)
-        else:
-            return None
+    def getNotifier(self, factory_name):
+        return self._notifiers.get(factory_name)
 
 
+    def notifierID(self):
+        return (self.ownerHome()._notifierPrefix, "%s/%s" % (self.ownerHome().uid(), self._ownerName,),)
+
+
+    def parentNotifierID(self):
+        return self.ownerHome().notifierID()
+
+
     @inlineCallbacks
-    def nodeName(self, label="default"):
-        if self._notifiers:
-            for notifier in self._notifiers:
-                name = (yield notifier.nodeName(label=label))
-                if name is not None:
-                    returnValue(name)
-        else:
-            returnValue(None)
+    def notifyChanged(self):
+        """
+        Trigger a notification of a change
+        """
 
+        # Update modified if object still exists
+        if self._resourceID:
+            yield self.bumpModified()
 
+        # Only send one set of change notifications per transaction
+        if self._notifiers and not self._txn.isNotifiedAlready(self):
+            for notifier in self._notifiers.values():
+                self._txn.postCommit(notifier.notify)
+            self._txn.notificationAddedForObject(self)
+
+
     @classproperty
     def _lockLastModifiedQuery(cls): #@NoSelf
         schema = cls._homeChildMetaDataSchema
@@ -4061,24 +4072,7 @@
             log.debug("CommonHomeChild.bumpModified failed")
 
 
-    @inlineCallbacks
-    def notifyChanged(self):
-        """
-        Trigger a notification of a change
-        """
 
-        # Update modified if object still exists
-        if self._resourceID:
-            yield self.bumpModified()
-
-        # Only send one set of change notifications per transaction
-        if self._notifiers and not self._txn.isNotifiedAlready(self):
-            for notifier in self._notifiers:
-                self._txn.postCommit(notifier.notify)
-            self._txn.notificationAddedForObject(self)
-
-
-
 class CommonObjectResource(LoggingMixIn, FancyEqMixin):
     """
     Base class for object resources.
@@ -4582,18 +4576,7 @@
 
         # Make sure we have push notifications setup to push on this collection
         # as well as the home it is in
-        if txn._notifierFactory:
-            childID = "%s/%s" % (uid, "notification")
-            notifier = txn._notifierFactory.newNotifier(
-                label="collection",
-                id=childID,
-                prefix=txn._homeClass[txn._primaryHomeType]._notifierPrefix
-            )
-            notifier.addID(id=uid)
-            notifiers = (notifier,)
-        else:
-            notifiers = None
-        self._notifiers = notifiers
+        self._notifiers = dict([(factory_name, factory.newNotifier(self),) for factory_name, factory in txn._notifierFactories.items()])
 
     _resourceIDFromUIDQuery = Select(
         [_homeSchema.RESOURCE_ID], From=_homeSchema,
@@ -4652,6 +4635,7 @@
         yield collection._loadPropertyStore()
         if created:
             yield collection._initSyncToken()
+            yield collection.notifyChanged()
         returnValue(collection)
 
 
@@ -4760,6 +4744,7 @@
             yield self._insertRevision("%s.xml" % (uid,))
         else:
             yield self._updateRevision("%s.xml" % (uid,))
+        yield self.notifyChanged()
 
 
     def removeNotificationObjectWithName(self, name):
@@ -4778,6 +4763,7 @@
             self._txn, uid=uid, resourceID=self._resourceID)
         self._notifications.pop(uid, None)
         yield self._deleteRevision("%s.xml" % (uid,))
+        yield self.notifyChanged()
 
     _initSyncTokenQuery = Insert(
         {
@@ -4814,30 +4800,24 @@
         return self._propertyStore
 
 
-    def addNotifier(self, notifier):
+    def addNotifier(self, factory_name, notifier):
         if self._notifiers is None:
-            self._notifiers = ()
-        self._notifiers += (notifier,)
+            self._notifiers = {}
+        self._notifiers[factory_name] = notifier
 
 
-    def notifierID(self, label="default"):
-        if self._notifiers:
-            return self._notifiers[0].getID(label)
-        else:
-            return None
+    def getNotifier(self, factory_name):
+        return self._notifiers.get(factory_name)
 
 
-    @inlineCallbacks
-    def nodeName(self, label="default"):
-        if self._notifiers:
-            for notifier in self._notifiers:
-                name = (yield notifier.nodeName(label=label))
-                if name is not None:
-                    returnValue(name)
-        else:
-            returnValue(None)
+    def notifierID(self):
+        return (self._txn._homeClass[self._txn._primaryHomeType]._notifierPrefix, "%s/notification" % (self.ownerHome().uid(),),)
 
 
+    def parentNotifierID(self):
+        return (self._txn._homeClass[self._txn._primaryHomeType]._notifierPrefix, "%s" % (self.ownerHome().uid(),),)
+
+
     def notifyChanged(self):
         """
         Trigger a notification of a change
@@ -4845,7 +4825,7 @@
 
         # Only send one set of change notifications per transaction
         if self._notifiers and not self._txn.isNotifiedAlready(self):
-            for notifier in self._notifiers:
+            for notifier in self._notifiers.values():
                 self._txn.postCommit(notifier.notify)
             self._txn.notificationAddedForObject(self)
 

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/current-oracle-dialect.sql	2013-05-08 18:03:05 UTC (rev 11148)
@@ -21,19 +21,19 @@
     "DATAVERSION" integer default 0 not null
 );
 
+create table CALENDAR (
+    "RESOURCE_ID" integer primary key
+);
+
 create table CALENDAR_HOME_METADATA (
     "RESOURCE_ID" integer primary key references CALENDAR_HOME on delete cascade,
     "QUOTA_USED_BYTES" integer default 0 not null,
-    "DEFAULT_EVENTS" integer default null,
-    "DEFAULT_TASKS" integer default null,
+    "DEFAULT_EVENTS" integer default null references CALENDAR on delete set null,
+    "DEFAULT_TASKS" integer default null references CALENDAR on delete set null,
     "CREATED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC',
     "MODIFIED" timestamp default CURRENT_TIMESTAMP at time zone 'UTC'
 );
 
-create table CALENDAR (
-    "RESOURCE_ID" integer primary key
-);
-
 create table CALENDAR_METADATA (
     "RESOURCE_ID" integer primary key references CALENDAR on delete cascade,
     "SUPPORTED_COMPONENTS" nvarchar2(255) default null,

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/current.sql	2013-05-08 18:03:05 UTC (rev 11148)
@@ -55,6 +55,14 @@
   DATAVERSION      integer      default 0 not null
 );
 
+--------------
+-- Calendar --
+--------------
+
+create table CALENDAR (
+  RESOURCE_ID integer   primary key default nextval('RESOURCE_ID_SEQ') -- implicit index
+);
+
 ----------------------------
 -- Calendar Home Metadata --
 ----------------------------
@@ -62,21 +70,13 @@
 create table CALENDAR_HOME_METADATA (
   RESOURCE_ID      integer      primary key references CALENDAR_HOME on delete cascade, -- implicit index
   QUOTA_USED_BYTES integer      default 0 not null,
-  DEFAULT_EVENTS   integer      default null,
-  DEFAULT_TASKS    integer      default null,
+  DEFAULT_EVENTS   integer      default null references CALENDAR on delete set null,
+  DEFAULT_TASKS    integer      default null references CALENDAR on delete set null,
   CREATED          timestamp    default timezone('UTC', CURRENT_TIMESTAMP),
   MODIFIED         timestamp    default timezone('UTC', CURRENT_TIMESTAMP)
 );
 
---------------
--- Calendar --
---------------
 
-create table CALENDAR (
-  RESOURCE_ID integer   primary key default nextval('RESOURCE_ID_SEQ') -- implicit index
-);
-
-
 -----------------------
 -- Calendar Metadata --
 -----------------------

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_18_to_19.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_18_to_19.sql	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/oracle-dialect/upgrade_from_18_to_19.sql	2013-05-08 18:03:05 UTC (rev 11148)
@@ -21,8 +21,8 @@
 -- Calendar home related updates
 
 alter table ATTACHMENT
- add ("DEFAULT_EVENTS" integer default null,
- 	  "DEFAULT_TASKS"  integer default null);
+ add ("DEFAULT_EVENTS" integer default null references CALENDAR on delete set null,
+ 	  "DEFAULT_TASKS"  integer default null references CALENDAR on delete set null);
 
  	  
 -- Calendar bind related updates

Modified: CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_18_to_19.sql
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_18_to_19.sql	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/common/datastore/sql_schema/upgrades/postgres-dialect/upgrade_from_18_to_19.sql	2013-05-08 18:03:05 UTC (rev 11148)
@@ -22,8 +22,8 @@
 -- Calendar home related updates
 
 alter table CALENDAR_HOME_METADATA
- add column DEFAULT_EVENTS integer default null,
- add column DEFAULT_TASKS integer default null;
+ add column DEFAULT_EVENTS integer default null references CALENDAR on delete set null,
+ add column DEFAULT_TASKS integer default null references CALENDAR on delete set null;
 
 
 -- Calendar bind related updates

Modified: CalendarServer/trunk/txdav/common/datastore/test/util.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/test/util.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/common/datastore/test/util.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -168,7 +168,8 @@
         cp.startService()
         reactor.addSystemEventTrigger("before", "shutdown", cp.stopService)
         cds = CommonDataStore(
-            cp.connection, StubNotifierFactory(),
+            cp.connection,
+            {"push": StubNotifierFactory(), },
             TestStoreDirectoryService(),
             attachmentRoot, "",
             quota=staticQuota
@@ -236,7 +237,7 @@
         quota = deriveQuota(testCase)
         store = CommonDataStore(
             cp.connection,
-            notifierFactory,
+            {"push": notifierFactory} if notifierFactory is not None else {},
             directoryService,
             attachmentRoot,
             "https://example.com/calendars/__uids__/%(home)s/attachments/%(name)s",
@@ -706,30 +707,18 @@
         self.hostname = "example.com"
 
 
-    def newNotifier(self, label="default", id=None, prefix=None):
-        return Notifier(self, label=label, id=id, prefix=prefix)
+    def newNotifier(self, storeObject):
+        return Notifier(self, storeObject)
 
 
-    def pushKeyForId(self, id):
-        path = "/"
+    def pushKeyForId(self, prefix, id):
+        return "/%s/%s/%s/" % (prefix, self.hostname, id)
 
-        try:
-            prefix, id = id.split("|", 1)
-            path += "%s/" % (prefix,)
-        except ValueError:
-            # id has no prefix
-            pass
 
-        path += "%s/" % (self.hostname,)
-        if id:
-            path += "%s/" % (id,)
-        return path
+    def send(self, prefix, id):
+        self.history.append(self.pushKeyForId(prefix, id))
 
 
-    def send(self, id):
-        self.history.append(id)
-
-
     def reset(self):
         self.history = []
 

Modified: CalendarServer/trunk/txdav/common/datastore/upgrade/sql/others/test/test_attachment_migration.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/upgrade/sql/others/test/test_attachment_migration.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/common/datastore/upgrade/sql/others/test/test_attachment_migration.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -47,7 +47,7 @@
         self.patch(config, "EnableManagedAttachments", enableManagedAttachments)
 
         store = yield theStoreBuilder.buildStore(
-            self, StubNotifierFactory()
+            self, {"push": StubNotifierFactory()}
         )
         store.enableManagedAttachments = enableManagedAttachments
 

Modified: CalendarServer/trunk/txdav/common/datastore/upgrade/sql/test/test_upgrade.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/upgrade/sql/test/test_upgrade.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/common/datastore/upgrade/sql/test/test_upgrade.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -143,7 +143,7 @@
         """
 
         store = yield theStoreBuilder.buildStore(
-            self, StubNotifierFactory()
+            self, {"push": StubNotifierFactory()}
         )
 
         @inlineCallbacks
@@ -223,7 +223,7 @@
         """
 
         store = yield theStoreBuilder.buildStore(
-            self, StubNotifierFactory()
+            self, {"push": StubNotifierFactory()}
         )
 
         @inlineCallbacks

Modified: CalendarServer/trunk/txdav/common/datastore/upgrade/test/test_migrate.py
===================================================================
--- CalendarServer/trunk/txdav/common/datastore/upgrade/test/test_migrate.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/common/datastore/upgrade/test/test_migrate.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -140,7 +140,7 @@
         self.filesPath = CachingFilePath(self.mktemp())
         self.filesPath.createDirectory()
         fileStore = self.fileStore = CommonDataStore(
-            self.filesPath, StubNotifierFactory(), TestStoreDirectoryService(), True, True
+            self.filesPath, {"push": StubNotifierFactory()}, TestStoreDirectoryService(), True, True
         )
         self.sqlStore = yield theStoreBuilder.buildStore(
             self, StubNotifierFactory()

Modified: CalendarServer/trunk/txdav/idav.py
===================================================================
--- CalendarServer/trunk/txdav/idav.py	2013-05-08 18:00:48 UTC (rev 11147)
+++ CalendarServer/trunk/txdav/idav.py	2013-05-08 18:03:05 UTC (rev 11148)
@@ -27,6 +27,8 @@
     "IDataStoreObject",
     "ITransaction",
     "INotifier",
+    "IStoreNotifierFactory",
+    "IStoreNotifier",
 ]
 
 from zope.interface import Attribute, Interface
@@ -41,6 +43,8 @@
     Property store error.
     """
 
+
+
 class PropertyChangeNotAllowedError(PropertyStoreError):
     """
     Property cannot be edited.
@@ -62,7 +66,7 @@
     namespace = Attribute("Namespace")
     name = Attribute("Name")
 
-    def toString():
+    def toString(): #@NoSelf
         """
         Returns the string representation of the property name.
 
@@ -70,6 +74,7 @@
         """
 
 
+
 class IPropertyStore(IMapping):
     """
     WebDAV property store
@@ -84,25 +89,26 @@
     # FIXME: the type for values isn't quite right, there should be some more
     # specific interface for that.
 
-    def flush():
+    def flush(): #@NoSelf
         """
         Flush the property store.
         @return: C{None}
         """
 
-    def abort():
+    def abort(): #@NoSelf
         """
         Abort changes to the property store.
         @return: C{None}
         """
 
 
+
 class IDataStore(Interface):
     """
     An L{IDataStore} is a storage of some objects.
     """
 
-    def newTransaction(label=None):
+    def newTransaction(label=None): #@NoSelf
         """
         Create a new transaction.
 
@@ -116,7 +122,7 @@
         @rtype: L{ITransaction}
         """
 
-    def setMigrating(state):
+    def setMigrating(state): #@NoSelf
         """
         Set the "migrating" state to either True or False.  This state is
         used to supress push notifications and etag changes.
@@ -126,12 +132,13 @@
         """
 
 
+
 class IDataStoreObject(Interface):
     """
     An L{IDataStoreObject} are the objects stored in an L{IDataStore}.
     """
 
-    def name():
+    def name(): #@NoSelf
         """
         Identify the name of the object
 
@@ -139,42 +146,42 @@
         @rtype: C{str}
         """
 
-    def contentType():
+    def contentType(): #@NoSelf
         """
         The content type of the object's content.
 
         @rtype: L{MimeType}
         """
 
-    def md5():
+    def md5(): #@NoSelf
         """
         The MD5 hex digest of this object's content.
 
         @rtype: C{str}
         """
 
-    def size():
+    def size(): #@NoSelf
         """
         The octet-size of this object's content.
 
         @rtype: C{int}
         """
 
-    def created():
+    def created(): #@NoSelf
         """
         The creation date-time stamp of this object.
 
         @rtype: C{int}
         """
 
-    def modified():
+    def modified(): #@NoSelf
         """
         The last modification date-time stamp of this object.
 
         @rtype: C{int}
         """
 
-    def properties():
+    def properties(): #@NoSelf
         """
         Retrieve the property store for this object.
 
@@ -189,7 +196,7 @@
     its entirety.
     """
 
-    def abort():
+    def abort(): #@NoSelf
         """
         Abort this transaction.
 
@@ -197,8 +204,7 @@
             an 'abort' or 'commit' and cannot be aborted again.
         """
 
-
-    def commit():
+    def commit(): #@NoSelf
         """
         Perform this transaction.
 
@@ -206,20 +212,17 @@
             an 'abort' or 'commit' and cannot be committed again.
         """
 
-
-    def postCommit(operation):
+    def postCommit(operation): #@NoSelf
         """
         @see: L{IAsyncTransaction.postCommit}
         """
 
-
-    def postAbort(operation):
+    def postAbort(operation): #@NoSelf
         """
         @see: L{IAsyncTransaction.postAbort}
         """
 
-
-    def store():
+    def store(): #@NoSelf
         """
         The store that this transaction was initiated from.
 
@@ -230,49 +233,92 @@
 
 class INotifier(Interface):
     """
-    Push notification interface
+    Interface for an object that can send change notifications. Notifiers are associated with specific notifier factories
+    and stored in a dict with keys matching the factory name.
     """
 
-    def notifierID(label):
+    _notifiers = Attribute("Dict of L{IStoreNotifier}'s to send notifications to.")
+
+    def addNotifier(factory_name, notifier): #@NoSelf
         """
-        Return a push notification id.
+        Add an L{IStoreNotifier} to the list of notifiers for this object.
 
-        Data store objects can have an associated Notifier object which is
-        responsible for the actual communication with the outside world.
-        Clients determine what notification service to subscribe to by
-        querying the server for various DAV properties.  These properties
-        include unique IDs from each resource, and the source of truth for
-        these IDs is the data store.  This method returns the notification
-        related ID for a given data store object.
+        @param factory_name: the "type" of notifier based on its factory name
+        @type factory_name: C{str}
+        @param notifier: the notifier
+        @type notifier: L{IStoreNotifier}
+        """
 
-        Sharing introduces the need for a data store object to have multiple
-        notifier IDs because a subscriber sees the ID for the particular
-        collection being shared while the sharer sees the ID of the parent
-        home.  Therefore there is a label parameter to identify which ID is
-        being requested: "default" (what a sharer sees), or "collection"
-        for the collection itself (what a subscriber sees).
+    def getNotifier(factory_name): #@NoSelf
+        """
+        Return a notifier for the specified factory name if it exists.
 
-        @return: a string (or None if notifications are disabled)
+        @param factory_name: the factory name for a notifier to look for.
+        @type factory_name: C{str}
+
+        @return: the notifier if found, else C{None}
+        @rtype: L{IStoreNotifier} or C{None}
         """
 
-    def nodeName(label):
+    def notifyChanged(): #@NoSelf
         """
-        Returns a pubsub node path.
+        Send a change notification to any notifiers assigned to the object.
+        """
 
-        A pubsub node path is comprised of the following values:
+    def notifierID(): #@NoSelf
+        """
+        Return a notification id. This is a tuple of two C{str}'s. The first item is the overall
+        service type "CalDAV" or "CardDAV". The second is the object identifier. For a home that
+        is the home's ownerUID, for a home child the ownerUID/name.
 
-        /<protocol>/<hostname>/<notifierID>/
+        @return: a tuple of two C[str}
+        """
 
-        <protocol> is either CalDAV or CardDAV
-        <hostname> is the name of the calendar server
-        <notifierID> is a unique string representing the resource
 
-        This method builds this string based on pubsub configuration
-        that was passed to the NotifierFactory, and it also attempts
-        to create and configure the node in the pubsub server.  If that
-        fails, a value of None will be returned. This is used when a client
-        requests push-related DAV properties.
 
-        @return: a deferred to a string (or None if notifications are disabled
-        or the node could not be created)
+class IStoreNotifierFactory(Interface):
+    """
+    A factory class for a particular type of store notification. App-layer clients of the store may need
+    to be notified of changes happening to objects in the store, and act in a particular way when such changes
+    occur (e.g., send a push notification, invalidate an app-layer cache etc). To do that they create a class
+    for the interface defined here and pass that in when the store itself is created. When the store creates
+    a home object, it will pass in the list of factories and the home will instantiate an L{IStoreNotifier} to use
+    for sending notifications. Home child resources "inherit" notifiers from their home parent using the
+    IStoreNotifier.clone() method - object resources do not have notifiers.
+    """
+
+    store = Attribute("The store associated with the notifier factory")
+
+    def newNotifier(storeObject): #@NoSelf
         """
+        Generate a notifier for the corresponding store object.
+
+        @param storeObject: the store object.
+        @type storeObject: L{CommonHome} or L{CommonHomeChild}
+        """
+
+
+
+class IStoreNotifier(Interface):
+    """
+    A notifier provided by the app-layer through an L{IStoreNotifierFactory} that the store uses to indicate
+    a change to a store home or home child object.
+    """
+
+    _storeObject = Attribute("The store object associated with the notifier: L{CommonHome} or L{CommonHomeChild}")
+
+    def notify(): #@NoSelf
+        """
+        Called by the store when the object associated with the notifier is changed.
+        """
+
+    def clone(storeObject): #@NoSelf
+        """
+        Called by the store when a home child is created and used to give the home child a notifier
+        "inherited" from the home.
+
+        @param label: a new label to use for the home child.
+        @type label: C{str}
+        @param storeObject: the store object associated with the notifier.
+        @type storeObject: L{CommonHome} or L{CommonHomeChild}
+        """
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130508/03a2c36a/attachment-0001.html>


More information about the calendarserver-changes mailing list