[CalendarServer-changes] [5442] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Wed Apr 7 13:00:48 PDT 2010


Revision: 5442
          http://trac.macosforge.org/projects/calendarserver/changeset/5442
Author:   cdaboo at apple.com
Date:     2010-04-07 13:00:47 -0700 (Wed, 07 Apr 2010)
Log Message:
-----------
Merged shared calendar support.

Modified Paths:
--------------
    CalendarServer/trunk/calendarserver/tap/caldav.py
    CalendarServer/trunk/calendarserver/tools/purge.py
    CalendarServer/trunk/calendarserver/tools/test/test_principals.py
    CalendarServer/trunk/conf/caldavd-apple.plist
    CalendarServer/trunk/conf/caldavd-test.plist
    CalendarServer/trunk/conf/caldavd.plist
    CalendarServer/trunk/twext/web2/dav/auth.py
    CalendarServer/trunk/twext/web2/dav/element/base.py
    CalendarServer/trunk/twext/web2/dav/noneprops.py
    CalendarServer/trunk/twext/web2/dav/test/test_xattrprops.py
    CalendarServer/trunk/twext/web2/dav/xattrprops.py
    CalendarServer/trunk/twistedcaldav/authkerb.py
    CalendarServer/trunk/twistedcaldav/caldavxml.py
    CalendarServer/trunk/twistedcaldav/customxml.py
    CalendarServer/trunk/twistedcaldav/directory/calendar.py
    CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py
    CalendarServer/trunk/twistedcaldav/directory/directory.py
    CalendarServer/trunk/twistedcaldav/directory/principal.py
    CalendarServer/trunk/twistedcaldav/directory/sudo.py
    CalendarServer/trunk/twistedcaldav/directorybackedaddressbook.py
    CalendarServer/trunk/twistedcaldav/dropbox.py
    CalendarServer/trunk/twistedcaldav/extensions.py
    CalendarServer/trunk/twistedcaldav/freebusyurl.py
    CalendarServer/trunk/twistedcaldav/ical.py
    CalendarServer/trunk/twistedcaldav/icaldav.py
    CalendarServer/trunk/twistedcaldav/index.py
    CalendarServer/trunk/twistedcaldav/instance.py
    CalendarServer/trunk/twistedcaldav/mail.py
    CalendarServer/trunk/twistedcaldav/memcacheprops.py
    CalendarServer/trunk/twistedcaldav/method/delete_common.py
    CalendarServer/trunk/twistedcaldav/method/get.py
    CalendarServer/trunk/twistedcaldav/method/mkcol.py
    CalendarServer/trunk/twistedcaldav/method/put_common.py
    CalendarServer/trunk/twistedcaldav/method/report_calquery.py
    CalendarServer/trunk/twistedcaldav/method/report_common.py
    CalendarServer/trunk/twistedcaldav/query/calendarquery.py
    CalendarServer/trunk/twistedcaldav/query/sqlgenerator.py
    CalendarServer/trunk/twistedcaldav/resource.py
    CalendarServer/trunk/twistedcaldav/schedule.py
    CalendarServer/trunk/twistedcaldav/scheduling/caldav.py
    CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
    CalendarServer/trunk/twistedcaldav/scheduling/itip.py
    CalendarServer/trunk/twistedcaldav/scheduling/processing.py
    CalendarServer/trunk/twistedcaldav/scheduling/utils.py
    CalendarServer/trunk/twistedcaldav/sql.py
    CalendarServer/trunk/twistedcaldav/static.py
    CalendarServer/trunk/twistedcaldav/stdconfig.py
    CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py
    CalendarServer/trunk/twistedcaldav/test/test_icalendar.py
    CalendarServer/trunk/twistedcaldav/test/test_index.py
    CalendarServer/trunk/twistedcaldav/test/test_memcacheprops.py
    CalendarServer/trunk/twistedcaldav/test/test_resource.py
    CalendarServer/trunk/twistedcaldav/test/test_xml.py
    CalendarServer/trunk/twistedcaldav/test/util.py
    CalendarServer/trunk/twistedcaldav/timezoneservice.py

Added Paths:
-----------
    CalendarServer/trunk/twistedcaldav/datafilters/
    CalendarServer/trunk/twistedcaldav/datafilters/__init__.py
    CalendarServer/trunk/twistedcaldav/datafilters/calendardata.py
    CalendarServer/trunk/twistedcaldav/datafilters/filter.py
    CalendarServer/trunk/twistedcaldav/datafilters/peruserdata.py
    CalendarServer/trunk/twistedcaldav/datafilters/privateevents.py
    CalendarServer/trunk/twistedcaldav/datafilters/test/
    CalendarServer/trunk/twistedcaldav/datafilters/test/__init__.py
    CalendarServer/trunk/twistedcaldav/datafilters/test/test_calendardata.py
    CalendarServer/trunk/twistedcaldav/datafilters/test/test_peruserdata.py
    CalendarServer/trunk/twistedcaldav/datafilters/test/test_privateevents.py
    CalendarServer/trunk/twistedcaldav/notifications.py
    CalendarServer/trunk/twistedcaldav/query/queryfilter.py
    CalendarServer/trunk/twistedcaldav/query/test/
    CalendarServer/trunk/twistedcaldav/query/test/__init__.py
    CalendarServer/trunk/twistedcaldav/query/test/test_calendarquery.py
    CalendarServer/trunk/twistedcaldav/query/test/test_queryfilter.py
    CalendarServer/trunk/twistedcaldav/sharedcalendar.py
    CalendarServer/trunk/twistedcaldav/sharing.py
    CalendarServer/trunk/twistedcaldav/test/test_sharing.py

Removed Paths:
-------------
    CalendarServer/trunk/twistedcaldav/datafilters/__init__.py
    CalendarServer/trunk/twistedcaldav/datafilters/calendardata.py
    CalendarServer/trunk/twistedcaldav/datafilters/filter.py
    CalendarServer/trunk/twistedcaldav/datafilters/peruserdata.py
    CalendarServer/trunk/twistedcaldav/datafilters/privateevents.py
    CalendarServer/trunk/twistedcaldav/datafilters/test/
    CalendarServer/trunk/twistedcaldav/datafilters/test/__init__.py
    CalendarServer/trunk/twistedcaldav/datafilters/test/test_calendardata.py
    CalendarServer/trunk/twistedcaldav/datafilters/test/test_peruserdata.py
    CalendarServer/trunk/twistedcaldav/datafilters/test/test_privateevents.py
    CalendarServer/trunk/twistedcaldav/query/test/__init__.py
    CalendarServer/trunk/twistedcaldav/query/test/test_calendarquery.py
    CalendarServer/trunk/twistedcaldav/query/test/test_queryfilter.py

Property Changed:
----------------
    CalendarServer/trunk/


Property changes on: CalendarServer/trunk
___________________________________________________________________
Modified: svn:mergeinfo
   - /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
   + /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093

Modified: CalendarServer/trunk/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/trunk/calendarserver/tap/caldav.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/calendarserver/tap/caldav.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -446,8 +446,9 @@
 
             if config.ProcessType in ('Combined', 'Single'):
 
-                # Memcached is not needed for this process
-                config.Memcached.Pools.Default.ClientEnabled = False
+                # Memcached is not needed for the "master" process
+                if config.ProcessType in ('Combined',):
+                    config.Memcached.Pools.Default.ClientEnabled = False
 
                 # Note: if the master process ever needs access to memcached
                 # we'll either have to start memcached prior to the

Modified: CalendarServer/trunk/calendarserver/tools/purge.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/purge.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/calendarserver/tools/purge.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -16,7 +16,6 @@
 # limitations under the License.
 ##
 
-from cStringIO import StringIO
 from calendarserver.tap.util import FakeRequest
 from calendarserver.tap.util import getRootResource
 from calendarserver.tools.principals import removeProxy
@@ -35,6 +34,7 @@
 from twistedcaldav.config import config, ConfigurationError
 from twistedcaldav.directory.directory import DirectoryError, DirectoryRecord
 from twistedcaldav.method.delete_common import DeleteResource
+from twistedcaldav.query import queryfilter
 import os
 import sys
 
@@ -256,14 +256,15 @@
     log.info("Purging events from %d calendar homes" % (len(records),))
 
     filter =  caldavxml.Filter(
-          caldavxml.ComponentFilter(
-              caldavxml.ComponentFilter(
-                  TimeRange(start=date,),
-                  name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
-              ),
-              name="VCALENDAR",
-           )
-      )
+        caldavxml.ComponentFilter(
+            caldavxml.ComponentFilter(
+                TimeRange(start=date,),
+                name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
+            ),
+            name="VCALENDAR",
+        )
+    )
+    filter = queryfilter.Filter(filter)
 
     eventCount = 0
     for record in records:
@@ -378,6 +379,7 @@
               name="VCALENDAR",
            )
       )
+    filter = queryfilter.Filter(filter)
 
     count = 0
 

Modified: CalendarServer/trunk/calendarserver/tools/test/test_principals.py
===================================================================
--- CalendarServer/trunk/calendarserver/tools/test/test_principals.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/calendarserver/tools/test/test_principals.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -81,6 +81,7 @@
         cwd = sourceRoot
 
         deferred = Deferred()
+        e = os.environ
         reactor.spawnProcess(CapturingProcessProtocol(deferred, None), python, args, env=os.environ, path=cwd)
         output = yield deferred
         returnValue(output)

Modified: CalendarServer/trunk/conf/caldavd-apple.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-apple.plist	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/conf/caldavd-apple.plist	2010-04-07 20:00:47 UTC (rev 5442)
@@ -477,6 +477,26 @@
     <key>EnablePrivateEvents</key>
     <true/>
 
+    <!-- Shared Calendars & Address Books -->
+    <key>Sharing</key>
+    <dict>
+    	<key>Enabled</key>
+    	<true/>
+    	<key>AllowExternalUsers</key>
+    	<false/>
+    	<key>Calendars</key>
+    	<dict>
+    		<key>Enabled</key>
+    		<true/>
+    		<key>AllowScheduling</key>
+    		<false/>
+    	</dict>
+    	<key>AddressBooks</key>
+    	<dict>
+    		<key>Enabled</key>
+    		<false/>
+    	</dict>
+    </dict>
 
     <!--
         Miscellaneous items

Modified: CalendarServer/trunk/conf/caldavd-test.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test.plist	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/conf/caldavd-test.plist	2010-04-07 20:00:47 UTC (rev 5442)
@@ -669,6 +669,26 @@
     <key>EnableTimezoneService</key>
     <true/>
 
+    <!-- Shared Calendars & Address Books -->
+    <key>Sharing</key>
+    <dict>
+    	<key>Enabled</key>
+    	<true/>
+    	<key>AllowExternalUsers</key>
+    	<false/>
+    	<key>Calendars</key>
+    	<dict>
+    		<key>Enabled</key>
+    		<true/>
+    		<key>AllowScheduling</key>
+    		<false/>
+    	</dict>
+    	<key>AddressBooks</key>
+    	<dict>
+    		<key>Enabled</key>
+    		<false/>
+    	</dict>
+    </dict>
 
     <!--
         Miscellaneous items

Modified: CalendarServer/trunk/conf/caldavd.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd.plist	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/conf/caldavd.plist	2010-04-07 20:00:47 UTC (rev 5442)
@@ -466,7 +466,28 @@
     <key>EnablePrivateEvents</key>
     <true/>
 
+    <!-- Shared Calendars & Address Books -->
+    <key>Sharing</key>
+    <dict>
+    	<key>Enabled</key>
+    	<true/>
+    	<key>AllowExternalUsers</key>
+    	<false/>
+    	<key>Calendars</key>
+    	<dict>
+    		<key>Enabled</key>
+    		<true/>
+    		<key>AllowScheduling</key>
+    		<false/>
+    	</dict>
+    	<key>AddressBooks</key>
+    	<dict>
+    		<key>Enabled</key>
+    		<false/>
+    	</dict>
+    </dict>
 
+
     <!--
         Miscellaneous items
       -->

Modified: CalendarServer/trunk/twext/web2/dav/auth.py
===================================================================
--- CalendarServer/trunk/twext/web2/dav/auth.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twext/web2/dav/auth.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -96,7 +96,12 @@
         pswd = str(pcreds.authnPrincipal.readDeadProperty(TwistedPasswordProperty))
 
         d = defer.maybeDeferred(credentials.checkPassword, pswd)
-        d.addCallback(self._cbPasswordMatch, (pcreds.authnPrincipal.principalURL(), pcreds.authzPrincipal.principalURL()))
+        d.addCallback(self._cbPasswordMatch, (
+            pcreds.authnPrincipal.principalURL(),
+            pcreds.authzPrincipal.principalURL(),
+            pcreds.authnPrincipal,
+            pcreds.authzPrincipal,
+        ))
         return d
 
 ##

Modified: CalendarServer/trunk/twext/web2/dav/element/base.py
===================================================================
--- CalendarServer/trunk/twext/web2/dav/element/base.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twext/web2/dav/element/base.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -219,9 +219,9 @@
         return child in self.children
 
 
-    def writeXML(self, output):
-        output.write("<?xml version='1.0' encoding='UTF-8'?>")
-        self.writeToStream(output, "", 0, True)
+    def writeXML(self, output, pretty=True):
+        output.write("<?xml version='1.0' encoding='UTF-8'?>" + ("\n" if pretty else ""))
+        self.writeToStream(output, "", 0, pretty)
 
     
     def writeToStream(self, output, ns, level, pretty):
@@ -294,9 +294,9 @@
 
         output.write(" %s='%s'" % (name, value,))  
       
-    def toxml(self):
+    def toxml(self, pretty=True):
         output = StringIO.StringIO()
-        self.writeXML(output)
+        self.writeXML(output, pretty)
         return str(output.getvalue())
 
     def element(self, document):

Modified: CalendarServer/trunk/twext/web2/dav/noneprops.py
===================================================================
--- CalendarServer/trunk/twext/web2/dav/noneprops.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twext/web2/dav/noneprops.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -49,19 +49,19 @@
     def __init__(self, resource):
         pass
 
-    def get(self, qname):
+    def get(self, qname, uid=None):
         raise HTTPError(StatusResponse(responsecode.NOT_FOUND, "No such property: {%s}%s" % qname))
 
-    def set(self, property):
+    def set(self, property, uid=None):
         raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Permission denied for setting property: %s" % (property,)))
 
-    def delete(self, qname):
+    def delete(self, qname, uid=None):
         # RFC 2518 Section 12.13.1 says that removal of
         # non-existing property is not an error.
         pass
 
-    def contains(self, qname):
+    def contains(self, qname, uid=None):
         return False
 
-    def list(self):
+    def list(self, uid=None):
         return ()

Modified: CalendarServer/trunk/twext/web2/dav/test/test_xattrprops.py
===================================================================
--- CalendarServer/trunk/twext/web2/dav/test/test_xattrprops.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twext/web2/dav/test/test_xattrprops.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -81,40 +81,42 @@
         self._forbiddenTest('get')
 
 
-    def _makeValue(self):
+    def _makeValue(self, uid=None):
         """
         Create and return any old WebDAVDocument for use by the get tests.
         """
-        element = Depth("0")
+        element = Depth(uid if uid is not None else "0")
         document = WebDAVDocument(element)
         return document
 
 
-    def _setValue(self, originalDocument, value):
+    def _setValue(self, originalDocument, value, uid=None):
         element = originalDocument.root_element
         attribute = (
             self.propertyStore.deadPropertyXattrPrefix +
+            (uid if uid is not None else "") +
             "{%s}%s" % element.qname())
         self.attrs[attribute] = value
 
 
-    def _getValue(self, originalDocument):
+    def _getValue(self, originalDocument, uid=None):
         element = originalDocument.root_element
         attribute = (
             self.propertyStore.deadPropertyXattrPrefix +
+            (uid if uid is not None else "") +
             "{%s}%s" % element.qname())
         return self.attrs[attribute]
 
 
-    def _checkValue(self, originalDocument):
+    def _checkValue(self, originalDocument, uid=None):
         property = originalDocument.root_element.qname()
 
         # Try to load it via xattrPropertyStore.get
-        loadedDocument = self.propertyStore.get(property)
+        loadedDocument = self.propertyStore.get(property, uid)
 
         # XXX Why isn't this a WebDAVDocument?
         self.assertIsInstance(loadedDocument, Depth)
-        self.assertEquals(str(loadedDocument), "0")
+        self.assertEquals(str(loadedDocument), uid if uid else "0")
 
 
     def test_getXML(self):
@@ -306,3 +308,99 @@
         # Make sure that the status is FORBIDDEN, a roughly reasonable mapping
         # of the EPERM failure.
         self.assertEquals(error.response.code, FORBIDDEN)
+
+    def test_get_uids(self):
+        """
+        L{xattrPropertyStore.get} accepts a L{WebDAVElement} and stores a
+        compressed XML document representing it in an extended attribute.
+        """
+        
+        for uid in (None, "123", "456",):
+            document = self._makeValue(uid)
+            self._setValue(document, document.toxml(), uid=uid)
+
+        for uid in (None, "123", "456",):
+            document = self._makeValue(uid)
+            self._checkValue(document, uid=uid)
+
+
+    def test_set_uids(self):
+        """
+        L{xattrPropertyStore.set} accepts a L{WebDAVElement} and stores a
+        compressed XML document representing it in an extended attribute.
+        """
+        
+        for uid in (None, "123", "456",):
+            document = self._makeValue(uid)
+            self.propertyStore.set(document.root_element, uid=uid)
+            self.assertEquals(
+                decompress(self._getValue(document, uid)), document.toxml())
+
+    def test_delete_uids(self):
+        """
+        L{xattrPropertyStore.set} accepts a L{WebDAVElement} and stores a
+        compressed XML document representing it in an extended attribute.
+        """
+        
+        for delete_uid in (None, "123", "456",):
+            for uid in (None, "123", "456",):
+                document = self._makeValue(uid)
+                self.propertyStore.set(document.root_element, uid=uid)
+            self.propertyStore.delete(document.root_element.qname(), uid=delete_uid)
+            self.assertRaises(KeyError, self._getValue, document, uid=delete_uid)
+            for uid in (None, "123", "456",):
+                if uid == delete_uid:
+                    continue
+                document = self._makeValue(uid)
+                self.assertEquals(
+                    decompress(self._getValue(document, uid)), document.toxml())
+        
+    def test_contains_uids(self):
+        """
+        L{xattrPropertyStore.contains} returns C{True} if the given property
+        has a value, C{False} otherwise.
+        """
+        for uid in (None, "123", "456",):
+            document = self._makeValue(uid)
+            self.assertFalse(
+                self.propertyStore.contains(document.root_element.qname(), uid=uid))
+            self._setValue(document, document.toxml(), uid=uid)
+            self.assertTrue(
+                self.propertyStore.contains(document.root_element.qname(), uid=uid))
+
+    def test_list_uids(self):
+        """
+        L{xattrPropertyStore.list} returns a C{list} of property names
+        associated with the wrapped file.
+        """
+        prefix = self.propertyStore.deadPropertyXattrPrefix
+        for uid in (None, "123", "456",):
+            user = uid if uid is not None else ""
+            self.attrs[prefix + '%s{foo}bar' % (user,)] = 'baz%s' % (user,)
+            self.attrs[prefix + '%s{bar}baz' % (user,)] = 'quux%s' % (user,)
+            self.attrs[prefix + '%s{moo}mar%s' % (user, user,)] = 'quux%s' % (user,)
+
+        for uid in (None, "123", "456",):
+            user = uid if uid is not None else ""
+            self.assertEquals(
+                set(self.propertyStore.list(uid)),
+                set([
+                    (u'foo', u'bar'),
+                    (u'bar', u'baz'),
+                    (u'moo', u'mar%s' % (user,)),
+                ]))
+
+        self.assertEquals(
+            set(self.propertyStore.list(filterByUID=False)),
+            set([
+                (u'foo', u'bar', None),
+                (u'bar', u'baz', None),
+                (u'moo', u'mar', None),
+                (u'foo', u'bar', "123"),
+                (u'bar', u'baz', "123"),
+                (u'moo', u'mar123', "123"),
+                (u'foo', u'bar', "456"),
+                (u'bar', u'baz', "456"),
+                (u'moo', u'mar456', "456"),
+            ]))
+

Modified: CalendarServer/trunk/twext/web2/dav/xattrprops.py
===================================================================
--- CalendarServer/trunk/twext/web2/dav/xattrprops.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twext/web2/dav/xattrprops.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -84,20 +84,29 @@
     if sys.platform == "linux2":
         deadPropertyXattrPrefix = "user."
 
-    def _encode(clazz, name):
+    def _encode(clazz, name, uid=None):
         result = urllib.quote("{%s}%s" % name, safe='{}:')
+        if uid:
+            result = uid + result
         r = clazz.deadPropertyXattrPrefix + result
         return r
 
     def _decode(clazz, name):
         name = urllib.unquote(name[len(clazz.deadPropertyXattrPrefix):])
 
-        index = name.find("}")
+        index1 = name.find("{")
+        index2 = name.find("}")
 
-        if (index is -1 or not len(name) > index or not name[0] == "{"):
+        if (index1 is -1 or index2 is -1 or not len(name) > index2):
             raise ValueError("Invalid encoded name: %r" % (name,))
+        if index1 == 0:
+            uid = None
+        else:
+            uid = name[:index1]
+        propnamespace = name[index1+1:index2]
+        propname = name[index2+1:]
 
-        return (name[1:index], name[index+1:])
+        return (propnamespace, propname, uid)
 
     _encode = classmethod(_encode)
     _decode = classmethod(_decode)
@@ -107,7 +116,7 @@
         self.attrs = xattr.xattr(self.resource.fp.path)
 
 
-    def get(self, qname):
+    def get(self, qname, uid=None):
         """
         Retrieve the value of a property stored as an extended attribute on the
         wrapped path.
@@ -115,6 +124,8 @@
         @param qname: The property to retrieve as a two-tuple of namespace URI
             and local name.
 
+        @param uid: The per-user identifier for per user properties.
+
         @raise HTTPError: If there is no value associated with the given
             property.
 
@@ -122,7 +133,7 @@
             given property.
         """
         try:
-            data = self.attrs.get(self._encode(qname))
+            data = self.attrs.get(self._encode(qname, uid))
         except KeyError:
             raise HTTPError(StatusResponse(
                     responsecode.NOT_FOUND,
@@ -176,29 +187,33 @@
         return doc.root_element
 
 
-    def set(self, property):
+    def set(self, property, uid=None):
         """
         Store the given property as an extended attribute on the wrapped path.
 
+        @param uid: The per-user identifier for per user properties.
+
         @param property: A L{WebDAVElement} to store.
         """
-        key = self._encode(property.qname())
-        value = compress(property.toxml())
+        key = self._encode(property.qname(), uid)
+        value = compress(property.toxml(pretty=False))
         untilConcludes(setitem, self.attrs, key, value)
 
         # Update the resource because we've modified it
         self.resource.fp.restat()
 
 
-    def delete(self, qname):
+    def delete(self, qname, uid=None):
         """
         Remove the extended attribute from the wrapped path which stores the
         property given by C{qname}.
 
+        @param uid: The per-user identifier for per user properties.
+
         @param qname: The property to delete as a two-tuple of namespace URI
             and local name.
         """
-        key = self._encode(qname)
+        key = self._encode(qname, uid)
         try:
             try:
                 self.attrs.remove(key)
@@ -214,7 +229,7 @@
                     "Unable to delete property: " + key))
 
 
-    def contains(self, qname):
+    def contains(self, qname, uid=None):
         """
         Determine whether the property given by C{qname} is stored in an
         extended attribute of the wrapped path.
@@ -222,9 +237,11 @@
         @param qname: The property to look up as a two-tuple of namespace URI
             and local name.
 
+        @param uid: The per-user identifier for per user properties.
+
         @return: C{True} if the property exists, C{False} otherwise.
         """
-        key = self._encode(qname)
+        key = self._encode(qname, uid)
         try:
             self.attrs.get(key)
         except KeyError:
@@ -240,11 +257,13 @@
             return True
 
 
-    def list(self):
+    def list(self, uid=None, filterByUID=True):
         """
         Enumerate the property names stored in extended attributes of the
         wrapped path.
 
+        @param uid: The per-user identifier for per user properties.
+
         @return: A C{list} of property names as two-tuples of namespace URI and
             local name.
         """
@@ -257,8 +276,16 @@
                     statusForFailure(Failure()),
                     "Unable to list properties: " + self.resource.fp.path))
         else:
-            return [
+            results = [
                 self._decode(name)
-                for name
-                in attrs
-                if name.startswith(prefix)]
+                for name in attrs
+                if name.startswith(prefix)
+            ]
+            if filterByUID:
+                return [ 
+                    (namespace, name)
+                    for namespace, name, propuid in results
+                    if propuid == uid
+                ]
+            else:
+                return results

Modified: CalendarServer/trunk/twistedcaldav/authkerb.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/authkerb.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/authkerb.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -179,7 +179,12 @@
                 self.log_error("%s" % (ex[0],))
                 raise error.UnauthorizedLogin("Bad credentials for: %s (%s: %s)" % (pcreds.authnURI, ex[0], ex[1],))
             else:
-                return succeed((pcreds.authnURI, pcreds.authzURI,))
+                return succeed((
+                    pcreds.authnPrincipal.principalURL(),
+                    pcreds.authzPrincipal.principalURL(),
+                    pcreds.authnPrincipal,
+                    pcreds.authzPrincipal,
+                ))
         
         raise error.UnauthorizedLogin("Bad credentials for: %s" % (pcreds.authnURI,))
 
@@ -307,7 +312,12 @@
 
         creds = pcreds.credentials
         if isinstance(creds, NegotiateCredentials):
-            return succeed((pcreds.authnURI, pcreds.authzURI,))
+            return succeed((
+                pcreds.authnPrincipal.principalURL(),
+                pcreds.authzPrincipal.principalURL(),
+                pcreds.authnPrincipal,
+                pcreds.authzPrincipal,
+            ))
         
         raise error.UnauthorizedLogin("Bad credentials for: %s" % (pcreds.authnURI,))
 

Modified: CalendarServer/trunk/twistedcaldav/caldavxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/caldavxml.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/caldavxml.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -98,41 +98,34 @@
         self.start = parse_date_or_datetime(attributes["start"]) if "start" in attributes else None
         self.end = parse_date_or_datetime(attributes["end"]) if "end" in attributes else None
 
+    def valid(self, level=0):
+        """
+        Indicate whether the time-range is valid (must be date-time in UTC).
+        
+        @return:      True if valid, False otherwise
+        """
+        
+        if self.start is not None and not isinstance(self.start, datetime.datetime):
+            log.msg("start attribute in <time-range> is not a date-time: %s" % (self.start,))
+            return False
+        if self.end is not None and not isinstance(self.end, datetime.datetime):
+            log.msg("end attribute in <time-range> is not a date-time: %s" % (self.end,))
+            return False
+        if self.start is not None and self.start.tzinfo != utc:
+            log.msg("start attribute in <time-range> is not UTC: %s" % (self.start,))
+            return False
+        if self.end is not None and self.end.tzinfo != utc:
+            log.msg("end attribute in <time-range> is not UTC: %s" % (self.end,))
+            return False
+
+        # No other tests
+        return True
+
 class CalDAVTimeZoneElement (CalDAVTextElement):
     """
     CalDAV element containing iCalendar data with a single VTIMEZONE component.
     """
-    def __init__(self, *children, **attributes):
-        super(CalDAVTimeZoneElement, self).__init__(*children, **attributes)
 
-        # An error in the data needs to be reported as a pre-condition error rather than
-        # an XML parse error. So this test is moved out of here into a separate method that
-        # gets called and can cause the proper WebDAV DAV:error response.
-        
-        # TODO: Remove the comment above and commented code below once this has been properly tested.
-#
-#        try:
-#            calendar = self.calendar()
-#            if calendar is None: raise ValueError("No data")
-#        except ValueError, e:
-#            log.err("Invalid iCalendar data (%s): %r" % (calendar, e))
-#            raise
-#
-#        found = False
-#
-#        for subcomponent in calendar.subcomponents():
-#            if subcomponent.name() == "VTIMEZONE":
-#                if found:
-#                    raise ValueError("CalDAV:%s may not contain iCalendar data with more than one VTIMEZONE component" % (self.name,))
-#                else:
-#                    found = True
-#            else:
-#                # FIXME: Spec doesn't seem to really disallow this; it's unclear...
-#                raise ValueError("%s component not allowed in CalDAV:timezone data" % (subcomponent.name(),))
-#
-#        if not found:
-#            raise ValueError("CalDAV:%s must contain iCalendar data with a VTIMEZONE component" % (self.name,))
-
     def calendar(self):
         """
         Returns a calendar component derived from this element, which contains
@@ -170,64 +163,6 @@
 
         return found
         
-class CalDAVFilterElement (CalDAVElement):
-    """
-    CalDAV filter element.
-    """
-    def __init__(self, *children, **attributes):
-        # FIXME: is-defined is obsoleted by CalDAV-access-09.  Filter it out here for compatibility.
-        children = [c for c in children if c is not None and c.qname() != (caldav_namespace, "is-defined")]
-
-        super(CalDAVFilterElement, self).__init__(*children, **attributes)
-
-        qualifier = None
-        filters = []
-
-        for child in self.children:
-            qname = child.qname()
-            
-            if qname in (
-                (caldav_namespace, "is-not-defined"),
-                (caldav_namespace, "time-range"),
-                (caldav_namespace, "text-match"),
-            ):
-                if qualifier is not None:
-                    raise ValueError("Only one of CalDAV:time-range, CalDAV:text-match allowed")
-                qualifier = child
-
-            else:
-                filters.append(child)
-
-        if qualifier and (qualifier.qname() == (caldav_namespace, "is-not-defined")) and (len(filters) != 0):
-            raise ValueError("No other tests allowed when CalDAV:is-not-defined is present")
-            
-        self.qualifier   = qualifier
-        self.filters     = filters
-        self.filter_name = attributes["name"]
-        if isinstance(self.filter_name, unicode):
-            self.filter_name = self.filter_name.encode("utf-8")
-        self.defined     = not self.qualifier or (self.qualifier.qname() != (caldav_namespace, "is-not-defined"))
-
-    def match(self, item, access=None):
-        """
-        Returns True if the given calendar item (either a component, property or parameter value)
-        matches this filter, False otherwise.
-        """
-        
-        # Always return True for the is-not-defined case as the result of this will
-        # be negated by the caller
-        if not self.defined: return True
-
-        if self.qualifier and not self.qualifier.match(item, access): return False
-
-        if len(self.filters) > 0:
-            for filter in self.filters:
-                if filter._match(item, access):
-                    return True
-            return False
-        else:
-            return True
-
 class CalendarHomeSet (CalDAVElement):
     """
     The calendar collections URLs for this principal's calendar user.
@@ -342,7 +277,7 @@
     def __init__(self, *children, **attributes):
         super(CalendarQuery, self).__init__(*children, **attributes)
 
-        query = None
+        props = None
         filter = None
         timezone = None
 
@@ -354,9 +289,9 @@
                 (davxml.dav_namespace, "propname"),
                 (davxml.dav_namespace, "prop"    ),
             ):
-                if query is not None:
+                if props is not None:
                     raise ValueError("Only one of CalDAV:allprop, CalDAV:propname, CalDAV:prop allowed")
-                query = child
+                props = child
 
             elif qname == (caldav_namespace, "filter"):
                 filter = child
@@ -371,7 +306,7 @@
             if filter is None:
                 raise ValueError("CALDAV:filter required")
 
-        self.query  = query
+        self.props  = props
         self.filter = filter
         self.timezone = timezone
 
@@ -398,6 +333,8 @@
     @classmethod
     def fromCalendar(clazz, calendar):
         if isinstance(calendar, str):
+            if not calendar:
+                raise ValueError("Missing calendar data")
             return clazz(davxml.PCDATAElement(calendar))
         elif isinstance(calendar, iComponent):
             assert calendar.name() == "VCALENDAR", "Not a calendar: %r" % (calendar,)
@@ -488,205 +425,6 @@
         
         return False
 
-    def elementFromResource(self, resource, timezone=None):
-        """
-        Return a new CalendarData element comprised of the possibly filtered
-        calendar data from the specified resource. If no filter is being applied
-        read the data directly from the resource without parsing it. If a filter
-        is required, parse the iCal data and filter using this CalendarData.
-        @param resource: the resource whose calendar data is to be returned.
-        @param timezone: the L{Component} the VTIMEZONE to use for floating/all-day.
-        @return: an L{CalendarData} with the (filtered) calendar data.
-        """
-        return self.elementFromCalendar(resource.iCalendarText(), timezone)
-
-    def elementFromCalendar(self, calendar, timezone=None):
-        """
-        Return a new CalendarData element comprised of the possibly filtered
-        calendar.
-        @param calendar: the calendar that is to be filtered and returned.
-        @param timezone: the L{Component} the VTIMEZONE to use for floating/all-day.
-        @return: an L{CalendarData} with the (filtered) calendar data.
-        """
-        
-        # Check for filtering or not
-        filtered = self.getFromICalendar(calendar, timezone)
-        return CalendarData.fromCalendar(filtered)
-
-    def elementFromResourceWithAccessRestrictions(self, resource, access, timezone=None):
-        """
-        Return a new CalendarData element comprised of the possibly filtered
-        calendar data from the specified resource. If no filter is being applied
-        read the data directly from the resource without parsing it. If a filter
-        is required, parse the iCal data and filter using this CalendarData.
-        
-        Also, apply appropriate access restriction filtering to the data.
-
-        @param resource: the resource whose calendar data is to be returned.
-        @param access: private event access restriction level.
-        @param timezone: the L{Component} the VTIMEZONE to use for floating/all-day.
-        @return: an L{CalendarData} with the (filtered) calendar data.
-        """
-        return self.elementFromCalendarWithAccessRestrictions(resource.iCalendarText(), access, timezone)
-
-    def elementFromCalendarWithAccessRestrictions(self, calendar, access, timezone=None):
-        """
-        Return a new CalendarData element comprised of the possibly filtered
-        calendar.
-        
-        Also, apply appropriate access restriction filtering to the data.
-
-        @param calendar: the calendar that is to be filtered and returned.
-        @param access: private event access restriction level.
-        @param timezone: the L{Component} the VTIMEZONE to use for floating/all-day.
-        @return: an L{CalendarData} with the (filtered) calendar data.
-        """
-        
-        # Do normal filtering first
-        filtered_calendar = self.getFromICalendar(calendar, timezone)
-        
-        if access in (iComponent.ACCESS_CONFIDENTIAL, iComponent.ACCESS_RESTRICTED):
-            # Create a CALDAV:calendar-data element with the appropriate iCalendar Component/Property
-            # filter in place for the access restriction in use
-            
-            extra_access = ()
-            if access == iComponent.ACCESS_RESTRICTED:
-                extra_access = (
-                    Property(name="SUMMARY"),
-                    Property(name="LOCATION"),
-                )
-
-            filter = CalendarData(
-                CalendarComponent(
-                    
-                    # VCALENDAR properties
-                    Property(name="PRODID"),
-                    Property(name="VERSION"),
-                    Property(name="CALSCALE"),
-                    Property(name=iComponent.ACCESS_PROPERTY),
-
-                    # VEVENT
-                    CalendarComponent(
-                        Property(name="UID"),
-                        Property(name="RECURRENCE-ID"),
-                        Property(name="SEQUENCE"),
-                        Property(name="DTSTAMP"),
-                        Property(name="STATUS"),
-                        Property(name="TRANSP"),
-                        Property(name="DTSTART"),
-                        Property(name="DTEND"),
-                        Property(name="DURATION"),
-                        Property(name="RRULE"),
-                        Property(name="RDATE"),
-                        Property(name="EXRULE"),
-                        Property(name="EXDATE"),
-                        *extra_access,
-                        **{"name":"VEVENT"}
-                    ),
-                    
-                    # VTODO
-                    CalendarComponent(
-                        Property(name="UID"),
-                        Property(name="RECURRENCE-ID"),
-                        Property(name="SEQUENCE"),
-                        Property(name="DTSTAMP"),
-                        Property(name="STATUS"),
-                        Property(name="DTSTART"),
-                        Property(name="COMPLETED"),
-                        Property(name="DUE"),
-                        Property(name="DURATION"),
-                        Property(name="RRULE"),
-                        Property(name="RDATE"),
-                        Property(name="EXRULE"),
-                        Property(name="EXDATE"),
-                        *extra_access,
-                        **{"name":"VTODO"}
-                    ),
-                    
-                    # VJOURNAL
-                    CalendarComponent(
-                        Property(name="UID"),
-                        Property(name="RECURRENCE-ID"),
-                        Property(name="SEQUENCE"),
-                        Property(name="DTSTAMP"),
-                        Property(name="STATUS"),
-                        Property(name="TRANSP"),
-                        Property(name="DTSTART"),
-                        Property(name="RRULE"),
-                        Property(name="RDATE"),
-                        Property(name="EXRULE"),
-                        Property(name="EXDATE"),
-                        *extra_access,
-                        **{"name":"VJOURNAL"}
-                    ),
-                    
-                    # VFREEBUSY
-                    CalendarComponent(
-                        Property(name="UID"),
-                        Property(name="DTSTAMP"),
-                        Property(name="DTSTART"),
-                        Property(name="DTEND"),
-                        Property(name="DURATION"),
-                        Property(name="FREEBUSY"),
-                        *extra_access,
-                        **{"name":"VFREEBUSY"}
-                    ),
-                    
-                    # VTIMEZONE
-                    CalendarComponent(
-                        AllProperties(),
-                        AllComponents(),
-                        name="VTIMEZONE",
-                    ),
-                    name="VCALENDAR",
-                ),
-            )
-
-            # Now "filter" the resource calendar data through the CALDAV:calendar-data element
-            return filter.elementFromCalendar(filtered_calendar, timezone)
-        else:
-            return CalendarData.fromCalendar(filtered_calendar)
-
-    def getFromICalendar(self, calendar, timezone=None):
-        """
-        @param timezone: the L{Component} the VTIMEZONE to use for floating/all-day.
-
-        Returns a calendar object containing the data in the given calendar
-        which is specified by this CalendarData.
-        """
-        if calendar is None or isinstance(calendar, str) and not calendar:
-            raise ValueError("Not a calendar: %r" % (calendar,))
-
-        # Empty element: get all data
-        if not self.children: return calendar
-
-        # If we were passed a string, parse it out as a Component
-        if isinstance(calendar, str):
-            try:
-                calendar = iComponent.fromString(calendar)
-            except ValueError:
-                raise ValueError("Not a calendar: %r" % (calendar,))
-
-        if calendar is None or calendar.name() != "VCALENDAR":
-            raise ValueError("Not a calendar: %r" % (calendar,))
-
-        # Pre-process the calendar data based on expand and limit options
-        if self.freebusy_set:
-            calendar = self.limitFreeBusy(calendar)
-
-        # Filter data based on any provided CALDAV:comp element, or use all current data
-        if self.component is not None:
-            calendar = self.component.getFromICalendar(calendar)
-        
-        # Post-process the calendar data based on the expand and limit options
-        if self.recurrence_set:
-            if isinstance(self.recurrence_set, LimitRecurrenceSet):
-                calendar = self.limitRecurrence(calendar)
-            elif isinstance(self.recurrence_set, Expand):
-                calendar = self.expandRecurrence(calendar, timezone)
-        
-        return calendar
-
     def calendar(self):
         """
         Returns a calendar component derived from this element.
@@ -710,55 +448,6 @@
 
         return str(data)
 
-    def expandRecurrence(self, calendar, timezone=None):
-        """
-        Expand the recurrence set into individual items.
-        @param calendar: the L{Component} for the calendar to operate on.
-        @param timezone: the L{Component} the VTIMEZONE to use for floating/all-day.
-        @return: the L{Component} for the result.
-        """
-        return calendar.expand(self.recurrence_set.start, self.recurrence_set.end, timezone)
-    
-    def limitRecurrence(self, calendar):
-        """
-        Limit the set of overridden instances returned to only those
-        that are needed to describe the range of instances covered
-        by the specified time range.
-        @param calendar: the L{Component} for the calendar to operate on.
-        @return: the L{Component} for the result.
-        """
-        raise NotImplementedError()
-        return calendar
-    
-    def limitFreeBusy(self, calendar):
-        """
-        Limit the range of any FREEBUSY properties in the calendar, returning
-        a new calendar if limits were applied, or the same one if no limits were applied.
-        @param calendar: the L{Component} for the calendar to operate on.
-        @return: the L{Component} for the result.
-        """
-        
-        # First check for any VFREEBUSYs - can ignore limit if there are none
-        if calendar.mainType() != "VFREEBUSY":
-            return calendar
-        
-        # Create duplicate calendar and filter FREEBUSY properties
-        calendar = calendar.duplicate()
-        for component in calendar.subcomponents():
-            if component.name() != "VFREEBUSY":
-                continue
-            for property in component.properties("FREEBUSY"):
-                newvalue = []
-                for period in property.value():
-                    clipped = clipPeriod(period, (self.freebusy_set.start, self.freebusy_set.end))
-                    if clipped:
-                        newvalue.append(clipped)
-                if len(newvalue):
-                    property.setValue(newvalue)
-                else:
-                    component.removeProperty(property)
-        return calendar
-
 class CalendarComponent (CalDAVElement):
     """
     Defines which component types to return.
@@ -936,79 +625,7 @@
 
     allowed_children = { (caldav_namespace, "comp-filter"): (1, 1) }
 
-    def match(self, component, access=None):
-        """
-        Returns True if the given calendar component matches this filter, False
-        otherwise.
-        """
-        
-        # We only care about certain access restrictions.
-        if access not in (iComponent.ACCESS_CONFIDENTIAL, iComponent.ACCESS_RESTRICTED):
-            access = None
-
-        # We need to prepare ourselves for a time-range query by pre-calculating
-        # the set of instances up to the latest time-range limit. That way we can
-        # avoid having to do some form of recurrence expansion for each query sub-part.
-        maxend, isStartTime = self.getmaxtimerange()
-        if maxend:
-            if isStartTime:
-                if component.isRecurringUnbounded():
-                    # Unbounded recurrence is always within a start-only time-range
-                    instances = None
-                else:
-                    # Expand the instances up to infinity
-                    instances = component.expandTimeRanges(datetime.datetime(2100, 1, 1, 0, 0, 0, tzinfo=utc), ignoreInvalidInstances=True)
-            else:
-                instances = component.expandTimeRanges(maxend, ignoreInvalidInstances=True)
-        else:
-            instances = None
-        self.children[0].setInstances(instances)
-
-        # <filter> contains exactly one <comp-filter>
-        return self.children[0].match(component, access)
-
-    def valid(self):
-        """
-        Indicate whether this filter element's structure is valid wrt iCalendar
-        data object model.
-        
-        @return: True if valid, False otherwise
-        """
-        
-        # Must have one child element for VCALENDAR
-        return self.children[0].valid(0)
-        
-    def settimezone(self, tzelement):
-        """
-        Set the default timezone to use with this query.
-        @param calendar: a L{Component} for the VCALENDAR containing the one
-            VTIMEZONE that we want
-        @return: the L{datetime.tzinfo} derived from the VTIMEZONE or utc.
-        """
-        assert tzelement is None or isinstance(tzelement, CalDAVTimeZoneElement)
-
-        if tzelement is not None:
-            calendar = tzelement.calendar()
-            if calendar is not None:
-                for subcomponent in calendar.subcomponents():
-                    if subcomponent.name() == "VTIMEZONE":
-                        # <filter> contains exactly one <comp-filter>
-                        tzinfo = subcomponent.gettzinfo()
-                        self.children[0].settzinfo(tzinfo)
-                        return tzinfo
-
-        # Default to using utc tzinfo
-        self.children[0].settzinfo(utc)
-        return utc
-
-    def getmaxtimerange(self):
-        """
-        Get the date farthest into the future in any time-range elements
-        """
-        
-        return self.children[0].getmaxtimerange(None, False)
-
-class ComponentFilter (CalDAVFilterElement):
+class ComponentFilter (CalDAVElement):
     """
     Limits a search to only the chosen component types.
     (CalDAV-access-09, section 9.6.1)
@@ -1016,7 +633,6 @@
     name = "comp-filter"
 
     allowed_children = {
-        (caldav_namespace, "is-defined"     ): (0, 1), # FIXME: obsoleted in CalDAV-access-09
         (caldav_namespace, "is-not-defined" ): (0, 1),
         (caldav_namespace, "time-range"     ): (0, 1),
         (caldav_namespace, "comp-filter"    ): (0, None),
@@ -1024,153 +640,7 @@
     }
     allowed_attributes = { "name": True }
 
-    def match(self, item, access):
-        """
-        Returns True if the given calendar item (which is a component)
-        matches this filter, False otherwise.
-        This specialization uses the instance matching option of the time-range filter
-        to minimize instance expansion.
-        """
-
-        # Always return True for the is-not-defined case as the result of this will
-        # be negated by the caller
-        if not self.defined: return True
-
-        if self.qualifier and not self.qualifier.matchinstance(item, self.instances): return False
-
-        if len(self.filters) > 0:
-            for filter in self.filters:
-                if filter._match(item, access):
-                    return True
-            return False
-        else:
-            return True
-
-    def _match(self, component, access):
-        # At least one subcomponent must match (or is-not-defined is set)
-        for subcomponent in component.subcomponents():
-            # If access restrictions are in force, restrict matching to specific components only.
-            # In particular do not match VALARM.
-            if access and subcomponent.name() not in ("VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VTIMEZONE",):
-                continue
-            
-            # Try to match the component name
-            if isinstance(self.filter_name, str):
-                if subcomponent.name() != self.filter_name: continue
-            else:
-                if subcomponent.name() not in self.filter_name: continue
-            if self.match(subcomponent, access): break
-        else:
-            return not self.defined
-        return self.defined
-        
-    def setInstances(self, instances):
-        """
-        Give the list of instances to each comp-filter element.
-        @param instances: the list of instances.
-        """
-        self.instances = instances
-        for compfilter in [x for x in self.filters if isinstance(x, ComponentFilter)]:
-            compfilter.setInstances(instances)
-        
-    def valid(self, level):
-        """
-        Indicate whether this filter element's structure is valid wrt iCalendar
-        data object model.
-        
-        @param level: the nesting level of this filter element, 0 being the top comp-filter.
-        @return:      True if valid, False otherwise
-        """
-        
-        # Check for time-range
-        timerange = self.qualifier and isinstance(self.qualifier, TimeRange)
-
-        if level == 0:
-            # Must have VCALENDAR at the top
-            if (self.filter_name != "VCALENDAR") or timerange:
-                log.msg("Top-level comp-filter must be VCALENDAR, instead: %s" % (self.filter_name,))
-                return False
-        elif level == 1:
-            # Disallow VCALENDAR, VALARM, STANDARD, DAYLIGHT, AVAILABLE at the top, everything else is OK
-            if self.filter_name in ("VCALENDAR", "VALARM", "STANDARD", "DAYLIGHT", "AVAILABLE"):
-                log.msg("comp-filter wrong component type: %s" % (self.filter_name,))
-                return False
-            
-            # time-range only on VEVENT, VTODO, VJOURNAL, VFREEBUSY, VAVAILABILITY
-            if timerange and self.filter_name not in ("VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VAVAILABILITY"):
-                log.msg("time-range cannot be used with component %s" % (self.filter_name,))
-                return False
-        elif level == 2:
-            # Disallow VCALENDAR, VTIMEZONE, VEVENT, VTODO, VJOURNAL, VFREEBUSY, VAVAILABILITY at the top, everything else is OK
-            if (self.filter_name in ("VCALENDAR", "VTIMEZONE", "VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VAVAILABILITY")):
-                log.msg("comp-filter wrong sub-component type: %s" % (self.filter_name,))
-                return False
-            
-            # time-range only on VALARM, AVAILABLE
-            if timerange and self.filter_name not in ("VALARM", "AVAILABLE",):
-                log.msg("time-range cannot be used with sub-component %s" % (self.filter_name,))
-                return False
-        else:
-            # Disallow all standard iCal components anywhere else
-            if (self.filter_name in ("VCALENDAR", "VTIMEZONE", "VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VALARM", "STANDARD", "DAYLIGHT", "AVAILABLE")) or timerange:
-                log.msg("comp-filter wrong standard component type: %s" % (self.filter_name,))
-                return False
-        
-        # Test each property
-        for propfilter in [x for x in self.filters if isinstance(x, PropertyFilter)]:
-            if not propfilter.valid():
-                return False
-
-        # Test each component
-        for compfilter in [x for x in self.filters if isinstance(x, ComponentFilter)]:
-            if not compfilter.valid(level + 1):
-                return False
-
-        # Test the time-range
-        if timerange:
-            if not self.qualifier.valid():
-                return False
-
-        return True
-
-    def settzinfo(self, tzinfo):
-        """
-        Set the default timezone to use with this query.
-        @param tzinfo: a L{datetime.tzinfo} to use.
-        """
-        
-        # Give tzinfo to any TimeRange we have
-        if isinstance(self.qualifier, TimeRange):
-            self.qualifier.settzinfo(tzinfo)
-        
-        # Pass down to sub components/properties
-        for x in self.filters:
-            x.settzinfo(tzinfo)
-
-    def getmaxtimerange(self, currentMaximum, currentIsStartTime):
-        """
-        Get the date furthest into the future in any time-range elements
-        
-        @param currentMaximum: current future value to compare with
-        @type currentMaximum: L{datetime.datetime}
-        """
-        
-        # Give tzinfo to any TimeRange we have
-        isStartTime = False
-        if isinstance(self.qualifier, TimeRange):
-            isStartTime = self.qualifier.end is None
-            compareWith = self.qualifier.start if isStartTime else self.qualifier.end
-            if currentMaximum is None or currentMaximum < compareWith:
-                currentMaximum = compareWith
-                currentIsStartTime = isStartTime
-        
-        # Pass down to sub components/properties
-        for x in self.filters:
-            currentMaximum, currentIsStartTime = x.getmaxtimerange(currentMaximum, currentIsStartTime)
-
-        return currentMaximum, currentIsStartTime
-
-class PropertyFilter (CalDAVFilterElement):
+class PropertyFilter (CalDAVElement):
     """
     Limits a search to specific properties.
     (CalDAV-access-09, section 9.6.2)
@@ -1178,7 +648,6 @@
     name = "prop-filter"
 
     allowed_children = {
-        (caldav_namespace, "is-defined"     ): (0, 1), # FIXME: obsoleted in CalDAV-access-09
         (caldav_namespace, "is-not-defined" ): (0, 1),
         (caldav_namespace, "time-range"     ): (0, 1),
         (caldav_namespace, "text-match"     ): (0, 1),
@@ -1186,80 +655,7 @@
     }
     allowed_attributes = { "name": True }
 
-    def _match(self, component, access):
-        # When access restriction is in force, we need to only allow matches against the properties
-        # allowed by the access restriction level.
-        if access:
-            allowedProperties = iComponent.confidentialPropertiesMap.get(component.name(), None)
-            if allowedProperties and access == iComponent.ACCESS_RESTRICTED:
-                allowedProperties += iComponent.extraRestrictedProperties
-        else:
-            allowedProperties = None
-
-        # At least one property must match (or is-not-defined is set)
-        for property in component.properties():
-            # Apply access restrictions, if any.
-            if allowedProperties is not None and property.name() not in allowedProperties:
-                continue
-            if property.name() == self.filter_name and self.match(property, access): break
-        else:
-            return not self.defined
-        return self.defined
-
-    def valid(self):
-        """
-        Indicate whether this filter element's structure is valid wrt iCalendar
-        data object model.
-        
-        @return:      True if valid, False otherwise
-        """
-        
-        # Check for time-range
-        timerange = self.qualifier and isinstance(self.qualifier, TimeRange)
-        
-        # time-range only on COMPLETED, CREATED, DTSTAMP, LAST-MODIFIED
-        if timerange and self.filter_name not in ("COMPLETED", "CREATED", "DTSTAMP", "LAST-MODIFIED"):
-            log.msg("time-range cannot be used with property %s" % (self.filter_name,))
-            return False
-
-        # Test the time-range
-        if timerange:
-            if not self.qualifier.valid():
-                return False
-
-        # No other tests
-        return True
-
-    def settzinfo(self, tzinfo):
-        """
-        Set the default timezone to use with this query.
-        @param tzinfo: a L{datetime.tzinfo} to use.
-        """
-        
-        # Give tzinfo to any TimeRange we have
-        if isinstance(self.qualifier, TimeRange):
-            self.qualifier.settzinfo(tzinfo)
-
-    def getmaxtimerange(self, currentMaximum, currentIsStartTime):
-        """
-        Get the date furthest into the future in any time-range elements
-        
-        @param currentMaximum: current future value to compare with
-        @type currentMaximum: L{datetime.datetime}
-        """
-        
-        # Give tzinfo to any TimeRange we have
-        isStartTime = False
-        if isinstance(self.qualifier, TimeRange):
-            isStartTime = self.qualifier.end is None
-            compareWith = self.qualifier.start if isStartTime else self.qualifier.end
-            if currentMaximum is None or currentMaximum < compareWith:
-                currentMaximum = compareWith
-                currentIsStartTime = isStartTime
-
-        return currentMaximum, currentIsStartTime
-
-class ParameterFilter (CalDAVFilterElement):
+class ParameterFilter (CalDAVElement):
     """
     Limits a search to specific parameters.
     (CalDAV-access-09, section 9.6.3)
@@ -1267,42 +663,11 @@
     name = "param-filter"
 
     allowed_children = {
-        (caldav_namespace, "is-defined"     ): (0, 1), # FIXME: obsoleted in CalDAV-access-09
         (caldav_namespace, "is-not-defined" ): (0, 1),
         (caldav_namespace, "text-match"     ): (0, 1),
     }
     allowed_attributes = { "name": True }
 
-    def _match(self, property, access):
-        # We have to deal with the problem that the 'Native' form of a property
-        # will be missing the TZID parameter due to the conversion performed. Converting
-        # to non-native for the entire calendar object causes problems elsewhere, so its
-        # best to do it here for this one special case.
-        if self.filter_name == "TZID":
-            transformed = property.transformAllFromNative()
-        else:
-            transformed = False
-
-        # At least one property must match (or is-not-defined is set)
-        result = not self.defined
-        for parameterName in property.params().keys():
-            if parameterName == self.filter_name and self.match(property.params()[parameterName], access):
-                result = self.defined
-                break
-
-        if transformed:
-            property.transformAllToNative()
-        return result
-
-class IsDefined (CalDAVEmptyElement):
-    """
-    FIXME: Removed from spec.
-    """
-    name = "is-defined"
-
-    def match(self, component, access):
-        return component is not None
-
 class IsNotDefined (CalDAVEmptyElement):
     """
     Specifies that the named iCalendar item does not exist.
@@ -1310,13 +675,6 @@
     """
     name = "is-not-defined"
 
-    def match(self, component, access):
-        # Oddly, this needs always to return True so that it appears there is
-        # a match - but we then "negate" the result if is-not-defined is set.
-        # Actually this method should never be called as we special case the
-        # is-not-defined option.
-        return True
-
 class TextMatch (CalDAVTextElement):
     """
     Specifies a substring match on a property or parameter value.
@@ -1344,68 +702,6 @@
         "negate-condition": False
     }
 
-    def __init__(self, *children, **attributes):
-        super(TextMatch, self).__init__(*children, **attributes)
-
-        if "caseless" in attributes:
-            caseless = attributes["caseless"]
-            if caseless == "yes":
-                self.caseless = True
-            elif caseless == "no":
-                self.caseless = False
-        else:
-            self.caseless = True
-
-        if "negate-condition" in attributes:
-            negate = attributes["negate-condition"]
-            if negate == "yes":
-                self.negate = True
-            elif caseless == "no":
-                self.negate = False
-        else:
-            self.negate = False
-
-    def match(self, item, access):
-        """
-        Match the text for the item.
-        If the item is a property, then match the property value,
-        otherwise it may be a list of parameter values - try to match anyone of those
-        """
-        if item is None: return False
-
-        if isinstance(item, iProperty):
-            values = [item.value()]
-        else:
-            values = item
-
-        test = unicode(str(self), "utf-8")
-        if self.caseless:
-            test = test.lower()
-
-        def _textCompare(s):
-            if self.caseless:
-                if s.lower().find(test) != -1:
-                    return True, not self.negate
-            else:
-                if s.find(test) != -1:
-                    return True, not self.negate
-            return False, False
-
-        for value in values:
-            # NB Its possible that we have a text list value which appears as a Python list,
-            # so we need to check for that an iterate over the list.
-            if isinstance(value, list):
-                for subvalue in value:
-                    matched, result = _textCompare(subvalue)
-                    if matched:
-                        return result
-            else:
-                matched, result = _textCompare(value)
-                if matched:
-                    return result
-        
-        return self.negate
-
 class TimeZone (CalDAVTimeZoneElement):
     """
     Specifies a time zone component.
@@ -1420,103 +716,6 @@
     """
     name = "time-range"
 
-    def __init__(self, *children, **attributes):
-        super(TimeRange, self).__init__(*children, **attributes)
-        self.tzinfo = None
-
-    def settzinfo(self, tzinfo):
-        """
-        Set the default timezone to use with this query.
-        @param tzinfo: a L{datetime.tzinfo} to use.
-        """
-        
-        # Give tzinfo to any TimeRange we have
-        self.tzinfo = tzinfo
-
-    def valid(self):
-        """
-        Indicate whether the time-range is valid (must be date-time in UTC).
-        
-        @return:      True if valid, False otherwise
-        """
-        
-        if self.start is not None and not isinstance(self.start, datetime.datetime):
-            log.msg("start attribute in <time-range> is not a date-time: %s" % (self.start,))
-            return False
-        if self.end is not None and not isinstance(self.end, datetime.datetime):
-            log.msg("end attribute in <time-range> is not a date-time: %s" % (self.end,))
-            return False
-        if self.start is not None and self.start.tzinfo != utc:
-            log.msg("start attribute in <time-range> is not UTC: %s" % (self.start,))
-            return False
-        if self.end is not None and self.end.tzinfo != utc:
-            log.msg("end attribute in <time-range> is not UTC: %s" % (self.end,))
-            return False
-
-        # No other tests
-        return True
-
-    def match(self, property, access):
-        """
-        NB This is only called when doing a time-range match on a property.
-        """
-        if property is None:
-            return False
-        else:
-            return property.containsTimeRange(self.start, self.end, self.tzinfo)
-
-    def matchinstance(self, component, instances):
-        """
-        Test whether this time-range element causes a match to the specified component
-        using the specified set of instances to determine the expanded time ranges.
-        @param component: the L{Component} to test.
-        @param instances: the list of expanded instances.
-        @return: True if the time-range query matches, False otherwise.
-        """
-        if component is None:
-            return False
-        
-        assert instances is not None or self.end is None, "Failure to expand instance for time-range filter: %r" % (self,)
-        
-        # Special case open-ended unbounded
-        if instances is None:
-            if component.getRecurrenceIDUTC() is None:
-                return True
-            else:
-                # See if the overridden component's start is past the start
-                start, _ignore_end = component.getEffectiveStartEnd()
-                if start is None:
-                    return True
-                else:
-                    return start >= self.start
-
-        # Handle alarms as a special case
-        alarms = (component.name() == "VALARM")
-        if alarms:
-            testcomponent = component._parent
-        else:
-            testcomponent = component
-            
-        for key in instances:
-            instance = instances[key]
-            
-            # First make sure components match
-            if not testcomponent.same(instance.component):
-                continue
-
-            if alarms:
-                # Get all the alarm triggers for this instance and test each one
-                triggers = instance.getAlarmTriggers()
-                for trigger in triggers:
-                    if timeRangesOverlap(trigger, None, self.start, self.end, self.tzinfo):
-                        return True
-            else:
-                # Regular instance overlap test
-                if timeRangesOverlap(instance.start, instance.end, self.start, self.end, self.tzinfo):
-                    return True
-
-        return False
-
 class CalendarMultiGet (CalDAVElement):
     """
     CalDAV report used to retrieve specific calendar component items via their

Modified: CalendarServer/trunk/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/customxml.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/customxml.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2010 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@
 from twext.web2.dav.element.base import twisted_private_namespace
 from twext.web2.dav import davxml
 
+from twistedcaldav import caldavxml
 from twistedcaldav.ical import Component as iComponent
 
 from vobject.icalendar import utc
@@ -49,10 +50,14 @@
     "calendarserver-private-comments",
 )
 
-calendarserver_principal_property_search = (
+calendarserver_principal_property_search_compliance = (
     "calendarserver-principal-property-search",
 )
 
+calendarserver_sharing_compliance = (
+    "calendarserver-sharing",
+)
+
 class TwistedCalendarAccessProperty (davxml.WebDAVTextElement):
     """
     Contains the calendar access level (private events) for the resource.
@@ -571,7 +576,310 @@
     namespace = calendarserver_namespace
     name = "auto-schedule"
 
+class ReadAccess (davxml.WebDAVEmptyElement):
+    """
+    Denotes read and update attendee partstat on a shared calendar.
+    """
+    namespace = calendarserver_namespace
+    name = "read"
+
+class ReadWriteAccess (davxml.WebDAVEmptyElement):
+    """
+    Denotes read and write access on a shared calendar.
+    """
+    namespace = calendarserver_namespace
+    name = "read-write"
+
+class ReadWriteScheduleAccess (davxml.WebDAVEmptyElement):
+    """
+    Denotes read and write and schedule access on a shared calendar.
+    """
+    namespace = calendarserver_namespace
+    name = "read-write-schedule"
+
+class UID (davxml.WebDAVTextElement):
+    namespace = calendarserver_namespace
+    name = "uid"
+
+class InReplyTo (davxml.WebDAVTextElement):
+    namespace = calendarserver_namespace
+    name = "in-reply-to"
+
 ##
+# Notifications
+##
+
+class SharedOwner (davxml.WebDAVEmptyElement):
+    """
+    Denotes a shared collection.
+    """
+    namespace = calendarserver_namespace
+    name = "shared-owner"
+
+class Shared (davxml.WebDAVEmptyElement):
+    """
+    Denotes a shared collection.
+    """
+    namespace = calendarserver_namespace
+    name = "shared"
+
+class Subscribed (davxml.WebDAVEmptyElement):
+    """
+    Denotes a subscribed calendar collection.
+    """
+    namespace = calendarserver_namespace
+    name = "subscribed"
+
+class SharedURL (davxml.WebDAVTextElement):
+    """
+    The source url for a shared calendar.
+    """
+    namespace = calendarserver_namespace
+    name = "shared-url"
+
+class SharedCalendar (davxml.WebDAVElement):
+    """
+    The url for a shared calendar.
+    """
+    namespace = calendarserver_namespace
+    name = "shared-calendar"
+
+    allowed_children = {
+        davxml.HRef.qname()    : (1, 1),
+    }
+
+class SharedAcceptEmailNotification (davxml.WebDAVTextElement):
+    """
+    The accept email flag for a shared calendar.
+    """
+    namespace = calendarserver_namespace
+    name = "shared-accept-email-notification"
+
+class Birthday (davxml.WebDAVEmptyElement):
+    """
+    Denotes a birthday calendar collection.
+    """
+    namespace = calendarserver_namespace
+    name = "birthday"
+
+class InviteShare (davxml.WebDAVElement):
+    namespace = calendarserver_namespace
+    name = "share"
+
+    allowed_children = {
+        (calendarserver_namespace, "set" )    : (0, None),
+        (calendarserver_namespace, "remove" ) : (0, None),
+    }
+
+class InviteSet (davxml.WebDAVElement):
+    namespace = calendarserver_namespace
+    name = "set"
+
+    allowed_children = {
+        (dav_namespace,            "href" )                : (1, 1),
+        (calendarserver_namespace, "summary" )             : (0, 1),
+        (calendarserver_namespace, "read" )                : (0, 1),
+        (calendarserver_namespace, "read-write" )          : (0, 1),
+        (calendarserver_namespace, "read-write-schedule" ) : (0, 1),
+    }
+
+class InviteRemove (davxml.WebDAVElement):
+    namespace = calendarserver_namespace
+    name = "remove"
+
+    allowed_children = {
+        (dav_namespace,            "href" )                : (1, 1),
+        (calendarserver_namespace, "read" )                : (0, 1),
+        (calendarserver_namespace, "read-write" )          : (0, 1),
+        (calendarserver_namespace, "read-write-schedule" ) : (0, 1),
+    }
+
+class InviteUser (davxml.WebDAVElement):
+    namespace = calendarserver_namespace
+    name = "user"
+
+    allowed_children = {
+        (calendarserver_namespace, "href" )              : (1, 1),
+        (calendarserver_namespace, "invite-noresponse" ) : (0, 1),
+        (calendarserver_namespace, "invite-deleted" )    : (0, 1),
+        (calendarserver_namespace, "invite-accepted" )   : (0, 1),
+        (calendarserver_namespace, "invite-declined" )   : (0, 1),
+        (calendarserver_namespace, "invite-invalid" )    : (0, 1),
+        (calendarserver_namespace, "access" )            : (1, 1),
+        (calendarserver_namespace, "summary" )           : (0, 1),
+    }
+
+class InviteAccess (davxml.WebDAVElement):
+    namespace = calendarserver_namespace
+    name = "access"
+
+    allowed_children = {
+        (calendarserver_namespace, "read" )               : (0, 1),
+        (calendarserver_namespace, "read-write" )         : (0, 1),
+        (calendarserver_namespace, "read-write-schedule" ): (0, 1),
+    }
+
+class Invite (davxml.WebDAVElement):
+    namespace = calendarserver_namespace
+    name = "invite"
+
+    allowed_children = {
+        (calendarserver_namespace, "user" )   : (0, None),
+    }
+
+class InviteSummary (davxml.WebDAVTextElement):
+    namespace = calendarserver_namespace
+    name = "summary"
+
+class InviteStatusNoResponse (davxml.WebDAVEmptyElement):
+    namespace = calendarserver_namespace
+    name = "invite-noresponse"
+
+class InviteStatusDeleted (davxml.WebDAVEmptyElement):
+    namespace = calendarserver_namespace
+    name = "invite-deleted"
+
+class InviteStatusAccepted (davxml.WebDAVEmptyElement):
+    namespace = calendarserver_namespace
+    name = "invite-accepted"
+
+class InviteStatusDeclined (davxml.WebDAVEmptyElement):
+    namespace = calendarserver_namespace
+    name = "invite-declined"
+
+class InviteStatusInvalid (davxml.WebDAVEmptyElement):
+    namespace = calendarserver_namespace
+    name = "invite-invalid"
+
+class HostURL (davxml.WebDAVElement):
+    """
+    The source for a shared calendar
+    """
+    namespace = calendarserver_namespace
+    name = "hosturl"
+
+    allowed_children = { (dav_namespace, "href"): (0, None) }
+
+class Organizer (davxml.WebDAVElement):
+    """
+    The organizer for a shared calendar
+    """
+    namespace = calendarserver_namespace
+    name = "organizer"
+
+    allowed_children = { (dav_namespace, "href"): (0, None) }
+
+class InviteNotification (davxml.WebDAVElement):
+    namespace = calendarserver_namespace
+    name = "invite-notification"
+
+    allowed_children = {
+        UID.qname()                      : (0, 1),
+        (dav_namespace, "href")          : (0, 1),
+        InviteStatusNoResponse.qname()   : (0, 1),
+        InviteStatusDeleted.qname()      : (0, 1),
+        InviteStatusAccepted.qname()     : (0, 1),
+        InviteStatusDeclined.qname()     : (0, 1),
+        InviteAccess.qname()             : (0, 1),
+        HostURL.qname()                  : (0, 1),
+        Organizer.qname()                : (0, 1),
+        InviteSummary.qname()            : (0, 1),
+    }
+
+    allowed_attributes = {
+        "shared-type" : True,
+    }
+
+class InviteReply (davxml.WebDAVElement):
+    namespace = calendarserver_namespace
+    name = "invite-reply"
+
+    allowed_children = {
+        (dav_namespace, "href")          : (0, 1),
+        InviteStatusAccepted.qname()     : (0, 1),
+        InviteStatusDeclined.qname()     : (0, 1),
+        HostURL.qname()                  : (0, 1),
+        InReplyTo.qname()                : (0, 1),
+        InviteSummary.qname()            : (0, 1),
+    }
+
+class ResourceUpdateAdded(davxml.WebDAVEmptyElement):
+    namespace = calendarserver_namespace
+    name = "resource-added-notification"
+
+class ResourceUpdateUpdated(davxml.WebDAVEmptyElement):
+    namespace = calendarserver_namespace
+    name = "resource-updated-notification"
+
+class ResourceDeletedUpdated(davxml.WebDAVEmptyElement):
+    namespace = calendarserver_namespace
+    name = "resource-deleted-notification"
+
+class ResourceUpdateNotification (davxml.WebDAVElement):
+    namespace = calendarserver_namespace
+    name = "resource-update-notification"
+
+    allowed_children = {
+        (dav_namespace, "href")          : (0, 1),
+        UID.qname()                      : (0, 1),
+        ResourceUpdateAdded.qname()      : (0, 1),
+        ResourceUpdateUpdated.qname()    : (0, 1),
+        ResourceDeletedUpdated.qname()   : (0, 1),
+    }
+
+class SharedCalendarUpdateNotification (davxml.WebDAVElement):
+    namespace = calendarserver_namespace
+    name = "shared-update-notification"
+
+    allowed_children = {
+        HostURL.qname()                  : (0, 1), # The shared calendar url
+        (dav_namespace, "href")          : (0, 1), # Email userid that was invited
+        InviteStatusDeleted.qname()      : (0, 1), # What the user did...
+        InviteStatusAccepted.qname()     : (0, 1),
+        InviteStatusDeclined.qname()     : (0, 1),
+    }
+
+class Notification (davxml.WebDAVElement):
+    """
+    Denotes a notification collection, or a notification message.
+    """
+    namespace = calendarserver_namespace
+    name = "notification"
+
+    allowed_children = {
+        DTStamp.qname()                            : (0, None),
+        InviteNotification.qname()                 : (0, None),
+        InviteReply.qname()                        : (0, None),
+        ResourceUpdateNotification.qname()         : (0, None),
+        SharedCalendarUpdateNotification.qname()   : (0, None),
+    }
+
+class NotificationURL (davxml.WebDAVElement):
+    """
+    A principal property to indicate the notification collection for the principal.
+    """
+    namespace = calendarserver_namespace
+    name = "notification-URL"
+    hidden = True
+    protected = True
+
+    allowed_children = { (davxml.dav_namespace, "href"): (0, 1) }
+
+class NotificationType (davxml.WebDAVElement):
+    """
+    A property to indicate what type of notification the resource represents.
+    """
+    namespace = calendarserver_namespace
+    name = "notification-type"
+    hidden = True
+    protected = True
+
+    allowed_children = {
+        InviteNotification.qname()                 : (0, None),
+        InviteReply.qname()                        : (0, None),
+    }
+
+##
 # Extensions to davxml.ResourceType
 ##
 
@@ -582,3 +890,5 @@
 davxml.ResourceType.timezones = davxml.ResourceType(Timezones())
 davxml.ResourceType.ischeduleinbox = davxml.ResourceType(IScheduleInbox())
 davxml.ResourceType.freebusyurl = davxml.ResourceType(FreeBusyURL())
+davxml.ResourceType.notification = davxml.ResourceType(davxml.Collection(), Notification())
+davxml.ResourceType.sharedcalendar = davxml.ResourceType(davxml.Collection(), caldavxml.Calendar(), SharedOwner())

Deleted: CalendarServer/trunk/twistedcaldav/datafilters/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/datafilters/__init__.py	2010-04-07 19:28:58 UTC (rev 5440)
+++ CalendarServer/trunk/twistedcaldav/datafilters/__init__.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -1,47 +0,0 @@
-##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from vobject.base import registerBehavior
-from vobject.icalendar import VCalendarComponentBehavior, VCalendar2_0
-
-"""
-Data filtering module.
-"""
-
-# This is where we register our special components with vobject
-
-class X_CALENDARSERVER_PERUSER(VCalendarComponentBehavior):
-    name='X-CALENDARSERVER-PERUSER'
-    description='A component used to encapsulate per-user data.'
-    sortFirst = ('uid', 'x-calendarserver-peruser-uid')
-    knownChildren = {
-        'UID':                            (1, 1, None),#min, max, behaviorRegistry id
-        'X-CALENDARSERVER-PERUSER-UID':   (1, 1, None),
-        'X-CALENDARSERVER-PERINSTANCE':   (0, None, None),
-    }
-      
-registerBehavior(X_CALENDARSERVER_PERUSER)
-VCalendar2_0.knownChildren['X-CALENDARSERVER-PERUSER'] = (0, None, None)
-
-class X_CALENDARSERVER_PERINSTANCE(VCalendarComponentBehavior):
-    name='X-CALENDARSERVER-PERINSTANCE'
-    description='A component used to encapsulate per-user instance data.'
-    sortFirst = ('recurrence-id',)
-    knownChildren = {
-        'RECURRENCE-ID':(0, 1, None),#min, max, behaviorRegistry id
-    }
-      
-registerBehavior(X_CALENDARSERVER_PERINSTANCE)

Copied: CalendarServer/trunk/twistedcaldav/datafilters/__init__.py (from rev 5440, CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/datafilters/__init__.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/datafilters/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/datafilters/__init__.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -0,0 +1,47 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from vobject.base import registerBehavior
+from vobject.icalendar import VCalendarComponentBehavior, VCalendar2_0
+
+"""
+Data filtering module.
+"""
+
+# This is where we register our special components with vobject
+
+class X_CALENDARSERVER_PERUSER(VCalendarComponentBehavior):
+    name='X-CALENDARSERVER-PERUSER'
+    description='A component used to encapsulate per-user data.'
+    sortFirst = ('uid', 'x-calendarserver-peruser-uid')
+    knownChildren = {
+        'UID':                            (1, 1, None),#min, max, behaviorRegistry id
+        'X-CALENDARSERVER-PERUSER-UID':   (1, 1, None),
+        'X-CALENDARSERVER-PERINSTANCE':   (0, None, None),
+    }
+      
+registerBehavior(X_CALENDARSERVER_PERUSER)
+VCalendar2_0.knownChildren['X-CALENDARSERVER-PERUSER'] = (0, None, None)
+
+class X_CALENDARSERVER_PERINSTANCE(VCalendarComponentBehavior):
+    name='X-CALENDARSERVER-PERINSTANCE'
+    description='A component used to encapsulate per-user instance data.'
+    sortFirst = ('recurrence-id',)
+    knownChildren = {
+        'RECURRENCE-ID':(0, 1, None),#min, max, behaviorRegistry id
+    }
+      
+registerBehavior(X_CALENDARSERVER_PERINSTANCE)

Deleted: CalendarServer/trunk/twistedcaldav/datafilters/calendardata.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/datafilters/calendardata.py	2010-04-07 19:28:58 UTC (rev 5440)
+++ CalendarServer/trunk/twistedcaldav/datafilters/calendardata.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -1,172 +0,0 @@
-##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twistedcaldav.caldavxml import LimitRecurrenceSet, Expand, AllComponents,\
-    AllProperties
-from twistedcaldav.datafilters.filter import CalendarFilter
-from twistedcaldav.dateops import clipPeriod
-from twistedcaldav.ical import Component
-
-__all__ = [
-    "CalendarDataFilter",
-]
-
-class CalendarDataFilter(CalendarFilter):
-    """
-    Filter using the CALDAV:calendar-data element specification
-    """
-
-    def __init__(self, calendardata, timezone=None):
-        """
-        
-        @param calendardata: the XML element describing how to filter
-        @type calendardata: L{CalendarData}
-        @param timezone: the VTIMEZONE to use for floating/all-day
-        @type timezone: L{Component}
-        """
-        
-        self.calendardata = calendardata
-        self.timezone = timezone
-    
-    def filter(self, ical):
-        """
-        Filter the supplied iCalendar (vobject) data using the request information.
-
-        @param ical: iCalendar object
-        @type ical: L{Component} or C{str}
-        
-        @return: L{Component} for the filtered calendar data
-        """
-        
-        # Empty element: get all data
-        if not self.calendardata.children:
-            return ical
-
-        # Make sure input is valid
-        ical = self.validCalendar(ical)
-
-        # Pre-process the calendar data based on expand and limit options
-        if self.calendardata.freebusy_set:
-            ical = self.limitFreeBusy(ical)
-
-        # Filter data based on any provided CALDAV:comp element, or use all current data
-        if self.calendardata.component is not None:
-            ical = self.compFilter(self.calendardata.component, ical)
-        
-        # Post-process the calendar data based on the expand and limit options
-        if self.calendardata.recurrence_set:
-            if isinstance(self.calendardata.recurrence_set, LimitRecurrenceSet):
-                ical = self.limitRecurrence(ical)
-            elif isinstance(self.calendardata.recurrence_set, Expand):
-                ical = self.expandRecurrence(ical, self.timezone)
-        
-        return ical
-
-    def compFilter(self, comp, component):
-        """
-        Returns a calendar component object containing the data in the given
-        component which is specified by this CalendarComponent.
-        """
-        if comp.type != component.name():
-            raise ValueError("%s of type %r can't get data from component of type %r"
-                             % (comp.sname(), comp.type, component.name()))
-
-        result = Component(comp.type)
-
-        xml_components = comp.components
-        xml_properties = comp.properties
-
-        # Empty element means do all properties and components
-        if xml_components is None and xml_properties is None:
-            xml_components = AllComponents()
-            xml_properties = AllProperties()
-
-        if xml_components is not None:
-            if xml_components == AllComponents():
-                for ical_subcomponent in component.subcomponents():
-                    result.addComponent(ical_subcomponent)
-            else:
-                for xml_subcomponent in xml_components:
-                    for ical_subcomponent in component.subcomponents():
-                        if ical_subcomponent.name() == xml_subcomponent.type:
-                            result.addComponent(self.compFilter(xml_subcomponent, ical_subcomponent))
-
-        if xml_properties is not None:
-            if xml_properties == AllProperties():
-                for ical_property in component.properties():
-                    result.addProperty(ical_property)
-            else:
-                for xml_property in xml_properties:
-                    name = xml_property.property_name
-                    for ical_property in component.properties(name):
-                        result.addProperty(ical_property)
-
-        return result
-
-    def expandRecurrence(self, calendar, timezone=None):
-        """
-        Expand the recurrence set into individual items.
-        @param calendar: the L{Component} for the calendar to operate on.
-        @param timezone: the L{Component} the VTIMEZONE to use for floating/all-day.
-        @return: the L{Component} for the result.
-        """
-        return calendar.expand(self.calendardata.recurrence_set.start, self.calendardata.recurrence_set.end, timezone)
-    
-    def limitRecurrence(self, calendar):
-        """
-        Limit the set of overridden instances returned to only those
-        that are needed to describe the range of instances covered
-        by the specified time range.
-        @param calendar: the L{Component} for the calendar to operate on.
-        @return: the L{Component} for the result.
-        """
-        raise NotImplementedError()
-        return calendar
-    
-    def limitFreeBusy(self, calendar):
-        """
-        Limit the range of any FREEBUSY properties in the calendar, returning
-        a new calendar if limits were applied, or the same one if no limits were applied.
-        @param calendar: the L{Component} for the calendar to operate on.
-        @return: the L{Component} for the result.
-        """
-        
-        # First check for any VFREEBUSYs - can ignore limit if there are none
-        if calendar.mainType() != "VFREEBUSY":
-            return calendar
-        
-        # Create duplicate calendar and filter FREEBUSY properties
-        calendar = calendar.duplicate()
-        for component in calendar.subcomponents():
-            if component.name() != "VFREEBUSY":
-                continue
-            for property in component.properties("FREEBUSY"):
-                newvalue = []
-                for period in property.value():
-                    clipped = clipPeriod(period, (self.calendardata.freebusy_set.start, self.calendardata.freebusy_set.end))
-                    if clipped:
-                        newvalue.append(clipped)
-                if len(newvalue):
-                    property.setValue(newvalue)
-                else:
-                    component.removeProperty(property)
-        return calendar
-
-    def merge(self, icalnew, icalold):
-        """
-        Calendar-data merging does not happen
-        """
-        raise NotImplementedError

Copied: CalendarServer/trunk/twistedcaldav/datafilters/calendardata.py (from rev 5440, CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/datafilters/calendardata.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/datafilters/calendardata.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/datafilters/calendardata.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -0,0 +1,172 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.caldavxml import LimitRecurrenceSet, Expand, AllComponents,\
+    AllProperties
+from twistedcaldav.datafilters.filter import CalendarFilter
+from twistedcaldav.dateops import clipPeriod
+from twistedcaldav.ical import Component
+
+__all__ = [
+    "CalendarDataFilter",
+]
+
+class CalendarDataFilter(CalendarFilter):
+    """
+    Filter using the CALDAV:calendar-data element specification
+    """
+
+    def __init__(self, calendardata, timezone=None):
+        """
+        
+        @param calendardata: the XML element describing how to filter
+        @type calendardata: L{CalendarData}
+        @param timezone: the VTIMEZONE to use for floating/all-day
+        @type timezone: L{Component}
+        """
+        
+        self.calendardata = calendardata
+        self.timezone = timezone
+    
+    def filter(self, ical):
+        """
+        Filter the supplied iCalendar (vobject) data using the request information.
+
+        @param ical: iCalendar object
+        @type ical: L{Component} or C{str}
+        
+        @return: L{Component} for the filtered calendar data
+        """
+        
+        # Empty element: get all data
+        if not self.calendardata.children:
+            return ical
+
+        # Make sure input is valid
+        ical = self.validCalendar(ical)
+
+        # Pre-process the calendar data based on expand and limit options
+        if self.calendardata.freebusy_set:
+            ical = self.limitFreeBusy(ical)
+
+        # Filter data based on any provided CALDAV:comp element, or use all current data
+        if self.calendardata.component is not None:
+            ical = self.compFilter(self.calendardata.component, ical)
+        
+        # Post-process the calendar data based on the expand and limit options
+        if self.calendardata.recurrence_set:
+            if isinstance(self.calendardata.recurrence_set, LimitRecurrenceSet):
+                ical = self.limitRecurrence(ical)
+            elif isinstance(self.calendardata.recurrence_set, Expand):
+                ical = self.expandRecurrence(ical, self.timezone)
+        
+        return ical
+
+    def compFilter(self, comp, component):
+        """
+        Returns a calendar component object containing the data in the given
+        component which is specified by this CalendarComponent.
+        """
+        if comp.type != component.name():
+            raise ValueError("%s of type %r can't get data from component of type %r"
+                             % (comp.sname(), comp.type, component.name()))
+
+        result = Component(comp.type)
+
+        xml_components = comp.components
+        xml_properties = comp.properties
+
+        # Empty element means do all properties and components
+        if xml_components is None and xml_properties is None:
+            xml_components = AllComponents()
+            xml_properties = AllProperties()
+
+        if xml_components is not None:
+            if xml_components == AllComponents():
+                for ical_subcomponent in component.subcomponents():
+                    result.addComponent(ical_subcomponent)
+            else:
+                for xml_subcomponent in xml_components:
+                    for ical_subcomponent in component.subcomponents():
+                        if ical_subcomponent.name() == xml_subcomponent.type:
+                            result.addComponent(self.compFilter(xml_subcomponent, ical_subcomponent))
+
+        if xml_properties is not None:
+            if xml_properties == AllProperties():
+                for ical_property in component.properties():
+                    result.addProperty(ical_property)
+            else:
+                for xml_property in xml_properties:
+                    name = xml_property.property_name
+                    for ical_property in component.properties(name):
+                        result.addProperty(ical_property)
+
+        return result
+
+    def expandRecurrence(self, calendar, timezone=None):
+        """
+        Expand the recurrence set into individual items.
+        @param calendar: the L{Component} for the calendar to operate on.
+        @param timezone: the L{Component} the VTIMEZONE to use for floating/all-day.
+        @return: the L{Component} for the result.
+        """
+        return calendar.expand(self.calendardata.recurrence_set.start, self.calendardata.recurrence_set.end, timezone)
+    
+    def limitRecurrence(self, calendar):
+        """
+        Limit the set of overridden instances returned to only those
+        that are needed to describe the range of instances covered
+        by the specified time range.
+        @param calendar: the L{Component} for the calendar to operate on.
+        @return: the L{Component} for the result.
+        """
+        raise NotImplementedError()
+        return calendar
+    
+    def limitFreeBusy(self, calendar):
+        """
+        Limit the range of any FREEBUSY properties in the calendar, returning
+        a new calendar if limits were applied, or the same one if no limits were applied.
+        @param calendar: the L{Component} for the calendar to operate on.
+        @return: the L{Component} for the result.
+        """
+        
+        # First check for any VFREEBUSYs - can ignore limit if there are none
+        if calendar.mainType() != "VFREEBUSY":
+            return calendar
+        
+        # Create duplicate calendar and filter FREEBUSY properties
+        calendar = calendar.duplicate()
+        for component in calendar.subcomponents():
+            if component.name() != "VFREEBUSY":
+                continue
+            for property in component.properties("FREEBUSY"):
+                newvalue = []
+                for period in property.value():
+                    clipped = clipPeriod(period, (self.calendardata.freebusy_set.start, self.calendardata.freebusy_set.end))
+                    if clipped:
+                        newvalue.append(clipped)
+                if len(newvalue):
+                    property.setValue(newvalue)
+                else:
+                    component.removeProperty(property)
+        return calendar
+
+    def merge(self, icalnew, icalold):
+        """
+        Calendar-data merging does not happen
+        """
+        raise NotImplementedError

Deleted: CalendarServer/trunk/twistedcaldav/datafilters/filter.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/datafilters/filter.py	2010-04-07 19:28:58 UTC (rev 5440)
+++ CalendarServer/trunk/twistedcaldav/datafilters/filter.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -1,65 +0,0 @@
-##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-from twistedcaldav.ical import Component
-
-__all__ = [
-    "CalendarFilter",
-]
-
-class CalendarFilter(object):
-    """
-    Abstract class that defines an iCalendar filter/merge object
-    """
-
-
-    def __init__(self):
-        pass
-    
-    def filter(self, ical):
-        """
-        Filter the supplied iCalendar (vobject) data using the request information.
-
-        @param ical: iCalendar object
-        @type ical: L{Component}
-        
-        @return: L{Component} for the filtered calendar data
-        """
-        raise NotImplementedError
-    
-    def merge(self, icalnew, icalold):
-        """
-        Merge the old iCalendar (vobject) data into the new iCalendar data using the request information.
-        
-        @param icalnew: new iCalendar object to merge data into
-        @type icalnew: L{Component}
-        @param icalold: old iCalendar data to merge data from
-        @type icalold: L{Component}
-        """
-        raise NotImplementedError
-
-    def validCalendar(self, ical):
-
-        # If we were passed a string, parse it out as a Component
-        if isinstance(ical, str):
-            try:
-                ical = Component.fromString(ical)
-            except ValueError:
-                raise ValueError("Not a calendar: %r" % (ical,))
-        
-        if ical is None or ical.name() != "VCALENDAR":
-            raise ValueError("Not a calendar: %r" % (ical,))
-        
-        return ical

Copied: CalendarServer/trunk/twistedcaldav/datafilters/filter.py (from rev 5440, CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/datafilters/filter.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/datafilters/filter.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/datafilters/filter.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -0,0 +1,65 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+from twistedcaldav.ical import Component
+
+__all__ = [
+    "CalendarFilter",
+]
+
+class CalendarFilter(object):
+    """
+    Abstract class that defines an iCalendar filter/merge object
+    """
+
+
+    def __init__(self):
+        pass
+    
+    def filter(self, ical):
+        """
+        Filter the supplied iCalendar (vobject) data using the request information.
+
+        @param ical: iCalendar object
+        @type ical: L{Component}
+        
+        @return: L{Component} for the filtered calendar data
+        """
+        raise NotImplementedError
+    
+    def merge(self, icalnew, icalold):
+        """
+        Merge the old iCalendar (vobject) data into the new iCalendar data using the request information.
+        
+        @param icalnew: new iCalendar object to merge data into
+        @type icalnew: L{Component}
+        @param icalold: old iCalendar data to merge data from
+        @type icalold: L{Component}
+        """
+        raise NotImplementedError
+
+    def validCalendar(self, ical):
+
+        # If we were passed a string, parse it out as a Component
+        if isinstance(ical, str):
+            try:
+                ical = Component.fromString(ical)
+            except ValueError:
+                raise ValueError("Not a calendar: %r" % (ical,))
+        
+        if ical is None or ical.name() != "VCALENDAR":
+            raise ValueError("Not a calendar: %r" % (ical,))
+        
+        return ical

Deleted: CalendarServer/trunk/twistedcaldav/datafilters/peruserdata.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/datafilters/peruserdata.py	2010-04-07 19:28:58 UTC (rev 5440)
+++ CalendarServer/trunk/twistedcaldav/datafilters/peruserdata.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -1,336 +0,0 @@
-##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twistedcaldav.datafilters.filter import CalendarFilter
-from twistedcaldav.ical import Component, Property
-
-__all__ = [
-    "PerUserDataFilter",
-]
-
-"""
-Object model for calendar data is as follows:
-
-VCALENDAR
-  VTIMEZONE*
-  VEVENT* / VTODO* / VJOURNAL*
-  BEGIN:X-CALENDARSERVER-PERUSER*
-    X-CALENDARSERVER-PERUSER-UID
-    UID
-    BEGIN:X-CALENDARSERVER-PERINSTANCE
-      RECURRENCE-ID?
-      TRANSP?
-      VALARM*
-
-So we will store per user data inside the top-level component (alongside VEVENT, VTODO etc). That new component will
-contain properties to identify the user and the UID of the VEVENT, VTODO it affects. It will contain sub-components
-for each instance overridden by the per-user data. These per-user overridden components may not correspond to an
-actual overridden component. In that situation the server has to re-construct the per-user data appropriately:
-
-e.g., 
-
-1. VEVENT contains an overridden instance, but X-CALENDARSERVER-PERUSER does not - server uses the must instance
-X-CALENDARSERVER-PERUSER data (if any) for the overridden instance.
-
-2. VEVENT does not contain an overridden instance, but X-CALENDARSERVER-PERUSER does - server synthesizes an
-overridden instance to match the X-CALENDARSERVER-PERUSER one.
-
-3. VEVENT contains overridden instance and X-CALENDARSERVER-PERUSER does - server merges X-CALENDARSERVER-PERUSER
-data into overridden instance.
-
-"""
-
-class PerUserDataFilter(CalendarFilter):
-    """
-    Filter per-user data
-    """
-
-    # If any of these change also change the vobject behaviors in this module's __init__.py
-    # and update usage in ical.py
-    PERUSER_COMPONENT     = "X-CALENDARSERVER-PERUSER"
-    PERUSER_UID           = "X-CALENDARSERVER-PERUSER-UID"
-    PERINSTANCE_COMPONENT = "X-CALENDARSERVER-PERINSTANCE"
-
-    PERUSER_PROPERTIES    = ("TRANSP",)
-    PERUSER_SUBCOMPONENTS = ("VALARM",)
-
-    def __init__(self, uid):
-        """
-        
-        @param uid: unique identifier of the user for whom the data is being filtered 
-        @type uid: C{str}
-        """
-        
-        self.uid = uid
-    
-    def filter(self, ical):
-        """
-        Filter the supplied iCalendar (vobject) data using the request information.
-        Assume that the object is a CalDAV calendar resource.
-
-        @param ical: iCalendar object - this will be modified and returned
-        @type ical: L{Component} or C{str}
-        
-        @return: L{Component} for the filtered calendar data
-        """
-        
-        # Make sure input is valid
-        ical = self.validCalendar(ical)
-
-        # Look for matching per-user sub-component, removing all the others
-        peruser_component = None
-        for component in tuple(ical.subcomponents()):
-            if component.name() == PerUserDataFilter.PERUSER_COMPONENT:
-                
-                # Check user id - remove if not matches
-                if component.propertyValue(PerUserDataFilter.PERUSER_UID) != self.uid:
-                    ical.removeComponent(component)
-                elif peruser_component is None:
-                    peruser_component = component
-                    ical.removeComponent(component)
-                else:
-                    raise AssertionError("Can't have two X-CALENDARSERVER-PERUSER components for the same user")
-
-        # Now transfer any components over
-        if peruser_component:
-            self._mergeBack(ical, peruser_component)
-
-        return ical
-
-    def merge(self, icalnew, icalold):
-        """
-        Merge the new data with the old taking per-user information into account.
-
-        @param icalnew: new calendar data
-        @type icalnew: L{Component} or C{str}
-        @param icalold: existing calendar data
-        @type icalold: L{Component} or C{str}
-        
-        @return: L{Component} for the merged calendar data
-        """
-
-        # Make sure input is valid
-        icalnew = self.validCalendar(icalnew)
-
-        # First split the new data into common and per-user pieces
-        self._splitPerUserData(icalnew)
-        if icalold is None:
-            return icalnew
-        
-        # Make sure input is valid
-        icalold = self.validCalendar(icalold)
-
-        self._mergeRepresentations(icalnew, icalold)
-        return icalnew
-
-    def _mergeBack(self, ical, peruser):
-        """
-        Merge the per-user data back into the main calendar data.
-
-        @param ical: main calendar data to merge into
-        @type ical: L{Component}
-        @param peruser: the per-user data to merge in
-        @type peruser: L{Component}
-        """
-        
-        # Iterate over each instance in the per-user data and build mapping
-        peruser_recurrence_map = {}
-        for subcomponent in peruser.subcomponents():
-            if subcomponent.name() != PerUserDataFilter.PERINSTANCE_COMPONENT:
-                raise AssertionError("Wrong sub-component '%s' in a X-CALENDARSERVER-PERUSER component" % (subcomponent.name(),))
-            peruser_recurrence_map[subcomponent.getRecurrenceIDUTC()] = subcomponent
-            
-        ical_recurrence_set = set(ical.getComponentInstances())
-        peruser_recurrence_set = set(peruser_recurrence_map.keys())
-        
-        # Set operations to find union and differences
-        union_set = ical_recurrence_set.intersection(peruser_recurrence_set)
-        ical_only_set = ical_recurrence_set.difference(peruser_recurrence_set)
-        peruser_only_set = peruser_recurrence_set.difference(ical_recurrence_set)
-        
-        # For ones in per-user data but no main data, we synthesize an instance and copy over per-user data
-        # NB We have to do this before we do any merge that may change the master
-        if ical.masterComponent() is not None:
-            for rid in peruser_only_set:
-                ical_component = ical.deriveInstance(rid)
-                peruser_component = peruser_recurrence_map[rid]
-                self._mergeBackComponent(ical_component, peruser_component)
-                ical.addComponent(ical_component)
-        elif peruser_only_set:
-            raise AssertionError("Cannot derive a per-user instance when there is no master component.")
-                    
-        # Process the unions by merging in per-user data
-        for rid in union_set:
-            ical_component = ical.overriddenComponent(rid)
-            peruser_component = peruser_recurrence_map[rid]
-            self._mergeBackComponent(ical_component, peruser_component)
-
-        # For ones in main data but no per-user data, we try and copy over the master per-user data
-        if ical_only_set:
-            peruser_master = peruser_recurrence_map.get(None)
-            if peruser_master:
-                for rid in ical_only_set:
-                    ical_component = ical.overriddenComponent(rid)
-                    self._mergeBackComponent(ical_component, peruser_master)
-                    
-    def _mergeBackComponent(self, ical, peruser):
-        """
-        Copy all properties and sub-components from per-user data into the main component
-        @param ical:
-        @type ical:
-        @param peruser:
-        @type peruser:
-        """
-        
-        # Each sub-component
-        for subcomponent in peruser.subcomponents():
-            ical.addComponent(subcomponent)
-        
-        # Each property except RECURRENCE-ID
-        for property in peruser.properties():
-            if property.name() == "RECURRENCE-ID":
-                continue
-            ical.addProperty(property)
-
-    def _splitPerUserData(self, ical):
-        
-        def init_peruser_component():
-            peruser = Component(PerUserDataFilter.PERUSER_COMPONENT)
-            peruser.addProperty(Property("UID", ical.resourceUID()))
-            peruser.addProperty(Property(PerUserDataFilter.PERUSER_UID, self.uid))
-            ical.addComponent(peruser)
-            return peruser
-        
-        components = tuple(ical.subcomponents())
-        peruser_component = init_peruser_component() if self.uid else None
-        perinstance_components = {}
-
-        for component in components:
-            if component.name() == "VTIMEZONE":
-                continue
-
-            def init_perinstance_component():
-                peruser = Component(PerUserDataFilter.PERINSTANCE_COMPONENT)
-                rid = component.getRecurrenceIDUTC()
-                if rid:
-                    peruser.addProperty(Property("RECURRENCE-ID", rid))
-                perinstance_components[rid] = peruser
-                return peruser
-
-            perinstance_component = init_perinstance_component() if self.uid else None
-            
-            # Transfer per-user properties from main component to per-instance component
-            for property in tuple(component.properties()):
-                if property.name() in PerUserDataFilter.PERUSER_PROPERTIES or property.name().startswith("X-"):
-                    if self.uid:
-                        perinstance_component.addProperty(property)
-                    component.removeProperty(property)
-            
-            # Transfer per-user components from main component to per-instance component
-            for subcomponent in tuple(component.subcomponents()):
-                if subcomponent.name() in PerUserDataFilter.PERUSER_SUBCOMPONENTS or subcomponent.name().startswith("X-"):
-                    if self.uid:
-                        perinstance_component.addComponent(subcomponent)
-                    component.removeComponent(subcomponent)
-            
-        if self.uid:
-            # Add unique per-instance components into the per-user component
-            master_perinstance = perinstance_components.get(None)
-            master_perinstance_txt = str(master_perinstance)
-            if master_perinstance:
-                peruser_component.addComponent(master_perinstance)
-            for rid, perinstance in perinstance_components.iteritems():
-                if rid is None:
-                    continue
-                perinstance_txt = str(perinstance)
-                perinstance_txt = "".join([line for line in perinstance_txt.splitlines(True) if not line.startswith("RECURRENCE-ID:")])
-                if master_perinstance is None or perinstance_txt != master_perinstance_txt:
-                    peruser_component.addComponent(perinstance)
-    
-            self._compactInstances(ical)
-
-    def _compactInstances(self, ical):
-        """
-        Remove recurrences instances that are the same as their master-derived counterparts. This gives the most
-        compact representation of the calendar data.
-
-        @param ical: calendar data to process
-        @type ical: L{Component}
-        """
-
-        # Must have a master component in order to do this
-        master = ical.masterComponent()
-        if master is None:
-            return
-
-        for subcomponent in tuple(ical.subcomponents()):
-            if subcomponent.name() == "VTIMEZONE" or subcomponent.name().startswith("X-"):
-                continue
-            rid = subcomponent.getRecurrenceIDUTC()
-            if rid is None:
-                continue
-            derived = ical.deriveInstance(rid)
-            if derived:
-                if str(derived) == str(subcomponent):
-                    ical.removeComponent(subcomponent)
-
-    def _mergeRepresentations(self, icalnew, icalold):
-        
-        # Test for simple case first
-        if icalnew.isRecurring() and icalold.isRecurring():
-            # Test each instance from old data to see whether it is still valid in the new one 
-            self._complexMerge(icalnew, icalold)
-        else:
-            self._simpleMerge(icalnew, icalold)
-    
-    def _simpleMerge(self, icalnew, icalold):
-        
-        # Take all per-user components from old and add to new, except for our user
-        new_recur = icalnew.isRecurring()
-        old_recur = icalold.isRecurring()
-        new_recur_has_no_master = new_recur and (icalnew.masterComponent() is None)
-        for component in icalold.subcomponents():
-            if component.name() == PerUserDataFilter.PERUSER_COMPONENT:
-                if component.propertyValue(PerUserDataFilter.PERUSER_UID) != self.uid and not new_recur_has_no_master:
-                    newcomponent = component.duplicate()
-                    
-                    # Only transfer the master components from the old data to the new when the old
-                    # was recurring and the new is not recurring
-                    if not new_recur and old_recur:
-                        for subcomponent in tuple(newcomponent.subcomponents()):
-                            if subcomponent.getRecurrenceIDUTC() is not None:
-                                newcomponent.removeComponent(subcomponent)
-
-                    if len(tuple(newcomponent.subcomponents())):
-                        icalnew.addComponent(newcomponent)
-    
-    def _complexMerge(self, icalnew, icalold):
-        
-        # Take all per-user components from old and add to new, except for our user
-        for component in icalold.subcomponents():
-            if component.name() == PerUserDataFilter.PERUSER_COMPONENT:
-                if component.propertyValue(PerUserDataFilter.PERUSER_UID) != self.uid:
-                    newcomponent = component.duplicate()
-                    
-                    # See which of the instances are still valid
-                    old_rids = dict([(subcomponent.getRecurrenceIDUTC(), subcomponent,) for subcomponent in newcomponent.subcomponents()])
-                    valid_rids = icalnew.validInstances(old_rids.keys())
-                    for old_rid, subcomponent in old_rids.iteritems():
-                        if old_rid not in valid_rids:
-                            newcomponent.removeComponent(subcomponent)
-
-                    if len(tuple(newcomponent.subcomponents())):
-                        icalnew.addComponent(newcomponent)

Copied: CalendarServer/trunk/twistedcaldav/datafilters/peruserdata.py (from rev 5440, CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/datafilters/peruserdata.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/datafilters/peruserdata.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/datafilters/peruserdata.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -0,0 +1,336 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.datafilters.filter import CalendarFilter
+from twistedcaldav.ical import Component, Property
+
+__all__ = [
+    "PerUserDataFilter",
+]
+
+"""
+Object model for calendar data is as follows:
+
+VCALENDAR
+  VTIMEZONE*
+  VEVENT* / VTODO* / VJOURNAL*
+  BEGIN:X-CALENDARSERVER-PERUSER*
+    X-CALENDARSERVER-PERUSER-UID
+    UID
+    BEGIN:X-CALENDARSERVER-PERINSTANCE
+      RECURRENCE-ID?
+      TRANSP?
+      VALARM*
+
+So we will store per user data inside the top-level component (alongside VEVENT, VTODO etc). That new component will
+contain properties to identify the user and the UID of the VEVENT, VTODO it affects. It will contain sub-components
+for each instance overridden by the per-user data. These per-user overridden components may not correspond to an
+actual overridden component. In that situation the server has to re-construct the per-user data appropriately:
+
+e.g., 
+
+1. VEVENT contains an overridden instance, but X-CALENDARSERVER-PERUSER does not - server uses the must instance
+X-CALENDARSERVER-PERUSER data (if any) for the overridden instance.
+
+2. VEVENT does not contain an overridden instance, but X-CALENDARSERVER-PERUSER does - server synthesizes an
+overridden instance to match the X-CALENDARSERVER-PERUSER one.
+
+3. VEVENT contains overridden instance and X-CALENDARSERVER-PERUSER does - server merges X-CALENDARSERVER-PERUSER
+data into overridden instance.
+
+"""
+
+class PerUserDataFilter(CalendarFilter):
+    """
+    Filter per-user data
+    """
+
+    # If any of these change also change the vobject behaviors in this module's __init__.py
+    # and update usage in ical.py
+    PERUSER_COMPONENT     = "X-CALENDARSERVER-PERUSER"
+    PERUSER_UID           = "X-CALENDARSERVER-PERUSER-UID"
+    PERINSTANCE_COMPONENT = "X-CALENDARSERVER-PERINSTANCE"
+
+    PERUSER_PROPERTIES    = ("TRANSP",)
+    PERUSER_SUBCOMPONENTS = ("VALARM",)
+
+    def __init__(self, uid):
+        """
+        
+        @param uid: unique identifier of the user for whom the data is being filtered 
+        @type uid: C{str}
+        """
+        
+        self.uid = uid
+    
+    def filter(self, ical):
+        """
+        Filter the supplied iCalendar (vobject) data using the request information.
+        Assume that the object is a CalDAV calendar resource.
+
+        @param ical: iCalendar object - this will be modified and returned
+        @type ical: L{Component} or C{str}
+        
+        @return: L{Component} for the filtered calendar data
+        """
+        
+        # Make sure input is valid
+        ical = self.validCalendar(ical)
+
+        # Look for matching per-user sub-component, removing all the others
+        peruser_component = None
+        for component in tuple(ical.subcomponents()):
+            if component.name() == PerUserDataFilter.PERUSER_COMPONENT:
+                
+                # Check user id - remove if not matches
+                if component.propertyValue(PerUserDataFilter.PERUSER_UID) != self.uid:
+                    ical.removeComponent(component)
+                elif peruser_component is None:
+                    peruser_component = component
+                    ical.removeComponent(component)
+                else:
+                    raise AssertionError("Can't have two X-CALENDARSERVER-PERUSER components for the same user")
+
+        # Now transfer any components over
+        if peruser_component:
+            self._mergeBack(ical, peruser_component)
+
+        return ical
+
+    def merge(self, icalnew, icalold):
+        """
+        Merge the new data with the old taking per-user information into account.
+
+        @param icalnew: new calendar data
+        @type icalnew: L{Component} or C{str}
+        @param icalold: existing calendar data
+        @type icalold: L{Component} or C{str}
+        
+        @return: L{Component} for the merged calendar data
+        """
+
+        # Make sure input is valid
+        icalnew = self.validCalendar(icalnew)
+
+        # First split the new data into common and per-user pieces
+        self._splitPerUserData(icalnew)
+        if icalold is None:
+            return icalnew
+        
+        # Make sure input is valid
+        icalold = self.validCalendar(icalold)
+
+        self._mergeRepresentations(icalnew, icalold)
+        return icalnew
+
+    def _mergeBack(self, ical, peruser):
+        """
+        Merge the per-user data back into the main calendar data.
+
+        @param ical: main calendar data to merge into
+        @type ical: L{Component}
+        @param peruser: the per-user data to merge in
+        @type peruser: L{Component}
+        """
+        
+        # Iterate over each instance in the per-user data and build mapping
+        peruser_recurrence_map = {}
+        for subcomponent in peruser.subcomponents():
+            if subcomponent.name() != PerUserDataFilter.PERINSTANCE_COMPONENT:
+                raise AssertionError("Wrong sub-component '%s' in a X-CALENDARSERVER-PERUSER component" % (subcomponent.name(),))
+            peruser_recurrence_map[subcomponent.getRecurrenceIDUTC()] = subcomponent
+            
+        ical_recurrence_set = set(ical.getComponentInstances())
+        peruser_recurrence_set = set(peruser_recurrence_map.keys())
+        
+        # Set operations to find union and differences
+        union_set = ical_recurrence_set.intersection(peruser_recurrence_set)
+        ical_only_set = ical_recurrence_set.difference(peruser_recurrence_set)
+        peruser_only_set = peruser_recurrence_set.difference(ical_recurrence_set)
+        
+        # For ones in per-user data but no main data, we synthesize an instance and copy over per-user data
+        # NB We have to do this before we do any merge that may change the master
+        if ical.masterComponent() is not None:
+            for rid in peruser_only_set:
+                ical_component = ical.deriveInstance(rid)
+                peruser_component = peruser_recurrence_map[rid]
+                self._mergeBackComponent(ical_component, peruser_component)
+                ical.addComponent(ical_component)
+        elif peruser_only_set:
+            raise AssertionError("Cannot derive a per-user instance when there is no master component.")
+                    
+        # Process the unions by merging in per-user data
+        for rid in union_set:
+            ical_component = ical.overriddenComponent(rid)
+            peruser_component = peruser_recurrence_map[rid]
+            self._mergeBackComponent(ical_component, peruser_component)
+
+        # For ones in main data but no per-user data, we try and copy over the master per-user data
+        if ical_only_set:
+            peruser_master = peruser_recurrence_map.get(None)
+            if peruser_master:
+                for rid in ical_only_set:
+                    ical_component = ical.overriddenComponent(rid)
+                    self._mergeBackComponent(ical_component, peruser_master)
+                    
+    def _mergeBackComponent(self, ical, peruser):
+        """
+        Copy all properties and sub-components from per-user data into the main component
+        @param ical:
+        @type ical:
+        @param peruser:
+        @type peruser:
+        """
+        
+        # Each sub-component
+        for subcomponent in peruser.subcomponents():
+            ical.addComponent(subcomponent)
+        
+        # Each property except RECURRENCE-ID
+        for property in peruser.properties():
+            if property.name() == "RECURRENCE-ID":
+                continue
+            ical.addProperty(property)
+
+    def _splitPerUserData(self, ical):
+        
+        def init_peruser_component():
+            peruser = Component(PerUserDataFilter.PERUSER_COMPONENT)
+            peruser.addProperty(Property("UID", ical.resourceUID()))
+            peruser.addProperty(Property(PerUserDataFilter.PERUSER_UID, self.uid))
+            ical.addComponent(peruser)
+            return peruser
+        
+        components = tuple(ical.subcomponents())
+        peruser_component = init_peruser_component() if self.uid else None
+        perinstance_components = {}
+
+        for component in components:
+            if component.name() == "VTIMEZONE":
+                continue
+
+            def init_perinstance_component():
+                peruser = Component(PerUserDataFilter.PERINSTANCE_COMPONENT)
+                rid = component.getRecurrenceIDUTC()
+                if rid:
+                    peruser.addProperty(Property("RECURRENCE-ID", rid))
+                perinstance_components[rid] = peruser
+                return peruser
+
+            perinstance_component = init_perinstance_component() if self.uid else None
+            
+            # Transfer per-user properties from main component to per-instance component
+            for property in tuple(component.properties()):
+                if property.name() in PerUserDataFilter.PERUSER_PROPERTIES or property.name().startswith("X-"):
+                    if self.uid:
+                        perinstance_component.addProperty(property)
+                    component.removeProperty(property)
+            
+            # Transfer per-user components from main component to per-instance component
+            for subcomponent in tuple(component.subcomponents()):
+                if subcomponent.name() in PerUserDataFilter.PERUSER_SUBCOMPONENTS or subcomponent.name().startswith("X-"):
+                    if self.uid:
+                        perinstance_component.addComponent(subcomponent)
+                    component.removeComponent(subcomponent)
+            
+        if self.uid:
+            # Add unique per-instance components into the per-user component
+            master_perinstance = perinstance_components.get(None)
+            master_perinstance_txt = str(master_perinstance)
+            if master_perinstance:
+                peruser_component.addComponent(master_perinstance)
+            for rid, perinstance in perinstance_components.iteritems():
+                if rid is None:
+                    continue
+                perinstance_txt = str(perinstance)
+                perinstance_txt = "".join([line for line in perinstance_txt.splitlines(True) if not line.startswith("RECURRENCE-ID:")])
+                if master_perinstance is None or perinstance_txt != master_perinstance_txt:
+                    peruser_component.addComponent(perinstance)
+    
+            self._compactInstances(ical)
+
+    def _compactInstances(self, ical):
+        """
+        Remove recurrences instances that are the same as their master-derived counterparts. This gives the most
+        compact representation of the calendar data.
+
+        @param ical: calendar data to process
+        @type ical: L{Component}
+        """
+
+        # Must have a master component in order to do this
+        master = ical.masterComponent()
+        if master is None:
+            return
+
+        for subcomponent in tuple(ical.subcomponents()):
+            if subcomponent.name() == "VTIMEZONE" or subcomponent.name().startswith("X-"):
+                continue
+            rid = subcomponent.getRecurrenceIDUTC()
+            if rid is None:
+                continue
+            derived = ical.deriveInstance(rid)
+            if derived:
+                if str(derived) == str(subcomponent):
+                    ical.removeComponent(subcomponent)
+
+    def _mergeRepresentations(self, icalnew, icalold):
+        
+        # Test for simple case first
+        if icalnew.isRecurring() and icalold.isRecurring():
+            # Test each instance from old data to see whether it is still valid in the new one 
+            self._complexMerge(icalnew, icalold)
+        else:
+            self._simpleMerge(icalnew, icalold)
+    
+    def _simpleMerge(self, icalnew, icalold):
+        
+        # Take all per-user components from old and add to new, except for our user
+        new_recur = icalnew.isRecurring()
+        old_recur = icalold.isRecurring()
+        new_recur_has_no_master = new_recur and (icalnew.masterComponent() is None)
+        for component in icalold.subcomponents():
+            if component.name() == PerUserDataFilter.PERUSER_COMPONENT:
+                if component.propertyValue(PerUserDataFilter.PERUSER_UID) != self.uid and not new_recur_has_no_master:
+                    newcomponent = component.duplicate()
+                    
+                    # Only transfer the master components from the old data to the new when the old
+                    # was recurring and the new is not recurring
+                    if not new_recur and old_recur:
+                        for subcomponent in tuple(newcomponent.subcomponents()):
+                            if subcomponent.getRecurrenceIDUTC() is not None:
+                                newcomponent.removeComponent(subcomponent)
+
+                    if len(tuple(newcomponent.subcomponents())):
+                        icalnew.addComponent(newcomponent)
+    
+    def _complexMerge(self, icalnew, icalold):
+        
+        # Take all per-user components from old and add to new, except for our user
+        for component in icalold.subcomponents():
+            if component.name() == PerUserDataFilter.PERUSER_COMPONENT:
+                if component.propertyValue(PerUserDataFilter.PERUSER_UID) != self.uid:
+                    newcomponent = component.duplicate()
+                    
+                    # See which of the instances are still valid
+                    old_rids = dict([(subcomponent.getRecurrenceIDUTC(), subcomponent,) for subcomponent in newcomponent.subcomponents()])
+                    valid_rids = icalnew.validInstances(old_rids.keys())
+                    for old_rid, subcomponent in old_rids.iteritems():
+                        if old_rid not in valid_rids:
+                            newcomponent.removeComponent(subcomponent)
+
+                    if len(tuple(newcomponent.subcomponents())):
+                        icalnew.addComponent(newcomponent)

Deleted: CalendarServer/trunk/twistedcaldav/datafilters/privateevents.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/datafilters/privateevents.py	2010-04-07 19:28:58 UTC (rev 5440)
+++ CalendarServer/trunk/twistedcaldav/datafilters/privateevents.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -1,173 +0,0 @@
-##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.web2 import responsecode
-from twisted.web2.http import HTTPError, StatusResponse
-from twistedcaldav.caldavxml import Property, CalendarData, CalendarComponent,\
-    AllProperties, AllComponents
-from twistedcaldav.datafilters.calendardata import CalendarDataFilter
-from twistedcaldav.datafilters.filter import CalendarFilter
-from twistedcaldav.ical import Component
-
-__all__ = [
-    "PrivateEventFilter",
-]
-
-class PrivateEventFilter(CalendarFilter):
-    """
-    Filter a private event to match the rights of the non-owner user accessing the data
-    """
-
-    def __init__(self, accessRestriction, isowner):
-        """
-        
-        @param accessRestriction: one of the access levels in L{Component}
-        @type accessRestriction: C{str}
-        @param isowner: whether the current user is the owner of the data
-        @type isowner: C{bool}
-        """
-        
-        self.accessRestriction = accessRestriction
-        self.isowner = isowner
-    
-    def filter(self, ical):
-        """
-        Filter the supplied iCalendar (vobject) data using the request information.
-
-        @param ical: iCalendar object
-        @type ical: L{Component} or C{str}
-        
-        @return: L{Component} for the filtered calendar data
-        """
-        
-        if self.isowner or self.accessRestriction == Component.ACCESS_PUBLIC or self.accessRestriction is None:
-            # No need to filter for the owner or public event
-            return ical
-        
-        elif self.accessRestriction == Component.ACCESS_PRIVATE:
-            # We should never get here because ACCESS_PRIVATE is protected via an ACL
-            raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Access Denied"))
-
-        elif self.accessRestriction == Component.ACCESS_PUBLIC:
-            return ical
-        elif self.accessRestriction in (Component.ACCESS_CONFIDENTIAL, Component.ACCESS_RESTRICTED):
-            # Create a CALDAV:calendar-data element with the appropriate iCalendar Component/Property
-            # filter in place for the access restriction in use
-            
-            extra_access = ()
-            if self.accessRestriction == Component.ACCESS_RESTRICTED:
-                extra_access = (
-                    Property(name="SUMMARY"),
-                    Property(name="LOCATION"),
-                )
-
-            calendardata = CalendarData(
-                CalendarComponent(
-                    
-                    # VCALENDAR properties
-                    Property(name="PRODID"),
-                    Property(name="VERSION"),
-                    Property(name="CALSCALE"),
-                    Property(name=Component.ACCESS_PROPERTY),
-
-                    # VEVENT
-                    CalendarComponent(
-                        Property(name="UID"),
-                        Property(name="RECURRENCE-ID"),
-                        Property(name="SEQUENCE"),
-                        Property(name="DTSTAMP"),
-                        Property(name="STATUS"),
-                        Property(name="TRANSP"),
-                        Property(name="DTSTART"),
-                        Property(name="DTEND"),
-                        Property(name="DURATION"),
-                        Property(name="RRULE"),
-                        Property(name="RDATE"),
-                        Property(name="EXRULE"),
-                        Property(name="EXDATE"),
-                        *extra_access,
-                        **{"name":"VEVENT"}
-                    ),
-                    
-                    # VTODO
-                    CalendarComponent(
-                        Property(name="UID"),
-                        Property(name="RECURRENCE-ID"),
-                        Property(name="SEQUENCE"),
-                        Property(name="DTSTAMP"),
-                        Property(name="STATUS"),
-                        Property(name="DTSTART"),
-                        Property(name="COMPLETED"),
-                        Property(name="DUE"),
-                        Property(name="DURATION"),
-                        Property(name="RRULE"),
-                        Property(name="RDATE"),
-                        Property(name="EXRULE"),
-                        Property(name="EXDATE"),
-                        *extra_access,
-                        **{"name":"VTODO"}
-                    ),
-                    
-                    # VJOURNAL
-                    CalendarComponent(
-                        Property(name="UID"),
-                        Property(name="RECURRENCE-ID"),
-                        Property(name="SEQUENCE"),
-                        Property(name="DTSTAMP"),
-                        Property(name="STATUS"),
-                        Property(name="TRANSP"),
-                        Property(name="DTSTART"),
-                        Property(name="RRULE"),
-                        Property(name="RDATE"),
-                        Property(name="EXRULE"),
-                        Property(name="EXDATE"),
-                        *extra_access,
-                        **{"name":"VJOURNAL"}
-                    ),
-                    
-                    # VFREEBUSY
-                    CalendarComponent(
-                        Property(name="UID"),
-                        Property(name="DTSTAMP"),
-                        Property(name="DTSTART"),
-                        Property(name="DTEND"),
-                        Property(name="DURATION"),
-                        Property(name="FREEBUSY"),
-                        *extra_access,
-                        **{"name":"VFREEBUSY"}
-                    ),
-                    
-                    # VTIMEZONE
-                    CalendarComponent(
-                        AllProperties(),
-                        AllComponents(),
-                        name="VTIMEZONE",
-                    ),
-                    name="VCALENDAR",
-                ),
-            )
-
-            # Now "filter" the resource calendar data through the CALDAV:calendar-data element
-            return CalendarDataFilter(calendardata).filter(ical)
-        else:
-            # Unknown access restriction
-            raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Access Denied"))
-    
-    def merge(self, icalnew, icalold):
-        """
-        Private event merging does not happen
-        """
-        raise NotImplementedError

Copied: CalendarServer/trunk/twistedcaldav/datafilters/privateevents.py (from rev 5440, CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/datafilters/privateevents.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/datafilters/privateevents.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/datafilters/privateevents.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -0,0 +1,173 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.web2 import responsecode
+from twisted.web2.http import HTTPError, StatusResponse
+from twistedcaldav.caldavxml import Property, CalendarData, CalendarComponent,\
+    AllProperties, AllComponents
+from twistedcaldav.datafilters.calendardata import CalendarDataFilter
+from twistedcaldav.datafilters.filter import CalendarFilter
+from twistedcaldav.ical import Component
+
+__all__ = [
+    "PrivateEventFilter",
+]
+
+class PrivateEventFilter(CalendarFilter):
+    """
+    Filter a private event to match the rights of the non-owner user accessing the data
+    """
+
+    def __init__(self, accessRestriction, isowner):
+        """
+        
+        @param accessRestriction: one of the access levels in L{Component}
+        @type accessRestriction: C{str}
+        @param isowner: whether the current user is the owner of the data
+        @type isowner: C{bool}
+        """
+        
+        self.accessRestriction = accessRestriction
+        self.isowner = isowner
+    
+    def filter(self, ical):
+        """
+        Filter the supplied iCalendar (vobject) data using the request information.
+
+        @param ical: iCalendar object
+        @type ical: L{Component} or C{str}
+        
+        @return: L{Component} for the filtered calendar data
+        """
+        
+        if self.isowner or self.accessRestriction == Component.ACCESS_PUBLIC or self.accessRestriction is None:
+            # No need to filter for the owner or public event
+            return ical
+        
+        elif self.accessRestriction == Component.ACCESS_PRIVATE:
+            # We should never get here because ACCESS_PRIVATE is protected via an ACL
+            raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Access Denied"))
+
+        elif self.accessRestriction == Component.ACCESS_PUBLIC:
+            return ical
+        elif self.accessRestriction in (Component.ACCESS_CONFIDENTIAL, Component.ACCESS_RESTRICTED):
+            # Create a CALDAV:calendar-data element with the appropriate iCalendar Component/Property
+            # filter in place for the access restriction in use
+            
+            extra_access = ()
+            if self.accessRestriction == Component.ACCESS_RESTRICTED:
+                extra_access = (
+                    Property(name="SUMMARY"),
+                    Property(name="LOCATION"),
+                )
+
+            calendardata = CalendarData(
+                CalendarComponent(
+                    
+                    # VCALENDAR properties
+                    Property(name="PRODID"),
+                    Property(name="VERSION"),
+                    Property(name="CALSCALE"),
+                    Property(name=Component.ACCESS_PROPERTY),
+
+                    # VEVENT
+                    CalendarComponent(
+                        Property(name="UID"),
+                        Property(name="RECURRENCE-ID"),
+                        Property(name="SEQUENCE"),
+                        Property(name="DTSTAMP"),
+                        Property(name="STATUS"),
+                        Property(name="TRANSP"),
+                        Property(name="DTSTART"),
+                        Property(name="DTEND"),
+                        Property(name="DURATION"),
+                        Property(name="RRULE"),
+                        Property(name="RDATE"),
+                        Property(name="EXRULE"),
+                        Property(name="EXDATE"),
+                        *extra_access,
+                        **{"name":"VEVENT"}
+                    ),
+                    
+                    # VTODO
+                    CalendarComponent(
+                        Property(name="UID"),
+                        Property(name="RECURRENCE-ID"),
+                        Property(name="SEQUENCE"),
+                        Property(name="DTSTAMP"),
+                        Property(name="STATUS"),
+                        Property(name="DTSTART"),
+                        Property(name="COMPLETED"),
+                        Property(name="DUE"),
+                        Property(name="DURATION"),
+                        Property(name="RRULE"),
+                        Property(name="RDATE"),
+                        Property(name="EXRULE"),
+                        Property(name="EXDATE"),
+                        *extra_access,
+                        **{"name":"VTODO"}
+                    ),
+                    
+                    # VJOURNAL
+                    CalendarComponent(
+                        Property(name="UID"),
+                        Property(name="RECURRENCE-ID"),
+                        Property(name="SEQUENCE"),
+                        Property(name="DTSTAMP"),
+                        Property(name="STATUS"),
+                        Property(name="TRANSP"),
+                        Property(name="DTSTART"),
+                        Property(name="RRULE"),
+                        Property(name="RDATE"),
+                        Property(name="EXRULE"),
+                        Property(name="EXDATE"),
+                        *extra_access,
+                        **{"name":"VJOURNAL"}
+                    ),
+                    
+                    # VFREEBUSY
+                    CalendarComponent(
+                        Property(name="UID"),
+                        Property(name="DTSTAMP"),
+                        Property(name="DTSTART"),
+                        Property(name="DTEND"),
+                        Property(name="DURATION"),
+                        Property(name="FREEBUSY"),
+                        *extra_access,
+                        **{"name":"VFREEBUSY"}
+                    ),
+                    
+                    # VTIMEZONE
+                    CalendarComponent(
+                        AllProperties(),
+                        AllComponents(),
+                        name="VTIMEZONE",
+                    ),
+                    name="VCALENDAR",
+                ),
+            )
+
+            # Now "filter" the resource calendar data through the CALDAV:calendar-data element
+            return CalendarDataFilter(calendardata).filter(ical)
+        else:
+            # Unknown access restriction
+            raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Access Denied"))
+    
+    def merge(self, icalnew, icalold):
+        """
+        Private event merging does not happen
+        """
+        raise NotImplementedError

Deleted: CalendarServer/trunk/twistedcaldav/datafilters/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/datafilters/test/__init__.py	2010-04-07 19:28:58 UTC (rev 5440)
+++ CalendarServer/trunk/twistedcaldav/datafilters/test/__init__.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -1,19 +0,0 @@
-##
-# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Tests for the twistedcaldav.datafilters module.
-"""

Copied: CalendarServer/trunk/twistedcaldav/datafilters/test/__init__.py (from rev 5440, CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/datafilters/test/__init__.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/datafilters/test/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/datafilters/test/__init__.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Tests for the twistedcaldav.datafilters module.
+"""

Deleted: CalendarServer/trunk/twistedcaldav/datafilters/test/test_calendardata.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/datafilters/test/test_calendardata.py	2010-04-07 19:28:58 UTC (rev 5440)
+++ CalendarServer/trunk/twistedcaldav/datafilters/test/test_calendardata.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -1,371 +0,0 @@
-##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-import twistedcaldav.test.util
-from twistedcaldav.datafilters.calendardata import CalendarDataFilter
-from twistedcaldav.caldavxml import CalendarData, CalendarComponent,\
-    AllComponents, AllProperties, Property
-from twistedcaldav.ical import Component
-
-class CalendarDataTest (twistedcaldav.test.util.TestCase):
-
-    def test_empty(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        empty = CalendarData()
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(CalendarDataFilter(empty).filter(item)), data)
-
-    def test_vcalendar_no_effect(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        no_effect = CalendarData(
-            CalendarComponent(
-                name="VCALENDAR"
-            )
-        )
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(CalendarDataFilter(no_effect).filter(item)), data)
- 
-        no_effect = CalendarData(
-            CalendarComponent(
-                AllComponents(),
-                AllProperties(),
-                name="VCALENDAR"
-            )
-        )
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(CalendarDataFilter(no_effect).filter(item)), data)
-
-    def test_vcalendar_no_props(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-X-WR-CALNAME:Help
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        result = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//PYVOBJECT//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        empty = CalendarData(
-            CalendarComponent(
-                AllComponents(),
-                name="VCALENDAR"
-            )
-        )
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(CalendarDataFilter(empty).filter(item)), result)
-
-    def test_vcalendar_no_comp(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-X-WR-CALNAME:Help
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        result = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-X-WR-CALNAME:Help
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        empty = CalendarData(
-            CalendarComponent(
-                AllProperties(),
-                name="VCALENDAR"
-            )
-        )
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(CalendarDataFilter(empty).filter(item)), result)
-
-    def test_vevent_no_effect(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        no_effect = CalendarData(
-            CalendarComponent(
-                CalendarComponent(
-                    name="VEVENT"
-                ),
-                AllProperties(),
-                name="VCALENDAR"
-            )
-        )
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(CalendarDataFilter(no_effect).filter(item)), data)
-
-    def test_vevent_other_component(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        result = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        other_component = CalendarData(
-            CalendarComponent(
-                CalendarComponent(
-                    name="VTODO"
-                ),
-                AllProperties(),
-                name="VCALENDAR"
-            )
-        )
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(CalendarDataFilter(other_component).filter(item)), result)
-
-    def test_vevent_no_props(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        result = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        empty = CalendarData(
-            CalendarComponent(
-                CalendarComponent(
-                    AllComponents(),
-                    name="VEVENT"
-                ),
-                AllProperties(),
-                name="VCALENDAR"
-            )
-        )
-        
-        for item in (data, Component.fromString(data),):
-            filtered = str(CalendarDataFilter(empty).filter(item))
-            filtered = "".join([line for line in filtered.splitlines(True) if not line.startswith("UID:")])
-            self.assertEqual(filtered, result)
-
-    def test_vevent_no_comp(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        result = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        empty = CalendarData(
-            CalendarComponent(
-                CalendarComponent(
-                    AllProperties(),
-                    name="VEVENT"
-                ),
-                AllProperties(),
-                name="VCALENDAR"
-            )
-        )
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(CalendarDataFilter(empty).filter(item)), result)
-
-    def test_vevent_some_props(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        result = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        empty = CalendarData(
-            CalendarComponent(
-                CalendarComponent(
-                    AllComponents(),
-                    Property(
-                        name="UID",
-                    ),
-                    Property(
-                        name="DTSTART",
-                    ),
-                    Property(
-                        name="DTEND",
-                    ),
-                    name="VEVENT"
-                ),
-                AllProperties(),
-                name="VCALENDAR"
-            )
-        )
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(CalendarDataFilter(empty).filter(item)), result)
-

Copied: CalendarServer/trunk/twistedcaldav/datafilters/test/test_calendardata.py (from rev 5440, CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/datafilters/test/test_calendardata.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/datafilters/test/test_calendardata.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/datafilters/test/test_calendardata.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -0,0 +1,371 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import twistedcaldav.test.util
+from twistedcaldav.datafilters.calendardata import CalendarDataFilter
+from twistedcaldav.caldavxml import CalendarData, CalendarComponent,\
+    AllComponents, AllProperties, Property
+from twistedcaldav.ical import Component
+
+class CalendarDataTest (twistedcaldav.test.util.TestCase):
+
+    def test_empty(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        empty = CalendarData()
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(CalendarDataFilter(empty).filter(item)), data)
+
+    def test_vcalendar_no_effect(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        no_effect = CalendarData(
+            CalendarComponent(
+                name="VCALENDAR"
+            )
+        )
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(CalendarDataFilter(no_effect).filter(item)), data)
+ 
+        no_effect = CalendarData(
+            CalendarComponent(
+                AllComponents(),
+                AllProperties(),
+                name="VCALENDAR"
+            )
+        )
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(CalendarDataFilter(no_effect).filter(item)), data)
+
+    def test_vcalendar_no_props(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+X-WR-CALNAME:Help
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        result = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        empty = CalendarData(
+            CalendarComponent(
+                AllComponents(),
+                name="VCALENDAR"
+            )
+        )
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(CalendarDataFilter(empty).filter(item)), result)
+
+    def test_vcalendar_no_comp(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+X-WR-CALNAME:Help
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        result = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+X-WR-CALNAME:Help
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        empty = CalendarData(
+            CalendarComponent(
+                AllProperties(),
+                name="VCALENDAR"
+            )
+        )
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(CalendarDataFilter(empty).filter(item)), result)
+
+    def test_vevent_no_effect(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        no_effect = CalendarData(
+            CalendarComponent(
+                CalendarComponent(
+                    name="VEVENT"
+                ),
+                AllProperties(),
+                name="VCALENDAR"
+            )
+        )
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(CalendarDataFilter(no_effect).filter(item)), data)
+
+    def test_vevent_other_component(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        result = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        other_component = CalendarData(
+            CalendarComponent(
+                CalendarComponent(
+                    name="VTODO"
+                ),
+                AllProperties(),
+                name="VCALENDAR"
+            )
+        )
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(CalendarDataFilter(other_component).filter(item)), result)
+
+    def test_vevent_no_props(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        result = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        empty = CalendarData(
+            CalendarComponent(
+                CalendarComponent(
+                    AllComponents(),
+                    name="VEVENT"
+                ),
+                AllProperties(),
+                name="VCALENDAR"
+            )
+        )
+        
+        for item in (data, Component.fromString(data),):
+            filtered = str(CalendarDataFilter(empty).filter(item))
+            filtered = "".join([line for line in filtered.splitlines(True) if not line.startswith("UID:")])
+            self.assertEqual(filtered, result)
+
+    def test_vevent_no_comp(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        result = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        empty = CalendarData(
+            CalendarComponent(
+                CalendarComponent(
+                    AllProperties(),
+                    name="VEVENT"
+                ),
+                AllProperties(),
+                name="VCALENDAR"
+            )
+        )
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(CalendarDataFilter(empty).filter(item)), result)
+
+    def test_vevent_some_props(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        result = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        empty = CalendarData(
+            CalendarComponent(
+                CalendarComponent(
+                    AllComponents(),
+                    Property(
+                        name="UID",
+                    ),
+                    Property(
+                        name="DTSTART",
+                    ),
+                    Property(
+                        name="DTEND",
+                    ),
+                    name="VEVENT"
+                ),
+                AllProperties(),
+                name="VCALENDAR"
+            )
+        )
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(CalendarDataFilter(empty).filter(item)), result)
+

Deleted: CalendarServer/trunk/twistedcaldav/datafilters/test/test_peruserdata.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/datafilters/test/test_peruserdata.py	2010-04-07 19:28:58 UTC (rev 5440)
+++ CalendarServer/trunk/twistedcaldav/datafilters/test/test_peruserdata.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -1,5688 +0,0 @@
-##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-import twistedcaldav.test.util
-from twistedcaldav.ical import Component
-from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
-
-class PerUserDataFilterTestNotRecurring (twistedcaldav.test.util.TestCase):
-
-    def test_public_noperuser(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), data)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").filter(item)), data)
-
-    def test_public_oneuser(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-TRANSP:OPAQUE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result02 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), result01)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user02").filter(item)), result02)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").filter(item)), result02)
-
-    def test_public_twousers(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test01
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-TRANSP:OPAQUE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test02
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-TRANSP:TRANSPARENT
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test01
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result02 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:TRANSPARENT
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test02
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result03 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), result01)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user02").filter(item)), result02)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user03").filter(item)), result03)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").filter(item)), result03)
-
-class PerUserDataFilterTestRecurring (twistedcaldav.test.util.TestCase):
-
-    def test_public_noperuser(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), data)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").filter(item)), data)
-
-    def test_public_oneuser_master(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-TRANSP:OPAQUE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result02 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), result01)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user02").filter(item)), result02)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").filter(item)), result02)
-
-    def test_public_oneuser_master_and_override(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-master
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-TRANSP:OPAQUE
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-TRANSP:TRANSPARENT
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-master
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:TRANSPARENT
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result02 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), result01)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user02").filter(item)), result02)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").filter(item)), result02)
-
-    def test_public_oneuser_override(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-TRANSP:TRANSPARENT
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:TRANSPARENT
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result02 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), result01)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user02").filter(item)), result02)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").filter(item)), result02)
-
-    def test_public_oneuser_master_derived_override(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-master
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-TRANSP:OPAQUE
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-TRANSP:TRANSPARENT
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-master
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T120000Z
-DTEND:20080602T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:TRANSPARENT
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result02 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), result01)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user02").filter(item)), result02)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").filter(item)), result02)
-
-    def test_public_oneuser_master_derived_override_x2(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-master-1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-TRANSP:OPAQUE
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override-1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-TRANSP:TRANSPARENT
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-master-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-TRANSP:OPAQUE
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080603T120000Z
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-TRANSP:TRANSPARENT
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-master-1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T120000Z
-DTEND:20080602T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:TRANSPARENT
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override-1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result02 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-master-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080603T120000Z
-DTSTART:20080603T120000Z
-DTEND:20080603T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:TRANSPARENT
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result03 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), result01)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user02").filter(item)), result02)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").filter(item)), result03)
-
-    def test_public_oneuser_no_master_and_override(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-TRANSP:TRANSPARENT
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:TRANSPARENT
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result02 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), result01)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user02").filter(item)), result02)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").filter(item)), result02)
-
-class PerUserDataMergeTestNewNotRecurring (twistedcaldav.test.util.TestCase):
-
-    def test_public_noperuser(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").merge(item, None)), result01)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").merge(item, None)), data)
-
-    def test_public_oneuser(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result02 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").merge(item, None)), result01)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").merge(item, None)), result02)
-
-class PerUserDataMergeTestNewRecurring (twistedcaldav.test.util.TestCase):
-
-    def test_public_noperuser(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").merge(item, None)), result01)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").merge(item, None)), data)
-
-    def test_public_oneuser_master(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result02 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").merge(item, None)), result01)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").merge(item, None)), result02)
-
-    def test_public_oneuser_master_and_override(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-master
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:TRANSPARENT
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-master
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-TRANSP:TRANSPARENT
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result02 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").merge(item, None)), result01)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").merge(item, None)), result02)
-
-    def test_public_oneuser_override(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:TRANSPARENT
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-TRANSP:TRANSPARENT
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result02 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").merge(item, None)), result01)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").merge(item, None)), result02)
-
-    def test_public_oneuser_master_compact_override(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-master
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T120000Z
-DTEND:20080602T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:TRANSPARENT
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-master
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-TRANSP:TRANSPARENT
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result02 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T120000Z
-DTEND:20080602T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").merge(item, None)), result01)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").merge(item, None)), result02)
-
-    def test_public_oneuser_master_noncompact_override(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-master
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:TRANSPARENT
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-master
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-TRANSP:TRANSPARENT
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-override
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result02 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("user01").merge(item, None)), result01)
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PerUserDataFilter("").merge(item, None)), result02)
-
-class PerUserDataMergeTestExistingNotRecurring (twistedcaldav.test.util.TestCase):
-
-    def test_public_noperuser(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        newresult = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
-
-    def test_public_oneuser(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers_removal(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-class PerUserDataMergeTestExistingNowRecurring (twistedcaldav.test.util.TestCase):
-
-    def test_public_noperuser_master(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        newresult = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
-
-    def test_public_noperuser_master_with_override(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        newresult = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
-
-    def test_public_noperuser_only_override(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        newresult = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
-
-    def test_public_oneuser_master(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_oneuser_master_with_override(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_oneuser_only_override(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers_master(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers_master_with_override(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers_only_override(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers_removal_master(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers_removal_master_with_override(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-
-    def test_public_twousers_removal_only_override(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-class PerUserDataMergeTestExistingWasRecurring (twistedcaldav.test.util.TestCase):
-
-    def test_public_noperuser_master(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        newresult = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
-
-    def test_public_noperuser_master_with_override(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        newresult = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
-
-    def test_public_noperuser_only_override(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        newresult = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
-
-    def test_public_oneuser_master(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_oneuser_master_with_override(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_oneuser_only_override(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers_master(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers_master_with_override(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2.1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2.2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2.1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers_only_override(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2.2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers_removal_master(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers_removal_master_with_override(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-
-    def test_public_twousers_removal_only_override(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2.2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-class PerUserDataMergeTestBothRecurringMasterOnly (twistedcaldav.test.util.TestCase):
-
-    def test_public_noperuser(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        newresult = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
-
-    def test_public_oneuser(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers_removal(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers_invalid_instance(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2.1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080701T110000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2.2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2.1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-class PerUserDataMergeTestBothRecurringMasterWithOverride (twistedcaldav.test.util.TestCase):
-
-    def test_public_noperuser(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        newresult = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T110000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
-
-    def test_public_oneuser(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T110000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T110000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T110000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T110000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2.1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T110000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2.2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2.1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers_removal(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T110000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T110000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2.1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T110000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2.2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2.1
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-class PerUserDataMergeTestBothRecurringOverrideOnly (twistedcaldav.test.util.TestCase):
-
-    def test_public_noperuser(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        newresult = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T110000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
-
-    def test_public_oneuser(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T110000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T110000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T110000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T110000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T110000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2.2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers_removal(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T110000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T110000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1.2
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T110000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2.2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T130000Z
-DTEND:20080602T140000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T120000Z
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-class PerUserDataMergeTestBothRecurringSpecialCase (twistedcaldav.test.util.TestCase):
-
-    def test_public_twousers_recurrence_truncation(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY;COUNT=5
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY;COUNT=10
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080605T110000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080610T110000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY;COUNT=5
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080605T110000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers_recurrence_shift(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080602T110000Z
-DTEND:20080602T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY;COUNT=10
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY;COUNT=10
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080610T110000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080602T110000Z
-DTEND:20080602T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY;COUNT=10
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080610T110000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers_rdate_removed(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY;COUNT=10
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RDATE:20080602T150000Z
-RRULE:FREQ=DAILY;COUNT=10
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T150000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY;COUNT=10
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-
-    def test_public_twousers_exdate_added(self):
-        
-        newdata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-EXDATE:20080602T110000Z
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY;COUNT=10
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        olddata = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY;COUNT=10
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1
-TRIGGER;RELATED=START:-PT20M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-RECURRENCE-ID:20080602T110000Z
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        result01 = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T110000Z
-DTEND:20080601T120000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-EXDATE:20080602T110000Z
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=DAILY;COUNT=10
-END:VEVENT
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user01
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-1mod
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-BEGIN:X-CALENDARSERVER-PERUSER
-UID:12345-67890
-X-CALENDARSERVER-PERUSER-UID:user02
-BEGIN:X-CALENDARSERVER-PERINSTANCE
-TRANSP:OPAQUE
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test-2
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:X-CALENDARSERVER-PERINSTANCE
-END:X-CALENDARSERVER-PERUSER
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for olditem in (olddata, Component.fromString(olddata),):
-            for newitem in (newdata, Component.fromString(newdata),):
-                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
-

Copied: CalendarServer/trunk/twistedcaldav/datafilters/test/test_peruserdata.py (from rev 5440, CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/datafilters/test/test_peruserdata.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/datafilters/test/test_peruserdata.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/datafilters/test/test_peruserdata.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -0,0 +1,5688 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+import twistedcaldav.test.util
+from twistedcaldav.ical import Component
+from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
+
+class PerUserDataFilterTestNotRecurring (twistedcaldav.test.util.TestCase):
+
+    def test_public_noperuser(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), data)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("").filter(item)), data)
+
+    def test_public_oneuser(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:OPAQUE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result02 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), result01)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user02").filter(item)), result02)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("").filter(item)), result02)
+
+    def test_public_twousers(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test01
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:OPAQUE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test02
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test01
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result02 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test02
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result03 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), result01)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user02").filter(item)), result02)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user03").filter(item)), result03)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("").filter(item)), result03)
+
+class PerUserDataFilterTestRecurring (twistedcaldav.test.util.TestCase):
+
+    def test_public_noperuser(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), data)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("").filter(item)), data)
+
+    def test_public_oneuser_master(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:OPAQUE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result02 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), result01)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user02").filter(item)), result02)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("").filter(item)), result02)
+
+    def test_public_oneuser_master_and_override(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-master
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:OPAQUE
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-master
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result02 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), result01)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user02").filter(item)), result02)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("").filter(item)), result02)
+
+    def test_public_oneuser_override(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result02 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), result01)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user02").filter(item)), result02)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("").filter(item)), result02)
+
+    def test_public_oneuser_master_derived_override(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-master
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:OPAQUE
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-master
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result02 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), result01)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user02").filter(item)), result02)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("").filter(item)), result02)
+
+    def test_public_oneuser_master_derived_override_x2(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-master-1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:OPAQUE
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override-1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-master-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:OPAQUE
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080603T120000Z
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-master-1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override-1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result02 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-master-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080603T120000Z
+DTSTART:20080603T120000Z
+DTEND:20080603T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result03 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), result01)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user02").filter(item)), result02)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("").filter(item)), result03)
+
+    def test_public_oneuser_no_master_and_override(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result02 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user01").filter(item)), result01)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user02").filter(item)), result02)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("").filter(item)), result02)
+
+class PerUserDataMergeTestNewNotRecurring (twistedcaldav.test.util.TestCase):
+
+    def test_public_noperuser(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user01").merge(item, None)), result01)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("").merge(item, None)), data)
+
+    def test_public_oneuser(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result02 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user01").merge(item, None)), result01)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("").merge(item, None)), result02)
+
+class PerUserDataMergeTestNewRecurring (twistedcaldav.test.util.TestCase):
+
+    def test_public_noperuser(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user01").merge(item, None)), result01)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("").merge(item, None)), data)
+
+    def test_public_oneuser_master(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result02 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user01").merge(item, None)), result01)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("").merge(item, None)), result02)
+
+    def test_public_oneuser_master_and_override(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-master
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-master
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result02 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user01").merge(item, None)), result01)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("").merge(item, None)), result02)
+
+    def test_public_oneuser_override(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result02 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user01").merge(item, None)), result01)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("").merge(item, None)), result02)
+
+    def test_public_oneuser_master_compact_override(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-master
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-master
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result02 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T120000Z
+DTEND:20080602T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user01").merge(item, None)), result01)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("").merge(item, None)), result02)
+
+    def test_public_oneuser_master_noncompact_override(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-master
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-master
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:TRANSPARENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-override
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result02 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("user01").merge(item, None)), result01)
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PerUserDataFilter("").merge(item, None)), result02)
+
+class PerUserDataMergeTestExistingNotRecurring (twistedcaldav.test.util.TestCase):
+
+    def test_public_noperuser(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        newresult = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
+
+    def test_public_oneuser(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers_removal(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+class PerUserDataMergeTestExistingNowRecurring (twistedcaldav.test.util.TestCase):
+
+    def test_public_noperuser_master(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        newresult = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
+
+    def test_public_noperuser_master_with_override(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        newresult = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
+
+    def test_public_noperuser_only_override(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        newresult = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
+
+    def test_public_oneuser_master(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_oneuser_master_with_override(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_oneuser_only_override(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers_master(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers_master_with_override(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers_only_override(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers_removal_master(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers_removal_master_with_override(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+
+    def test_public_twousers_removal_only_override(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+class PerUserDataMergeTestExistingWasRecurring (twistedcaldav.test.util.TestCase):
+
+    def test_public_noperuser_master(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        newresult = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
+
+    def test_public_noperuser_master_with_override(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        newresult = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
+
+    def test_public_noperuser_only_override(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        newresult = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
+
+    def test_public_oneuser_master(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_oneuser_master_with_override(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_oneuser_only_override(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers_master(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers_master_with_override(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2.1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2.2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2.1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers_only_override(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2.2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers_removal_master(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers_removal_master_with_override(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+
+    def test_public_twousers_removal_only_override(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2.2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+class PerUserDataMergeTestBothRecurringMasterOnly (twistedcaldav.test.util.TestCase):
+
+    def test_public_noperuser(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        newresult = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
+
+    def test_public_oneuser(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers_removal(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers_invalid_instance(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2.1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080701T110000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2.2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2.1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+class PerUserDataMergeTestBothRecurringMasterWithOverride (twistedcaldav.test.util.TestCase):
+
+    def test_public_noperuser(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        newresult = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T110000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
+
+    def test_public_oneuser(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T110000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T110000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T110000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T110000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2.1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T110000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2.2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2.1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers_removal(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T110000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T110000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2.1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T110000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2.2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2.1
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+class PerUserDataMergeTestBothRecurringOverrideOnly (twistedcaldav.test.util.TestCase):
+
+    def test_public_noperuser(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        newresult = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T110000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), newresult)
+
+    def test_public_oneuser(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T110000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T110000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T110000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T110000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T110000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2.2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers_removal(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T110000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T110000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1.2
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T110000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2.2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+class PerUserDataMergeTestBothRecurringSpecialCase (twistedcaldav.test.util.TestCase):
+
+    def test_public_twousers_recurrence_truncation(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=5
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=10
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080605T110000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080610T110000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=5
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080605T110000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers_recurrence_shift(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T110000Z
+DTEND:20080602T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=10
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=10
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080610T110000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080602T110000Z
+DTEND:20080602T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=10
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080610T110000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers_rdate_removed(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=10
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RDATE:20080602T150000Z
+RRULE:FREQ=DAILY;COUNT=10
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T150000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=10
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+
+    def test_public_twousers_exdate_added(self):
+        
+        newdata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+EXDATE:20080602T110000Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=10
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        olddata = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=10
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1
+TRIGGER;RELATED=START:-PT20M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T110000Z
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        result01 = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T110000Z
+DTEND:20080601T120000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+EXDATE:20080602T110000Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=10
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-1mod
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:OPAQUE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test-2
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for olditem in (olddata, Component.fromString(olddata),):
+            for newitem in (newdata, Component.fromString(newdata),):
+                self.assertEqual(str(PerUserDataFilter("user01").merge(newitem, olditem)), result01)
+

Deleted: CalendarServer/trunk/twistedcaldav/datafilters/test/test_privateevents.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/datafilters/test/test_privateevents.py	2010-04-07 19:28:58 UTC (rev 5440)
+++ CalendarServer/trunk/twistedcaldav/datafilters/test/test_privateevents.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -1,179 +0,0 @@
-##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twisted.web2.http import HTTPError
-import twistedcaldav.test.util
-from twistedcaldav.datafilters.privateevents import PrivateEventFilter
-from twistedcaldav.ical import Component
-
-class PrivateEventsTest (twistedcaldav.test.util.TestCase):
-
-    def test_public_default(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PrivateEventFilter(Component.ACCESS_PUBLIC, True).filter(item)), data)
-            self.assertEqual(str(PrivateEventFilter(Component.ACCESS_PUBLIC, False).filter(item)), data)
-
-    def test_public_none(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PrivateEventFilter(None, True).filter(item)), data)
-            self.assertEqual(str(PrivateEventFilter(None, False).filter(item)), data)
-
-    def test_public(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-X-CALENDARSERVER-ACCESS:PUBLIC
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PrivateEventFilter(Component.ACCESS_PUBLIC, True).filter(item)), data)
-            self.assertEqual(str(PrivateEventFilter(Component.ACCESS_PUBLIC, False).filter(item)), data)
-
-    def test_private(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-X-CALENDARSERVER-ACCESS:PRIVATE
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PrivateEventFilter(Component.ACCESS_PRIVATE, True).filter(item)), data)
-            pfilter = PrivateEventFilter(Component.ACCESS_PRIVATE, False)
-            self.assertRaises(HTTPError, pfilter.filter, item)
-
-    def test_confidential(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-DESCRIPTION:In confidence
-LOCATION:My office
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-SUMMARY:Confidential
-END:VEVENT
-X-CALENDARSERVER-ACCESS:CONFIDENTIAL
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        filtered = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-END:VEVENT
-X-CALENDARSERVER-ACCESS:CONFIDENTIAL
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PrivateEventFilter(Component.ACCESS_CONFIDENTIAL, True).filter(item)), data)
-            self.assertEqual(str(PrivateEventFilter(Component.ACCESS_CONFIDENTIAL, False).filter(item)), filtered)
-
-    def test_restricted(self):
-        
-        data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-DESCRIPTION:In confidence
-LOCATION:My office
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-SUMMARY:Confidential
-END:VEVENT
-X-CALENDARSERVER-ACCESS:RESTRICTED
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        filtered = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-LOCATION:My office
-SUMMARY:Confidential
-END:VEVENT
-X-CALENDARSERVER-ACCESS:RESTRICTED
-END:VCALENDAR
-""".replace("\n", "\r\n")
-        
-        for item in (data, Component.fromString(data),):
-            self.assertEqual(str(PrivateEventFilter(Component.ACCESS_RESTRICTED, True).filter(item)), data)
-            self.assertEqual(str(PrivateEventFilter(Component.ACCESS_RESTRICTED, False).filter(item)), filtered)

Copied: CalendarServer/trunk/twistedcaldav/datafilters/test/test_privateevents.py (from rev 5440, CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/datafilters/test/test_privateevents.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/datafilters/test/test_privateevents.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/datafilters/test/test_privateevents.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -0,0 +1,179 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twisted.web2.http import HTTPError
+import twistedcaldav.test.util
+from twistedcaldav.datafilters.privateevents import PrivateEventFilter
+from twistedcaldav.ical import Component
+
+class PrivateEventsTest (twistedcaldav.test.util.TestCase):
+
+    def test_public_default(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PrivateEventFilter(Component.ACCESS_PUBLIC, True).filter(item)), data)
+            self.assertEqual(str(PrivateEventFilter(Component.ACCESS_PUBLIC, False).filter(item)), data)
+
+    def test_public_none(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PrivateEventFilter(None, True).filter(item)), data)
+            self.assertEqual(str(PrivateEventFilter(None, False).filter(item)), data)
+
+    def test_public(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+X-CALENDARSERVER-ACCESS:PUBLIC
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PrivateEventFilter(Component.ACCESS_PUBLIC, True).filter(item)), data)
+            self.assertEqual(str(PrivateEventFilter(Component.ACCESS_PUBLIC, False).filter(item)), data)
+
+    def test_private(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+X-CALENDARSERVER-ACCESS:PRIVATE
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PrivateEventFilter(Component.ACCESS_PRIVATE, True).filter(item)), data)
+            pfilter = PrivateEventFilter(Component.ACCESS_PRIVATE, False)
+            self.assertRaises(HTTPError, pfilter.filter, item)
+
+    def test_confidential(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DESCRIPTION:In confidence
+LOCATION:My office
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+SUMMARY:Confidential
+END:VEVENT
+X-CALENDARSERVER-ACCESS:CONFIDENTIAL
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        filtered = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+END:VEVENT
+X-CALENDARSERVER-ACCESS:CONFIDENTIAL
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PrivateEventFilter(Component.ACCESS_CONFIDENTIAL, True).filter(item)), data)
+            self.assertEqual(str(PrivateEventFilter(Component.ACCESS_CONFIDENTIAL, False).filter(item)), filtered)
+
+    def test_restricted(self):
+        
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DESCRIPTION:In confidence
+LOCATION:My office
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+SUMMARY:Confidential
+END:VEVENT
+X-CALENDARSERVER-ACCESS:RESTRICTED
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        filtered = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+LOCATION:My office
+SUMMARY:Confidential
+END:VEVENT
+X-CALENDARSERVER-ACCESS:RESTRICTED
+END:VCALENDAR
+""".replace("\n", "\r\n")
+        
+        for item in (data, Component.fromString(data),):
+            self.assertEqual(str(PrivateEventFilter(Component.ACCESS_RESTRICTED, True).filter(item)), data)
+            self.assertEqual(str(PrivateEventFilter(Component.ACCESS_RESTRICTED, False).filter(item)), filtered)

Modified: CalendarServer/trunk/twistedcaldav/directory/calendar.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/calendar.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/directory/calendar.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -46,6 +46,7 @@
 from twistedcaldav.directory.idirectory import IDirectoryService
 from twistedcaldav.directory.wiki import getWikiACL
 from twistedcaldav.directory.resource import AutoProvisioningResourceMixIn
+from twistedcaldav.notifications import NotificationCollectionResource
 
 log = Logger()
 
@@ -277,6 +278,10 @@
             childlist += (
                 ("freebusy", FreeBusyURLResource),
             )
+        if config.Sharing.Enabled and config.Sharing.Calendars.Enabled:
+            childlist += (
+                ("notification", NotificationCollectionResource),
+            )
         for name, cls in childlist:
             child = self.provisionChild(name)
             assert isinstance(child, cls), "Child %r is not a %s: %r" % (name, cls.__name__, child)
@@ -360,6 +365,9 @@
     def ownerPrincipal(self, request):
         return succeed(self.principalForRecord())
 
+    def resourceOwnerPrincipal(self, request):
+        return succeed(self.principalForRecord())
+
     def defaultAccessControlList(self):
         myPrincipal = self.principalForRecord()
 

Modified: CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/directory/calendaruserproxy.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -128,13 +128,13 @@
         """
         return ProxyDBService
 
-    def resourceType(self):
+    def resourceType(self, request):
         if self.proxyType == "calendar-proxy-read":
-            return davxml.ResourceType.calendarproxyread
+            return succeed(davxml.ResourceType.calendarproxyread)
         elif self.proxyType == "calendar-proxy-write":
-            return davxml.ResourceType.calendarproxywrite
+            return succeed(davxml.ResourceType.calendarproxywrite)
         else:
-            return super(CalendarUserProxyPrincipalResource, self).resourceType()
+            return super(CalendarUserProxyPrincipalResource, self).resourceType(request)
 
     def isProxyType(self, read_write):
         if (

Modified: CalendarServer/trunk/twistedcaldav/directory/directory.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/directory.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/directory/directory.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -124,12 +124,16 @@
             return (
                 credentials.authnPrincipal.principalURL(),
                 credentials.authzPrincipal.principalURL(),
+                credentials.authnPrincipal,
+                credentials.authzPrincipal,
             )
         else:
             if credentials.authnPrincipal.record.verifyCredentials(credentials.credentials):
                 return (
                     credentials.authnPrincipal.principalURL(),
                     credentials.authzPrincipal.principalURL(),
+                    credentials.authnPrincipal,
+                    credentials.authzPrincipal,
                 )
             else:
                 raise UnauthorizedLogin("Incorrect credentials for %s" % (credentials.credentials.username,)) 

Modified: CalendarServer/trunk/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/principal.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/directory/principal.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -887,6 +887,16 @@
 
         return succeed(inbox)
 
+    def notificationCollection(self, request):
+        
+        notification = None
+        if config.Sharing.Enabled:
+            home = self.calendarHome()
+            if home is not None:    
+                notification = home.getChild("notification")
+    
+        return succeed(notification)
+
     def calendarHomeURLs(self):
         homeURL = self._homeChildURL(None)
         return (homeURL,) if homeURL else ()
@@ -903,6 +913,12 @@
         else:
             return None
 
+    def notificationURL(self):
+        if config.Sharing.Enabled:
+            return self._homeChildURL("notification/")
+        else:
+            return None
+
     def addressBookHomeURLs(self):
         home = self._addressBookHome()
         if home is None:

Modified: CalendarServer/trunk/twistedcaldav/directory/sudo.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/sudo.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/directory/sudo.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -109,6 +109,8 @@
             return (
                 credentials.authnPrincipal.principalURL(),
                 credentials.authzPrincipal.principalURL(),
+                credentials.authnPrincipal,
+                credentials.authzPrincipal,
                 )
         else:
             raise UnauthorizedLogin(

Modified: CalendarServer/trunk/twistedcaldav/directorybackedaddressbook.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directorybackedaddressbook.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/directorybackedaddressbook.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -94,8 +94,8 @@
            ),
         )
 
-    def resourceType(self):
-        return davxml.ResourceType.addressbook #@UndefinedVariable
+    def resourceType(self, request):
+        return succeed(davxml.ResourceType.addressbook)
 
     def isDirectoryBackedAddressBookCollection(self):
         return True

Modified: CalendarServer/trunk/twistedcaldav/dropbox.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/dropbox.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/dropbox.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -32,14 +32,16 @@
 
 from twistedcaldav.customxml import calendarserver_namespace
 
+from twisted.internet.defer import succeed
+
 log = Logger()
 
 class DropBoxHomeResource (DAVResource):
     """
     Drop box collection resource.
     """
-    def resourceType(self):
-        return davxml.ResourceType.dropboxhome
+    def resourceType(self, request):
+        return succeed(davxml.ResourceType.dropboxhome)
 
     def isCollection(self):
         return True
@@ -51,8 +53,8 @@
     """
     Drop box resource.
     """
-    def resourceType(self):
-        return davxml.ResourceType.dropbox
+    def resourceType(self, request):
+        return succeed(davxml.ResourceType.dropbox)
 
     def isCollection(self):
         return True

Modified: CalendarServer/trunk/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/extensions.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/extensions.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -472,7 +472,8 @@
 
         if namespace == dav_namespace:
             if name == "resourcetype":
-                returnValue(self.resourceType())
+                rtype = (yield self.resourceType(request))
+                returnValue(rtype)
 
         elif namespace == calendarserver_namespace:
             if name == "expanded-group-member-set":
@@ -513,14 +514,14 @@
     def expandedGroupMemberships(self):
         return succeed(())
 
-    def resourceType(self):
+    def resourceType(self, request):
         # Allow live property to be overridden by dead property
         if self.deadProperties().contains((dav_namespace, "resourcetype")):
-            return self.deadProperties().get((dav_namespace, "resourcetype"))
+            return succeed(self.deadProperties().get((dav_namespace, "resourcetype")))
         if self.isCollection():
-            return davxml.ResourceType(davxml.Collection(), davxml.Principal())
+            return succeed(davxml.ResourceType(davxml.Collection(), davxml.Principal()))
         else:
-            return davxml.ResourceType(davxml.Principal())
+            return succeed(davxml.ResourceType(davxml.Principal()))
 
 
 class DAVFile (SudoersMixin, SuperDAVFile, LoggingMixIn):
@@ -534,17 +535,17 @@
             qname = property.qname()
 
         if qname == (dav_namespace, "resourcetype"):
-            return succeed(self.resourceType())
+            return self.resourceType(request)
 
         return super(DAVFile, self).readProperty(property, request)
 
-    def resourceType(self):
+    def resourceType(self, request):
         # Allow live property to be overridden by dead property
         if self.deadProperties().contains((dav_namespace, "resourcetype")):
-            return self.deadProperties().get((dav_namespace, "resourcetype"))
+            return succeed(self.deadProperties().get((dav_namespace, "resourcetype")))
         if self.isCollection():
-            return davxml.ResourceType.collection
-        return davxml.ResourceType.empty
+            return succeed(davxml.ResourceType.collection)
+        return succeed(davxml.ResourceType.empty)
 
     def render(self, request):
         if not self.fp.exists():
@@ -861,38 +862,44 @@
         self.propertyStore = propertyStore
         self.resource = propertyStore.resource
 
-    def get(self, qname):
+    def get(self, qname, uid=None):
         #self.log_debug("Get: %r, %r" % (self.resource.fp.path, qname))
 
         cache = self._cache()
+        
+        cachedQname = qname + (uid,)
 
-        if qname in cache:
-            property = cache.get(qname, None)
+        if cachedQname in cache:
+            property = cache.get(cachedQname, None)
             if property is None:
                 self.log_debug("Cache miss: %r, %r, %r" % (self, self.resource.fp.path, qname))
                 try:
-                    property = self.propertyStore.get(qname)
+                    property = self.propertyStore.get(qname, uid)
                 except HTTPError:
-                    del cache[qname]
+                    del cache[cachedQname]
                     raise PropertyNotFoundError(qname)
-                cache[qname] = property
+                cache[cachedQname] = property
 
             return property
         else:
             raise PropertyNotFoundError(qname)
 
-    def set(self, property):
+    def set(self, property, uid=None):
         #self.log_debug("Set: %r, %r" % (self.resource.fp.path, property))
 
         cache = self._cache()
 
-        cache[property.qname()] = None
-        self.propertyStore.set(property)
-        cache[property.qname()] = property
+        cachedQname = property.qname() + (uid,)
 
-    def contains(self, qname):
+        cache[cachedQname] = None
+        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))
 
+        cachedQname = qname + (uid,)
+
         try:
             cache = self._cache()
         except HTTPError, e:
@@ -901,30 +908,40 @@
             else:
                 raise
 
-        if qname in cache:
+        if cachedQname in cache:
             #self.log_debug("Contains cache hit: %r, %r, %r" % (self, self.resource.fp.path, qname))
             return True
         else:
             return False
 
-    def delete(self, qname):
+    def delete(self, qname, uid=None):
         #self.log_debug("Delete: %r, %r" % (self.resource.fp.path, qname))
 
-        if self._data is not None and qname in self._data:
-            del self._data[qname]
+        cachedQname = qname + (uid,)
 
-        self.propertyStore.delete(qname)
+        if self._data is not None and cachedQname in self._data:
+            del self._data[cachedQname]
 
-    def list(self):
+        self.propertyStore.delete(qname, uid)
+
+    def list(self, uid=None, filterByUID=True):
         #self.log_debug("List: %r" % (self.resource.fp.path,))
-        return self._cache().iterkeys()
+        keys = self._cache().iterkeys()
+        if filterByUID:
+            return [ 
+                (namespace, name)
+                for namespace, name, propuid in keys
+                if propuid == uid
+            ]
+        else:
+            return keys
 
     def _cache(self):
         if not hasattr(self, "_data"):
             #self.log_debug("Cache init: %r" % (self.resource.fp.path,))
             self._data = dict(
                 (name, None)
-                for name in self.propertyStore.list()
+                for name in self.propertyStore.list(filterByUID=False)
             )
         return self._data
 

Modified: CalendarServer/trunk/twistedcaldav/freebusyurl.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/freebusyurl.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/freebusyurl.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -26,7 +26,7 @@
 
 from vobject.icalendar import utc
 
-from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 
 from twext.python.log import Logger
 from twext.web2 import responsecode
@@ -99,8 +99,8 @@
             )
         return davxml.ACL(*aces)
 
-    def resourceType(self):
-        return davxml.ResourceType.freebusyurl
+    def resourceType(self, request):
+        return succeed(davxml.ResourceType.freebusyurl)
 
     def isCollection(self):
         return False

Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/ical.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -131,6 +131,8 @@
     "ORGANIZER":    normalizeCUAddr,
 }
 
+ignoredComponents = ("VTIMEZONE", "X-CALENDARSERVER-PERUSER",)
+
 class InvalidICalendarDataError(ValueError):
     pass
 
@@ -423,7 +425,7 @@
         
         mtype = None
         for component in self.subcomponents():
-            if component.name() == "VTIMEZONE":
+            if component.name() in ignoredComponents:
                 continue
             elif mtype and (mtype != component.name()):
                 raise InvalidICalendarDataError("Component contains more than one type of primary type: %r" % (self,))
@@ -442,7 +444,7 @@
         
         result = None
         for component in self.subcomponents():
-            if component.name() == "VTIMEZONE":
+            if component.name() in ignoredComponents:
                 continue
             elif not allow_multiple and (result is not None):
                 raise InvalidICalendarDataError("Calendar contains more than one primary component: %r" % (self,))
@@ -462,7 +464,7 @@
         assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
         
         for component in self.subcomponents():
-            if component.name() == "VTIMEZONE":
+            if component.name() in ignoredComponents:
                 continue
             if not component.hasProperty("RECURRENCE-ID"):
                 return component
@@ -481,7 +483,7 @@
         assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
         
         for component in self.subcomponents():
-            if component.name() == "VTIMEZONE":
+            if component.name() in ignoredComponents:
                 continue
             rid = component.getRecurrenceIDUTC()
             if rid and recurrence_id and dateordatetime(rid) == recurrence_id:
@@ -758,10 +760,6 @@
         if self.name() not in ("VEVENT", ):
             return "FREE"
         
-        # If it is TRANSPARENT we always ignore it
-        if self.propertyValue("TRANSP") == "TRANSPARENT":
-            return "FREE"
-        
         # Handle status
         status = self.propertyValue("STATUS")
         if status == "CANCELLED":
@@ -1023,7 +1021,7 @@
         if self.name() == "VCALENDAR":
             result = ()
             for component in self.subcomponents():
-                if component.name() != "VTIMEZONE":
+                if component.name() not in ignoredComponents:
                     result += component.getComponentInstances()
             return result
         else:
@@ -1038,7 +1036,7 @@
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
             for component in self.subcomponents():
-                if component.name() != "VTIMEZONE" and component.isRecurring():
+                if component.name() not in ignoredComponents and component.isRecurring():
                     return True
         else:
             for propname in ("RRULE", "RDATE", "EXDATE", "RECURRENCE-ID",):
@@ -1158,6 +1156,51 @@
 
         return newcomp
         
+    def validInstances(self, rids):
+        """
+        Test whether the specified recurrence-ids are valid instances in this event.
+
+        @param rid: recurrence-id values
+        @type rid: iterable
+        
+        @return: C{set} of valid rids
+        """
+        
+        valid = set()
+        non_master_rids = [rid for rid in rids if rid is not None]
+        if non_master_rids:
+            highest_rid = max(non_master_rids)
+            self.cacheExpandedTimeRanges(highest_rid + datetime.timedelta(days=1))
+        for rid in rids:
+            if self.validInstance(rid, clear_cache=False):
+                valid.add(rid)
+        return valid
+
+    def validInstance(self, rid, clear_cache=True):
+        """
+        Test whether the specified recurrence-id is a valid instance in this event.
+
+        @param rid: recurrence-id value
+        @type rid: L{datetime.datetime}
+        
+        @return: C{bool}
+        """
+        
+        # First check overridden instances already in this component
+        if not hasattr(self, "cachedComponentInstances") or clear_cache:
+            self.cachedComponentInstances = set(self.getComponentInstances())
+        if rid in self.cachedComponentInstances:
+            return True
+            
+        # Must have a master component
+        if self.masterComponent() is None:
+            return False
+
+        # Get expansion
+        instances = self.cacheExpandedTimeRanges(rid + datetime.timedelta(days=1))
+        new_rids = set([instances[key].rid for key in instances])
+        return rid in new_rids
+
     def resourceUID(self):
         """
         @return: the UID of the subcomponents in this component.
@@ -1166,7 +1209,7 @@
 
         if not hasattr(self, "_resource_uid"):
             for subcomponent in self.subcomponents():
-                if subcomponent.name() != "VTIMEZONE":
+                if subcomponent.name() not in ignoredComponents:
                     self._resource_uid = subcomponent.propertyValue("UID")
                     break
             else:
@@ -1188,6 +1231,8 @@
                 name = subcomponent.name()
                 if name == "VTIMEZONE":
                     has_timezone = True
+                elif subcomponent.name() in ignoredComponents:
+                    continue
                 else:
                     self._resource_type = name
                     break
@@ -1259,6 +1304,8 @@
         
             if subcomponent.name() == "VTIMEZONE":
                 timezones.add(subcomponent.propertyValue("TZID"))
+            elif subcomponent.name() in ignoredComponents:
+                continue
             else:
                 if ctype is None:
                     ctype = subcomponent.name()
@@ -1477,7 +1524,7 @@
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
             for component in self.subcomponents():
-                if component.name() != "VTIMEZONE":
+                if component.name() not in ignoredComponents:
                     return component.getOrganizer()
         else:
             try:
@@ -1499,7 +1546,7 @@
         if self.name() == "VCALENDAR":
             result = ()
             for component in self.subcomponents():
-                if component.name() != "VTIMEZONE":
+                if component.name() not in ignoredComponents:
                     result += component.getOrganizersByInstance()
             return result
         else:
@@ -1523,7 +1570,7 @@
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
             for component in self.subcomponents():
-                if component.name() != "VTIMEZONE":
+                if component.name() not in ignoredComponents:
                     return component.getOrganizerProperty()
         else:
             try:
@@ -1557,7 +1604,7 @@
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
             for component in self.subcomponents():
-                if component.name() != "VTIMEZONE":
+                if component.name() not in ignoredComponents:
                     return component.getAttendees()
         else:
             # Find the property values
@@ -1580,7 +1627,7 @@
         if self.name() == "VCALENDAR":
             result = ()
             for component in self.subcomponents():
-                if component.name() != "VTIMEZONE":
+                if component.name() not in ignoredComponents:
                     result += component.getAttendeesByInstance(makeUnique, onlyScheduleAgentServer)
             return result
         else:
@@ -1618,7 +1665,7 @@
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
             for component in self.subcomponents():
-                if component.name() != "VTIMEZONE":
+                if component.name() not in ignoredComponents:
                     attendee = component.getAttendeeProperty(match)
                     if attendee is not None:
                         return attendee
@@ -1643,7 +1690,7 @@
         # Extract appropriate sub-component if this is a VCALENDAR
         results = []
         for component in self.subcomponents():
-            if component.name() != "VTIMEZONE":
+            if component.name() not in ignoredComponents:
                 attendee = component.getAttendeeProperty(match)
                 if attendee:
                     results.append(attendee)
@@ -1660,7 +1707,7 @@
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
             for component in self.subcomponents():
-                if component.name() != "VTIMEZONE":
+                if component.name() not in ignoredComponents:
                     for attendee in component.getAllAttendeeProperties():
                         yield attendee
         else:
@@ -1679,7 +1726,7 @@
         # Extract appropriate sub-component if this is a VCALENDAR
         if self.name() == "VCALENDAR":
             for component in self.subcomponents():
-                if component.name() != "VTIMEZONE":
+                if component.name() not in ignoredComponents:
                     return component.getMaskUID()
         else:
             try:
@@ -1705,7 +1752,7 @@
         """
         
         for component in self.subcomponents():
-            if component.name() == "VTIMEZONE":
+            if component.name() in ignoredComponents:
                 continue
             for property in component.properties(propname):
                 if propvalue is None or property.value() == propvalue:
@@ -1738,7 +1785,7 @@
         """
 
         for component in self.subcomponents():
-            if component.name() == "VTIMEZONE":
+            if component.name() in ignoredComponents:
                 continue
             found = component.getProperty(property.name())
             if not found or found.value() != property.value():
@@ -1755,7 +1802,7 @@
         """
         
         for component in self.subcomponents():
-            if component.name() == "VTIMEZONE":
+            if component.name() in ignoredComponents:
                 continue
             component.addProperty(property)
 
@@ -1766,7 +1813,7 @@
         """
         
         for component in self.subcomponents():
-            if component.name() == "VTIMEZONE":
+            if component.name() in ignoredComponents:
                 continue
             component.replaceProperty(property)
     
@@ -1785,7 +1832,7 @@
         
         if self.name() == "VCALENDAR":
             for component in self.subcomponents():
-                if component.name() == "VTIMEZONE":
+                if component.name() in ignoredComponents:
                     continue
                 component.transferProperties(from_calendar, properties)
         else:
@@ -1815,7 +1862,7 @@
         master_component = None
         removed_master = False
         for component in self.subcomponents():
-            if component.name() == "VTIMEZONE":
+            if component.name() in ignoredComponents:
                 continue
             found_all_attendees = True
             for attendee in attendees:
@@ -1859,7 +1906,7 @@
         components = tuple(self.subcomponents())
         remaining = len(components)
         for component in components:
-            if component.name() == "VTIMEZONE":
+            if component.name() in ignoredComponents:
                 remaining -= 1
                 continue
             rid = component.getRecurrenceIDUTC()
@@ -1877,7 +1924,7 @@
         assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
 
         for component in self.subcomponents():
-            if component.name() == "VTIMEZONE":
+            if component.name() in ignoredComponents:
                 continue
             [component.removeProperty(p) for p in tuple(component.properties("ATTENDEE")) if p.value().lower() != attendee.lower()]
             
@@ -1888,7 +1935,7 @@
 
         if self.name() == "VCALENDAR":
             for component in self.subcomponents():
-                if component.name() == "VTIMEZONE":
+                if component.name() in ignoredComponents:
                     continue
                 component.removeAlarms()
         else:
@@ -1905,13 +1952,22 @@
             for component in self.subcomponents():
                 component.filterProperties(remove, keep, do_subcomponents=False)
         else:
-            if self.name() == "VTIMEZONE":
+            if self.name() in ignoredComponents:
                 return
             
             for p in tuple(self.properties()):
                 if (keep and p.name() not in keep) or (remove and p.name() in remove):
                     self.removeProperty(p)
                 
+    def removeXComponents(self, keep_components=()):
+        """
+        Remove all X- properties except the specified ones
+        """
+
+        for component in tuple(self.subcomponents()):
+            if component.name().startswith("X-") and component.name() not in keep_components:
+                self.removeComponent(component)
+            
     def removeXProperties(self, keep_properties=(), remove_x_parameters=True, do_subcomponents=True):
         """
         Remove all X- properties except the specified ones
@@ -1921,7 +1977,7 @@
             for component in self.subcomponents():
                 component.removeXProperties(keep_properties, remove_x_parameters, do_subcomponents=False)
         else:
-            if self.name() == "VTIMEZONE":
+            if self.name() in ignoredComponents:
                 return
             for p in tuple(self.properties()):
                 xpname = p.name().startswith("X-")
@@ -1939,7 +1995,7 @@
 
         if self.name() == "VCALENDAR":
             for component in self.subcomponents():
-                if component.name() == "VTIMEZONE":
+                if component.name() in ignoredComponents:
                     continue
                 component.removePropertyParameters(property, params)
         else:
@@ -1958,7 +2014,7 @@
 
         if self.name() == "VCALENDAR":
             for component in self.subcomponents():
-                if component.name() == "VTIMEZONE":
+                if component.name() in ignoredComponents:
                     continue
                 component.removePropertyParametersByValue(property, paramvalues)
         else:
@@ -2079,7 +2135,7 @@
         
         if self.name() == "VCALENDAR":
             for component in self.subcomponents():
-                if component.name() == "VTIMEZONE":
+                if component.name() in ignoredComponents:
                     continue
                 component.normalizePropertyValueLists(propname)
         else:
@@ -2096,7 +2152,7 @@
         
         if self.name() == "VCALENDAR":
             for component in self.subcomponents():
-                if component.name() == "VTIMEZONE":
+                if component.name() in ignoredComponents:
                     continue
                 component.normalizeAttachments()
         else:
@@ -2118,7 +2174,7 @@
         @type lookupFunction: L{Function}
         """
         for component in self.subcomponents():
-            if component.name() == "VTIMEZONE":
+            if component.name() in ignoredComponents:
                 continue
             for prop in itertools.chain(
                 component.properties("ORGANIZER"),
@@ -2203,7 +2259,49 @@
                         except KeyError:
                             pass
 
+
+    def allPerUserUIDs(self):
         
+        results = set()
+        for component in self.subcomponents():
+            if component.name() == "X-CALENDARSERVER-PERUSER":
+                results.add(component.propertyValue("X-CALENDARSERVER-PERUSER-UID"))
+        return results
+
+    def perUserTransparency(self, rid):
+        
+        # We will create a cache of all user/rid/transparency values as we will likely
+        # be calling this a lot
+        if not hasattr(self, "_perUserTransparency"):
+            self._perUserTransparency = {}
+            
+            # Do per-user data
+            for component in self.subcomponents():
+                if component.name() == "X-CALENDARSERVER-PERUSER":
+                    uid = component.propertyValue("X-CALENDARSERVER-PERUSER-UID")
+                    for subcomponent in component.subcomponents():
+                        if subcomponent.name() == "X-CALENDARSERVER-PERINSTANCE":
+                            instancerid = subcomponent.propertyValue("RECURRENCE-ID")
+                            transp = subcomponent.propertyValue("TRANSP") == "TRANSPARENT"                                
+                            self._perUserTransparency.setdefault(uid, {})[instancerid] = transp
+                elif component.name() not in ignoredComponents:
+                    instancerid = component.propertyValue("RECURRENCE-ID")
+                    transp = component.propertyValue("TRANSP") == "TRANSPARENT"                    
+                    self._perUserTransparency.setdefault("", {})[instancerid] = transp
+
+        # Now lookup in cache
+        results = []
+        for uid, cachedRids in sorted(self._perUserTransparency.items(), key=lambda x:x[0]):
+            lookupRid = rid
+            if lookupRid not in cachedRids:
+                lookupRid = None
+            if lookupRid in cachedRids:
+                results.append((uid, cachedRids[lookupRid],))
+            else:
+                results.append((uid, False,))     
+        
+        return tuple(results)
+
 ##
 # Dates and date-times
 ##

Modified: CalendarServer/trunk/twistedcaldav/icaldav.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/icaldav.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/icaldav.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -94,20 +94,6 @@
             of type C{"VCALENDAR"}.
         """
 
-    def iCalendarXML(name=None):
-        """
-        Constructs a CalDAV XML element representing this resource or its child
-        with the given name.
-        The behavior of this method is not specified if it is called on a
-        resource that is not a calendar collection or a calendar resource within
-        a calendar collection.
-        @param name: the name of the desired child of this resource, or None
-            if this resource is desired.  Must be None if this resource is
-            not a calendar collection.
-        @return: a L{twistedcaldav.caldavxml.CalendarData} containing the
-            iCalendar data for the requested resource.
-        """
-
 class ICalendarPrincipalResource(IDAVResource):
     """
     CalDAV principle resource.

Modified: CalendarServer/trunk/twistedcaldav/index.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/index.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/index.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -47,10 +47,9 @@
 from twext.python.log import Logger, LoggingMixIn
 
 from twistedcaldav.ical import Component
-from twistedcaldav.query import calendarquery
+from twistedcaldav.query import calendarquery, queryfilter
 from twistedcaldav.sql import AbstractSQLDatabase
 from twistedcaldav.sql import db_prefix
-from twistedcaldav import caldavxml
 from twistedcaldav.instance import InvalidOverriddenInstanceError
 from twistedcaldav.config import config
 from twistedcaldav.memcachepool import CachePoolUserMixIn
@@ -58,7 +57,7 @@
 log = Logger()
 
 db_basename = db_prefix + "sqlite"
-schema_version = "9"
+schema_version = "10"
 collection_types = {"Calendar": "Regular Calendar Collection", "iTIP": "iTIP Calendar Collection"}
 
 icalfbtype_to_indexfbtype = {
@@ -298,7 +297,7 @@
         
         return changed, deleted,
 
-    def indexedSearch(self, filter, fbtype=False):
+    def indexedSearch(self, filter, useruid="", fbtype=False):
         """
         Finds resources matching the given qualifiers.
         @param filter: the L{Filter} for the calendar-query to execute.
@@ -310,7 +309,7 @@
 
         # Make sure we have a proper Filter element and get the partial SQL
         # statement to use.
-        if isinstance(filter, caldavxml.Filter):
+        if isinstance(filter, queryfilter.Filter):
             qualifiers = calendarquery.sqlcalendarquery(filter)
             if qualifiers is not None:
                 # Determine how far we need to extend the current expansion of
@@ -335,10 +334,25 @@
             rowiter = self._db_execute("select NAME, UID, TYPE from RESOURCE")
         else:
             if fbtype:
+                # Lookup the useruid - try the empty (default) one if needed
+                dbuseruid = self._db_value_for_sql(
+                    "select PERUSERID from PERUSER where USERUID == :1",
+                    useruid,
+                )
+                count = self._db_value_for_sql(
+                    "select COUNT(PERUSERID) from TRANSPARENCY where PERUSERID == :1",
+                    dbuseruid,
+                )
+                if dbuseruid is None or count == 0:
+                    dbuseruid = self._db_value_for_sql(
+                        "select PERUSERID from PERUSER where USERUID == :1",
+                        "",
+                    )
+                    
                 # For a free-busy time-range query we return all instances
                 rowiter = self._db_execute(
-                    "select RESOURCE.NAME, RESOURCE.UID, RESOURCE.TYPE, RESOURCE.ORGANIZER, TIMESPAN.FLOAT, TIMESPAN.START, TIMESPAN.END, TIMESPAN.FBTYPE" + 
-                    qualifiers[0],
+                    "select RESOURCE.NAME, RESOURCE.UID, RESOURCE.TYPE, RESOURCE.ORGANIZER, TIMESPAN.FLOAT, TIMESPAN.START, TIMESPAN.END, TIMESPAN.FBTYPE, TRANSPARENCY.TRANSPARENT" + 
+                    qualifiers[0] + " AND TRANSPARENCY.PERUSERID == '%s'" % (dbuseruid,),
                     *qualifiers[1]
                 )
             else:
@@ -430,6 +444,7 @@
         q.execute(
             """
             create table RESOURCE (
+                RESOURCEID     integer primary key autoincrement,
                 NAME           text unique,
                 UID            text%s,
                 TYPE           text,
@@ -455,11 +470,12 @@
         q.execute(
             """
             create table TIMESPAN (
-                NAME   text,
-                FLOAT  text(1),
-                START  date,
-                END    date,
-                FBTYPE text(1)
+                INSTANCEID   integer primary key autoincrement,
+                RESOURCEID   integer,
+                FLOAT        text(1),
+                START        date,
+                END          date,
+                FBTYPE       text(1)
             )
             """
         )
@@ -470,6 +486,41 @@
         )
 
         #
+        # PERUSER table tracks per-user ids
+        #   PERUSERID: autoincrement primary key
+        #   UID: User ID used in calendar data
+        #
+        q.execute(
+            """
+            create table PERUSER (
+                PERUSERID       integer primary key autoincrement,
+                USERUID         text
+            )
+            """
+        )
+        q.execute(
+            """
+            create index PERUSER_UID on PERUSER (USERUID)
+            """
+        )
+
+        #
+        # TRANSPARENCY table tracks per-user per-instance transparency
+        #   PERUSERID: user id key
+        #   INSTANCEID: instance id key
+        #   TRANSPARENT: Y if transparent, N if opaque
+        #
+        q.execute(
+            """
+            create table TRANSPARENCY (
+                PERUSERID       integer,
+                INSTANCEID      integer,
+                TRANSPARENT     text(1)
+            )
+            """
+        )
+
+        #
         # REVISIONS table tracks changes
         #   NAME: Last URI component (eg. <uid>.ics, RESOURCE primary key)
         #   REVISION: revision number
@@ -506,42 +557,42 @@
                 """
             )
 
+        # Cascading triggers to help on delete
+        q.execute(
+            """
+            create trigger resourceDelete after delete on RESOURCE
+            for each row
+            begin
+                delete from TIMESPAN where TIMESPAN.RESOURCEID = OLD.RESOURCEID;
+            end
+            """
+        )
+        q.execute(
+            """
+            create trigger timespanDelete after delete on TIMESPAN
+            for each row
+            begin
+                delete from TRANSPARENCY where INSTANCEID = OLD.INSTANCEID;
+            end
+            """
+        )
+        
     def _db_can_upgrade(self, old_version):
         """
         Can we do an in-place upgrade
         """
         
-        # Previous versions can be upgraded as per _db_upgrade_data_tables
-        return True
+        # v10 is a big change - no upgrade possible
+        return False
 
     def _db_upgrade_data_tables(self, q, old_version):
         """
         Upgrade the data from an older version of the DB.
         """
 
-        # When going to version 7+ all we need to do is add a column to the resource and timespan
-        if old_version < "7":
-            q.execute("alter table RESOURCE add column ORGANIZER text default '?'")
-            q.execute("alter table TIMESPAN add column FBTYPE text(1) default '?'")
+        # v10 is a big change - no upgrade possible
+        pass
 
-        # When going to version 8+ all we need to do is add an index
-        if old_version < "8":
-            q.execute("create index STARTENDFLOAT on TIMESPAN (START, END, FLOAT)")
-
-        # When going to version 9+ all we need to do is add revision table and index
-        if old_version < "9":
-            q.execute(
-                """
-                create table REVISIONS (
-                    NAME            text unique,
-                    REVISION        integer,
-                    CREATEDREVISION integer,
-                    WASDELETED      text(1)
-                )
-                """
-            )
-            q.execute("create index REVISION on REVISIONS (REVISION)")
-
     def notExpandedBeyond(self, minDate):
         """
         Gives all resources which have not been expanded beyond a given date
@@ -598,6 +649,35 @@
 
         self._delete_from_db(name, uid, None)
 
+        # Add RESOURCE item
+        self._db_execute(
+            """
+            insert into RESOURCE (NAME, UID, TYPE, RECURRANCE_MAX, ORGANIZER)
+            values (:1, :2, :3, :4, :5)
+            """, name, uid, calendar.resourceType(), instances.limit, organizer
+        )
+        resourceid = self.lastrowid
+
+        # Get a set of all referenced per-user UIDs and map those to entries already
+        # in the DB and add new ones as needed
+        useruids = calendar.allPerUserUIDs()
+        useruids.add("")
+        useruidmap = {}
+        for useruid in useruids:
+            peruserid = self._db_value_for_sql(
+                "select PERUSERID from PERUSER where USERUID = :1",
+                useruid
+            )
+            if peruserid is None:
+                self._db_execute(
+                    """
+                    insert into PERUSER (USERUID)
+                    values (:1)
+                    """, useruid
+                )
+                peruserid = self.lastrowid
+            useruidmap[useruid] = peruserid
+            
         for key in instances:
             instance = instances[key]
             start = instance.start.replace(tzinfo=utc)
@@ -605,10 +685,21 @@
             float = 'Y' if instance.start.tzinfo is None else 'N'
             self._db_execute(
                 """
-                insert into TIMESPAN (NAME, FLOAT, START, END, FBTYPE)
+                insert into TIMESPAN (RESOURCEID, FLOAT, START, END, FBTYPE)
                 values (:1, :2, :3, :4, :5)
-                """, name, float, start, end, icalfbtype_to_indexfbtype.get(instance.component.getFBType(), 'F')
+                """, resourceid, float, start, end, icalfbtype_to_indexfbtype.get(instance.component.getFBType(), 'F')
             )
+            instanceid = self.lastrowid
+            peruserdata = calendar.perUserTransparency(instance.rid)
+            for useruid, transp in peruserdata:
+                peruserid = useruidmap[useruid]
+                self._db_execute(
+                    """
+                    insert into TRANSPARENCY (PERUSERID, INSTANCEID, TRANSPARENT)
+                    values (:1, :2, :3)
+                    """, peruserid, instanceid, 'T' if transp else 'F'
+                )
+                    
 
         # Special - for unbounded recurrence we insert a value for "infinity"
         # that will allow an open-ended time-range to always match it.
@@ -618,18 +709,21 @@
             float = 'N'
             self._db_execute(
                 """
-                insert into TIMESPAN (NAME, FLOAT, START, END, FBTYPE)
+                insert into TIMESPAN (RESOURCEID, FLOAT, START, END, FBTYPE)
                 values (:1, :2, :3, :4, :5)
-                """, name, float, start, end, '?'
+                """, resourceid, float, start, end, '?'
             )
+            instanceid = self.lastrowid
+            peruserdata = calendar.perUserTransparency(None)
+            for useruid, transp in peruserdata:
+                peruserid = useruidmap[useruid]
+                self._db_execute(
+                    """
+                    insert into TRANSPARENCY (PERUSERID, INSTANCEID, TRANSPARENT)
+                    values (:1, :2, :3)
+                    """, peruserid, instanceid, 'T' if transp else 'F'
+                )
             
-        self._db_execute(
-            """
-            insert into RESOURCE (NAME, UID, TYPE, RECURRANCE_MAX, ORGANIZER)
-            values (:1, :2, :3, :4, :5)
-            """, name, uid, calendar.resourceType(), instances.limit, organizer
-        )
-
         if revision is not None:
             created = self._db_value_for_sql("select CREATEDREVISION from REVISIONS where NAME = :1", name)
             if created is None:
@@ -647,7 +741,6 @@
         @param name: the name of the resource to delete.
         @param uid: the uid of the resource to delete.
         """
-        self._db_execute("delete from TIMESPAN where NAME = :1", name)
         self._db_execute("delete from RESOURCE where NAME = :1", name)
         if revision is not None:
             self._db_execute(

Modified: CalendarServer/trunk/twistedcaldav/instance.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/instance.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/instance.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -326,7 +326,7 @@
 
         # Make sure override RECURRENCE-ID is a valid instance of the master
         if got_master:
-            if str(rid) not in self.instances and dateordatetime(rid) <= limit:
+            if str(rid) not in self.instances and dateordatetime(rid) < limit:
                 if self.ignoreInvalidInstances:
                     return
                 else:

Modified: CalendarServer/trunk/twistedcaldav/mail.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/mail.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/mail.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -218,8 +218,8 @@
 
         return succeed(self.iMIPACL)
 
-    def resourceType(self):
-        return davxml.ResourceType.ischeduleinbox
+    def resourceType(self, request):
+        return succeed(davxml.ResourceType.ischeduleinbox)
 
     def isCollection(self):
         return False

Modified: CalendarServer/trunk/twistedcaldav/memcacheprops.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/memcacheprops.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/memcacheprops.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -76,7 +76,7 @@
         #    "/path/to/resource/file":
         #      (
         #        {
-        #          (namespace, name): property,
+        #          (namespace, name, uid): property,
         #          ...,
         #        },
         #        memcache_token,
@@ -192,8 +192,8 @@
 
             propertyStore = child.deadProperties()
             props = {}
-            for qname in propertyStore.list(cache=False):
-                props[qname] = propertyStore.get(qname, cache=False)
+            for pnamespace, pname, puid in propertyStore.list(filterByUID=False, cache=False):
+                props[(pnamespace, pname, puid,)] = propertyStore.get((pnamespace, pname,), uid=puid, cache=False)
 
             cache[child.fp.path] = props
 
@@ -201,16 +201,18 @@
 
         return cache
 
-    def setProperty(self, child, property, delete=False):
+    def setProperty(self, child, property, uid, delete=False):
         propertyCache, key, childCache, token = self.childCache(child)
 
         if delete:
             qname = property
-            if childCache.has_key(qname):
-                del childCache[qname]
+            qnameuid = qname + (uid,)
+            if childCache.has_key(qnameuid):
+                del childCache[qnameuid]
         else:
             qname = property.qname()
-            childCache[qname] = property
+            qnameuid = qname + (uid,)
+            childCache[qnameuid] = property
 
         client = self.memcacheClient()
 
@@ -238,20 +240,24 @@
                 propertyCache, key, childCache, token = self.childCache(child)
 
                 if delete:
-                    if childCache.has_key(qname):
-                        del childCache[qname]
+                    if childCache.has_key(qnameuid):
+                        del childCache[qnameuid]
                 else:
-                    childCache[qname] = property
+                    childCache[qnameuid] = property
 
             else:
                 log.error("memcacheprops setProperty had too many failures")
                 delattr(self, "_propertyCache")
-                raise MemcacheError("Unable to %s property {%s}%s on %s"
-                    % ("delete" if delete else "set",
-                    qname[0], qname[1], child))
+                raise MemcacheError("Unable to %s property %s{%s}%s on %s" % (
+                    "delete" if delete else "set",
+                    uid if uid else "",
+                    qname[0],
+                    qname[1],
+                    child
+                ))
 
-    def deleteProperty(self, child, qname):
-        return self.setProperty(child, qname, delete=True)
+    def deleteProperty(self, child, qname, uid):
+        return self.setProperty(child, qname, uid, delete=True)
 
     def flushCache(self, child):
         path = child.fp.path
@@ -285,49 +291,72 @@
         def flushCache(self):
             self.parentPropertyCollection.flushCache(self.child)
 
-        def get(self, qname, cache=True):
+        def get(self, qname, uid=None, cache=True):
             if cache:
                 propertyCache = self.propertyCache()
-                if qname in propertyCache:
-                    return propertyCache[qname]
+                qnameuid = qname + (uid,)
+                if qnameuid in propertyCache:
+                    return propertyCache[qnameuid]
                 else:
                     raise HTTPError(StatusResponse(
                         responsecode.NOT_FOUND,
-                        "No such property: {%s}%s" % qname
+                        "No such property: %s{%s}%s" % (uid if uid else "", qname[0], qname[1],)
                     ))
 
-            self.log_debug("Read for %s on %s"
-                           % (qname, self.childPropertyStore.resource.fp.path))
-            return self.childPropertyStore.get(qname)
+            self.log_debug("Read for %s%s on %s" % (
+                ("{%s}:" % (uid,)) if uid else "",
+                qname,
+                self.childPropertyStore.resource.fp.path
+            ))
+            return self.childPropertyStore.get(qname, uid=uid)
 
-        def set(self, property):
-            self.log_debug("Write for %s on %s"
-                           % (property.qname(), self.childPropertyStore.resource.fp.path))
+        def set(self, property, uid=None):
+            self.log_debug("Write for %s%s on %s" % (
+                ("{%s}:" % (uid,)) if uid else "",
+                property.qname(),
+                self.childPropertyStore.resource.fp.path
+            ))
 
-            self.parentPropertyCollection.setProperty(self.child, property)
-            self.childPropertyStore.set(property)
+            self.parentPropertyCollection.setProperty(self.child, property, uid)
+            self.childPropertyStore.set(property, uid=uid)
 
-        def delete(self, qname):
-            self.log_debug("Delete for %s on %s"
-                           % (qname, self.childPropertyStore.resource.fp.path))
+        def delete(self, qname, uid=None):
+            self.log_debug("Delete for %s%s on %s" % (
+                ("{%s}:" % (uid,)) if uid else "",
+                qname,
+                self.childPropertyStore.resource.fp.path
+            ))
 
-            self.parentPropertyCollection.deleteProperty(self.child, qname)
-            self.childPropertyStore.delete(qname)
+            self.parentPropertyCollection.deleteProperty(self.child, qname, uid)
+            self.childPropertyStore.delete(qname, uid=uid)
 
-        def contains(self, qname, cache=True):
+        def contains(self, qname, uid=None, cache=True):
             if cache:
                 propertyCache = self.propertyCache()
-                return qname in propertyCache
+                qnameuid = qname + (uid,)
+                return qnameuid in propertyCache
 
-            self.log_debug("Contains for %s"
-                           % (self.childPropertyStore.resource.fp.path,))
-            return self.childPropertyStore.contains(qname)
+            self.log_debug("Contains for %s%s on %s" % (
+                ("{%s}:" % (uid,)) if uid else "",
+                qname,
+                self.childPropertyStore.resource.fp.path,
+            ))
+            return self.childPropertyStore.contains(qname, uid=uid)
 
-        def list(self, cache=True):
+        def list(self, uid=None, filterByUID=True, cache=True):
             if cache:
                 propertyCache = self.propertyCache()
-                return propertyCache.iterkeys()
+                results = propertyCache.keys()
+                if filterByUID:
+                    return [ 
+                        (namespace, name)
+                        for namespace, name, propuid in results
+                        if propuid == uid
+                    ]
+                else:
+                    return results
+                
 
             self.log_debug("List for %s"
                            % (self.childPropertyStore.resource.fp.path,))
-            return self.childPropertyStore.list()
+            return self.childPropertyStore.list(uid=uid, filterByUID=filterByUID)

Modified: CalendarServer/trunk/twistedcaldav/method/delete_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/delete_common.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/method/delete_common.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -143,7 +143,7 @@
         lock = None
         if not self.internal_request and self.allowImplicitSchedule:
             # Get data we need for implicit scheduling
-            calendar = delresource.iCalendar()
+            calendar = (yield delresource.iCalendarForUser(self.request))
             scheduler = ImplicitScheduler()
             do_implicit_action, _ignore = (yield scheduler.testImplicitSchedulingDELETE(self.request, delresource, calendar))
             if do_implicit_action:
@@ -214,6 +214,18 @@
                 errors.add(childurl, responsecode.BAD_REQUEST)
 
         # Now do normal delete
+
+        # Check virtual share first
+        isVirtual = yield delresource.isVirtualShare(self.request)
+        if isVirtual:
+            yield delresource.removeVirtualShare(self.request)
+            returnValue(responsecode.NO_CONTENT)
+
+        # Handle sharing
+        wasShared = (yield delresource.isShared(self.request))
+        if wasShared:
+            yield delresource.downgradeFromShare(self.request)
+
         # Change CTag
         yield delresource.bumpSyncToken()
         more_responses = (yield self.deleteResource(delresource, deluri, parent))
@@ -348,6 +360,12 @@
                 errors.add(childurl, responsecode.BAD_REQUEST)
 
         # Now do normal delete
+
+        # Handle sharing
+        wasShared = (yield delresource.isShared(self.request))
+        if wasShared:
+            yield delresource.downgradeFromShare(self.request)
+
         yield delresource.updateCTag()
         more_responses = (yield self.deleteResource(delresource, deluri, parent))
         

Modified: CalendarServer/trunk/twistedcaldav/method/get.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/get.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/method/get.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -22,52 +22,56 @@
 
 from twisted.internet.defer import inlineCallbacks, returnValue
 from twext.web2.dav import davxml
+from twext.web2.dav.util import parentForURL
 from twext.web2.http import HTTPError
 from twext.web2.http import Response
 from twext.web2.http_headers import MimeType
 from twext.web2.stream import MemoryStream
 
-from twistedcaldav import caldavxml
 from twistedcaldav.caldavxml import ScheduleTag
 from twistedcaldav.customxml import TwistedCalendarAccessProperty
-from twistedcaldav.ical import Component
+from twistedcaldav.datafilters.privateevents import PrivateEventFilter
+from twistedcaldav.resource import isPseudoCalendarCollectionResource
 
 @inlineCallbacks
 def http_GET(self, request):
 
     # Look for calendar access restriction on existing resource.
     if self.exists():
-        try:
-            access = self.readDeadProperty(TwistedCalendarAccessProperty)
-        except HTTPError:
-            access = None
-            
-        if access in (Component.ACCESS_CONFIDENTIAL, Component.ACCESS_RESTRICTED):
+        parentURL = parentForURL(request.uri)
+        parent = (yield request.locateResource(parentURL))
+        if isPseudoCalendarCollectionResource(parent):
     
             # Check authorization first
             yield self.authorize(request, (davxml.Read(),))
 
-            # Non DAV:owner's have limited access to the data
-            isowner = (yield self.isOwner(request, adminprincipals=True, readprincipals=True))
-            
-            if not isowner:
-                # Now "filter" the resource calendar data through the CALDAV:calendar-data element and apply
-                # access restrictions to the data.
-                caldata = caldavxml.CalendarData().elementFromResourceWithAccessRestrictions(self, access).calendarData()
+            caldata = (yield self.iCalendarForUser(request))
 
-                response = Response()
-                response.stream = MemoryStream(caldata)
-                response.headers.setHeader("content-type", MimeType.fromString("text/calendar; charset=utf-8"))
-                returnValue(response)
+            try:
+                access = self.readDeadProperty(TwistedCalendarAccessProperty)
+            except HTTPError:
+                access = None
+                
+            if access:
+        
+                # Non DAV:owner's have limited access to the data
+                isowner = (yield self.isOwner(request, adminprincipals=True, readprincipals=True))
+                
+                # Now "filter" the resource calendar data
+                caldata = PrivateEventFilter(access, isowner).filter(caldata)
+    
+            response = Response()
+            response.stream = MemoryStream(str(caldata))
+            response.headers.setHeader("content-type", MimeType.fromString("text/calendar; charset=utf-8"))
+    
+            # Add Schedule-Tag header if property is present
+            if self.hasDeadProperty(ScheduleTag):
+                scheduletag = self.readDeadProperty(ScheduleTag)
+                if scheduletag:
+                    response.headers.setHeader("Schedule-Tag", str(scheduletag))
+        
+            returnValue(response)
 
-
     # Do normal GET behavior
     response = (yield super(CalDAVFile, self).http_GET(request))
-    
-    # Add Schedule-Tag header if property is present
-    if self.exists() and self.hasDeadProperty(ScheduleTag):
-        scheduletag = self.readDeadProperty(ScheduleTag)
-        if scheduletag:
-            response.headers.setHeader("Schedule-Tag", str(scheduletag))
-
     returnValue(response)

Modified: CalendarServer/trunk/twistedcaldav/method/mkcol.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/mkcol.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/method/mkcol.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -165,11 +165,10 @@
             # Now handle other properties
             for property in mkcol.children[0].children[0].children:
                 try:
-                    if rtype == "calendar":
-                        if property.qname() == (caldavxml.caldav_namespace, "supported-calendar-component-set"):
-                            self.writeDeadProperty(property)
-                        elif not isinstance(property, davxml.ResourceType):
-                            yield self.writeProperty(property, request)
+                    if rtype == "calendar" and property.qname() == (caldavxml.caldav_namespace, "supported-calendar-component-set"):
+                        self.writeDeadProperty(property)
+                    else:
+                        yield self.writeProperty(property, request)
                 except HTTPError:
                     errors.add(Failure(), property)
                     got_an_error = True

Modified: CalendarServer/trunk/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put_common.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/method/put_common.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -55,6 +55,7 @@
     TwistedCalendarHasPrivateCommentsProperty, TwistedSchedulingObjectResource,\
     TwistedScheduleMatchETags
 from twistedcaldav.customxml import TwistedCalendarAccessProperty
+from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
 from twistedcaldav.fileops import copyToWithXAttrs, copyXAttrs
 from twistedcaldav.fileops import putWithXAttrs
 from twistedcaldav.fileops import copyWithXAttrs
@@ -315,7 +316,11 @@
                         raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
                 
                     # At this point we need the calendar data to do more tests
-                    self.calendar = self.source.iCalendar()
+                    try:
+                        self.calendar = (yield self.source.iCalendarForUser(self.request))
+                    except ValueError, e:
+                        log.err(str(e))
+                        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data"), description="Can't parse calendar data"))
                 else:
                     try:
                         if type(self.calendar) in (types.StringType, types.UnicodeType,):
@@ -364,7 +369,7 @@
 
                 # FIXME: We need this here because we have to re-index the destination. Ideally it
                 # would be better to copy the index entries from the source and add to the destination.
-                self.calendar = self.source.iCalendar()
+                self.calendar = (yield self.source.iCalendarForUser(self.request))
 
             # Check access
             if self.destinationcal and config.EnablePrivateEvents:
@@ -375,7 +380,7 @@
 
         elif self.sourcecal:
             self.source_index = self.sourceparent.index()
-            self.calendar = self.source.iCalendar()
+            self.calendar = (yield self.source.iCalendarForUser(self.request))
     
     @inlineCallbacks
     def validCopyMoveOperation(self):
@@ -692,6 +697,7 @@
         else:
             return False
 
+    @inlineCallbacks
     def preservePrivateComments(self):
         # Check for private comments on the old resource and the new resource and re-insert
         # ones that are lost.
@@ -709,14 +715,14 @@
             if old_has_private_comments and not new_has_private_comments:
                 # Transfer old comments to new calendar
                 log.debug("Private Comments properties were entirely removed by the client. Restoring existing properties.")
-                old_calendar = self.destination.iCalendar()
+                old_calendar = (yield self.destination.iCalendarForUser(self.request))
                 self.calendar.transferProperties(old_calendar, (
                     "X-CALENDARSERVER-PRIVATE-COMMENT",
                     "X-CALENDARSERVER-ATTENDEE-COMMENT",
                 ))
                 self.calendardata = None
         
-        return new_has_private_comments
+        returnValue(new_has_private_comments)
 
     @inlineCallbacks
     def doImplicitScheduling(self):
@@ -789,7 +795,26 @@
         returnValue((is_scheduling_resource, data_changed, did_implicit_action,))
 
     @inlineCallbacks
+    def mergePerUserData(self):
+        
+        if self.calendar:
+            accessUID = (yield self.destination.resourceOwnerPrincipal(self.request))
+            accessUID = accessUID.principalUID() if accessUID else ""
+            oldCal = self.destination.iCalendar() if self.destination.exists() and self.destinationcal else None
+            
+            # Duplicate before we do the merge because someone else may "own" the calendar object
+            # and we should not change it. This is not ideal as we may duplicate it unnecessarily
+            # but we currently have no api to let the caller tell us whether it cares about the
+            # whether the calendar data is changed or not.
+            self.calendar = PerUserDataFilter(accessUID).merge(self.calendar.duplicate(), oldCal)
+            self.calendardata = None
+            
+    @inlineCallbacks
     def doStore(self, implicit):
+
+        # Always do the per-user data merge right before we store
+        yield self.mergePerUserData()
+
         # Do put or copy based on whether source exists
         if self.source is not None:
             if implicit:
@@ -966,7 +991,7 @@
             rruleChanged = self.truncateRecurrence()
 
             # Preserve private comments
-            new_has_private_comments = self.preservePrivateComments()
+            new_has_private_comments = (yield self.preservePrivateComments())
     
             # Do scheduling
             implicit_result = (yield self.doImplicitScheduling())

Modified: CalendarServer/trunk/twistedcaldav/method/report_calquery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_calquery.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/method/report_calquery.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -40,6 +40,7 @@
 from twistedcaldav.index import IndexedSearchException
 from twistedcaldav.instance import TooManyInstancesError
 from twistedcaldav.method import report_common
+from twistedcaldav.query import queryfilter
 
 log = Logger()
 
@@ -64,10 +65,11 @@
 
     responses = []
 
-    filter = calendar_query.filter
-    query  = calendar_query.query
+    xmlfilter = calendar_query.filter
+    filter = queryfilter.Filter(xmlfilter)
+    props  = calendar_query.props
 
-    assert query is not None
+    assert props is not None
     
     # Get the original timezone provided in the query, if any, and validate it now
     query_timezone = None
@@ -80,19 +82,19 @@
         filter.settimezone(query_tz)
         query_timezone = tuple(calendar_query.timezone.calendar().subcomponents())[0]
 
-    if query.qname() == ("DAV:", "allprop"):
+    if props.qname() == ("DAV:", "allprop"):
         propertiesForResource = report_common.allPropertiesForResource
         generate_calendar_data = False
 
-    elif query.qname() == ("DAV:", "propname"):
+    elif props.qname() == ("DAV:", "propname"):
         propertiesForResource = report_common.propertyNamesForResource
         generate_calendar_data = False
 
-    elif query.qname() == ("DAV:", "prop"):
+    elif props.qname() == ("DAV:", "prop"):
         propertiesForResource = report_common.propertyListForResource
         
         # Verify that any calendar-data element matches what we can handle
-        result, message, generate_calendar_data = report_common.validPropertyListCalendarDataTypeVersion(query)
+        result, message, generate_calendar_data = report_common.validPropertyListCalendarDataTypeVersion(props)
         if not result:
             log.err(message)
             raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
@@ -102,7 +104,7 @@
 
     # Verify that the filter element is valid
     if (filter is None) or not filter.valid():
-        log.err("Invalid filter element: %r" % (filter,))
+        log.err("Invalid filter element: %r" % (xmlfilter,))
         raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-filter")))
 
     matchcount = [0]
@@ -145,7 +147,7 @@
                 else:
                     href = davxml.HRef.fromString(uri)
             
-                return report_common.responseForHref(request, responses, href, resource, calendar, timezone, propertiesForResource, query, isowner)
+                return report_common.responseForHref(request, responses, href, resource, calendar, timezone, propertiesForResource, props, isowner)
             else:
                 return succeed(None)
     
@@ -200,7 +202,7 @@
                     child_path_name = urllib.unquote(child_uri_name)
                     
                     if generate_calendar_data or not index_query_ok:
-                        calendar = calresource.iCalendar(child_path_name)
+                        calendar = (yield calresource.iCalendarForUser(request, child_path_name))
                         assert calendar is not None, "Calendar %s is missing from calendar collection %r" % (child_uri_name, self)
                     else:
                         calendar = None
@@ -224,7 +226,7 @@
             # Check private events access status
             isowner = (yield calresource.isOwner(request, adminprincipals=True, readprincipals=True))
 
-            calendar = calresource.iCalendar()
+            calendar = (yield calresource.iCalendarForUser(request))
             yield queryCalendarObjectResource(calresource, uri, None, calendar, timezone)
 
         returnValue(True)

Modified: CalendarServer/trunk/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_common.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/method/report_common.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -55,12 +55,15 @@
 
 from twistedcaldav import caldavxml
 from twistedcaldav import carddavxml
-from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.caldavxml import caldav_namespace, CalendarData
 from twistedcaldav.customxml import TwistedCalendarAccessProperty
+from twistedcaldav.datafilters.calendardata import CalendarDataFilter
+from twistedcaldav.datafilters.privateevents import PrivateEventFilter
 from twistedcaldav.dateops import clipPeriod, normalizePeriodList, timeRangesOverlap
 from twistedcaldav.ical import Component, Property, iCalendarProductID
 from twistedcaldav.instance import InstanceList
 from twistedcaldav.index import IndexedSearchException
+from twistedcaldav.query import queryfilter
 
 log = Logger()
 
@@ -335,20 +338,16 @@
     for property in props:
         if isinstance(property, caldavxml.CalendarData):
             # Handle private events access restrictions
-            if not isowner:
-                try:
-                    access = resource.readDeadProperty(TwistedCalendarAccessProperty)
-                except HTTPError:
-                    access = None
-            else:
+            try:
+                access = resource.readDeadProperty(TwistedCalendarAccessProperty)
+            except HTTPError:
                 access = None
 
-            if calendar:
-                propvalue = property.elementFromCalendarWithAccessRestrictions(calendar, access, timezone)
-            else:
-                propvalue = property.elementFromResourceWithAccessRestrictions(resource, access, timezone)
-            if propvalue is None:
-                raise ValueError("Invalid CalDAV:calendar-data for request: %r" % (property,))
+            if calendar is None:
+                calendar = (yield resource.iCalendarForUser(request))
+            filtered = PrivateEventFilter(access, isowner).filter(calendar)
+            filtered = CalendarDataFilter(property, timezone).filter(filtered)
+            propvalue = CalendarData().fromCalendar(filtered)
             properties_by_status[responsecode.OK].append(propvalue)
             continue
     
@@ -438,6 +437,7 @@
                       name="VCALENDAR",
                    )
               )
+    filter = queryfilter.Filter(filter)
 
     # Get the timezone property from the collection, and store in the query filter
     # for use during the query itself.
@@ -453,13 +453,17 @@
     filteredaces = (yield calresource.inheritedACEsforChildren(request))
 
     try:
-        resources = calresource.index().indexedSearch(filter, fbtype=True)
+        useruid = (yield calresource.resourceOwnerPrincipal(request))
+        useruid = useruid.principalUID() if useruid else ""
+        resources = calresource.index().indexedSearch(filter, useruid=useruid, fbtype=True)
     except IndexedSearchException:
         resources = calresource.index().bruteForceSearch()
 
     # We care about separate instances for VEVENTs only
     aggregated_resources = {}
-    for name, uid, type, test_organizer, float, start, end, fbtype in resources:
+    for name, uid, type, test_organizer, float, start, end, fbtype, transp in resources:
+        if transp == 'T' and fbtype != '?':
+            fbtype = 'F'
         aggregated_resources.setdefault((name, uid, type, test_organizer,), []).append((float, start, end, fbtype,))
 
     for key in aggregated_resources.iterkeys():
@@ -528,7 +532,7 @@
                     raise NumberOfMatchesWithinLimits(max_number_of_matches)
                 
         else:
-            calendar = calresource.iCalendar(name)
+            calendar = (yield calresource.iCalendarForUser(request, name))
             
             # The calendar may come back as None if the resource is being changed, or was deleted
             # between our initial index query and getting here. For now we will ignore this error, but in

Copied: CalendarServer/trunk/twistedcaldav/notifications.py (from rev 5440, CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/notifications.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/notifications.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/notifications.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -0,0 +1,218 @@
+##
+# Copyright (c) 2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Implements notification functionality.
+"""
+
+__all__ = [
+    "NotificationResource",
+    "NotificationCollectionResource",
+]
+
+from twext.python.log import Logger, LoggingMixIn
+from twext.web2 import responsecode
+from twext.web2.dav import davxml
+from twext.web2.dav.resource import DAVResource
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue
+from twistedcaldav.sql import AbstractSQLDatabase, db_prefix
+import os
+import types
+
+log = Logger()
+
+class NotificationResource(DAVResource):
+    """
+    An xml resource in a Notification collection.
+    """
+    def __init__(self, parent):
+        self._parent = parent
+        DAVResource.__init__(self)
+
+    def principalCollections(self):
+        return self._parent.principalCollections()
+
+    def isCollection(self):
+        return False
+
+    def resourceName(self):
+        raise NotImplementedError
+        
+    def http_PUT(self, request):
+        return responsecode.FORBIDDEN
+
+    @inlineCallbacks
+    def http_DELETE(self, request):
+        
+        response = (yield super(NotificationResource, self).http_DELETE(request))
+        if response == responsecode.NO_CONTENT:
+            yield self._parent.removedNotifictionMessage(request, self.resourceName())
+        returnValue(response)
+
+class NotificationCollectionResource(DAVResource):
+    
+    def notificationsDB(self):
+        
+        if not hasattr(self, "_notificationsDB"):
+            self._notificationsDB = NotificationsDatabase(self)
+        return self._notificationsDB
+
+    def isCollection(self):
+        return True
+
+    def resourceType(self, request):
+        return succeed(davxml.ResourceType.notification)
+
+    @inlineCallbacks
+    def addNotification(self, request, uid, xmltype, xmldata):
+        
+        # Write data to file
+        rname = uid + ".xml"
+        yield self._writeNotification(request, uid, rname, xmltype, xmldata)
+
+        # Update database
+        self.notificationsDB().addOrUpdateRecord(NotificationRecord(uid, rname, xmltype.name))
+
+    def _writeNotification(self, request, uid, rname, xmltype, xmldata):
+        raise NotImplementedError
+
+    def getNotifictionMessages(self, request, componentType=None, returnLatestVersion=True):
+        return succeed([])
+
+    def getNotifictionMessageByUID(self, request, uid):
+        return succeed(self.notificationsDB().recordForUID(uid))
+
+    @inlineCallbacks
+    def deleteNotifictionMessageByUID(self, request, uid):
+        
+        # See if it exists and delete the resource
+        record = self.notificationsDB().recordForUID(uid)
+        if record:
+            yield self._deleteNotification(request, record.name)
+            self.notificationsDB().removeRecordForUID(record.uid)
+
+    def deleteNotifictionMessageByName(self, request, rname):
+
+        # See if it exists and delete the resource
+        record = self.notificationsDB().recordForName(rname)
+        if record:
+            self._deleteNotification(request, record.name)
+            self.notificationsDB().removeRecordForUID(record.uid)
+        
+        return succeed(None)
+
+    def removedNotifictionMessage(self, request, rname):
+        self.notificationsDB().removeRecordForName(rname)
+        return succeed(None)
+        
+class NotificationRecord(object):
+    
+    def __init__(self, uid, name, xmltype):
+        self.uid = uid
+        self.name = name
+        self.xmltype = xmltype
+
+class NotificationsDatabase(AbstractSQLDatabase, LoggingMixIn):
+    
+    db_basename = db_prefix + "notifications"
+    schema_version = "1"
+    db_type = "notifications"
+
+    def __init__(self, resource):
+        """
+        @param resource: the L{twistedcaldav.static.CalDAVFile} resource for
+            the notifications collection.)
+        """
+        self.resource = resource
+        db_filename = os.path.join(self.resource.fp.path, NotificationsDatabase.db_basename)
+        super(NotificationsDatabase, self).__init__(db_filename, True, autocommit=True)
+
+    def allRecords(self):
+        
+        records = self._db_execute("select * from NOTIFICATIONS")
+        return [self._makeRecord(row) for row in (records if records is not None else ())]
+    
+    def recordForUID(self, uid):
+        
+        row = self._db_execute("select * from NOTIFICATIONS where UID = :1", uid)
+        return self._makeRecord(row[0]) if row else None
+    
+    def addOrUpdateRecord(self, record):
+
+        self._db_execute("""insert or replace into NOTIFICATIONS (UID, NAME, TYPE)
+            values (:1, :2, :3)
+            """, record.uid, record.name, record.xmltype,
+        )
+    
+    def removeRecordForUID(self, uid):
+
+        self._db_execute("delete from NOTIFICATIONS where UID = :1", uid)
+    
+    def removeRecordForName(self, rname):
+
+        self._db_execute("delete from NOTIFICATIONS where NAME = :1", rname)
+    
+    def _db_version(self):
+        """
+        @return: the schema version assigned to this index.
+        """
+        return NotificationsDatabase.schema_version
+
+    def _db_type(self):
+        """
+        @return: the collection type assigned to this index.
+        """
+        return NotificationsDatabase.db_type
+
+    def _db_init_data_tables(self, q):
+        """
+        Initialise the underlying database tables.
+        @param q:           a database cursor to use.
+        """
+        #
+        # NOTIFICATIONS table is the primary table
+        #   UID: UID for this notification
+        #   NAME: child resource name
+        #   TYPE: type of notification
+        #
+        q.execute(
+            """
+            create table NOTIFICATIONS (
+                UID            text unique,
+                NAME           text unique,
+                TYPE           text
+            )
+            """
+        )
+
+        q.execute(
+            """
+            create index UID on NOTIFICATIONS (UID)
+            """
+        )
+
+    def _db_upgrade_data_tables(self, q, old_version):
+        """
+        Upgrade the data from an older version of the DB.
+        """
+
+        # Nothing to do as we have not changed the schema
+        pass
+
+    def _makeRecord(self, row):
+        
+        return NotificationRecord(*[str(item) if type(item) == types.UnicodeType else item for item in row])
+

Modified: CalendarServer/trunk/twistedcaldav/query/calendarquery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/query/calendarquery.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/query/calendarquery.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -27,9 +27,7 @@
 ]
 
 from twistedcaldav.dateops import floatoffset
-from twistedcaldav.query import sqlgenerator
-from twistedcaldav.query import expression
-from twistedcaldav import caldavxml
+from twistedcaldav.query import expression, sqlgenerator, queryfilter
 
 # SQL Index column (field) names
 
@@ -51,17 +49,17 @@
     # Lets assume we have a valid filter from the outset.
     
     # Top-level filter contains exactly one comp-filter element
-    assert len(filter.children) == 1
-    vcalfilter = filter.children[0]
-    assert isinstance(vcalfilter, caldavxml.ComponentFilter)
+    assert filter.child is not None
+    vcalfilter = filter.child
+    assert isinstance(vcalfilter, queryfilter.ComponentFilter)
     assert vcalfilter.filter_name == "VCALENDAR"
     
-    if len(vcalfilter.children) > 0:
+    if len(vcalfilter.filters) > 0:
         # Only comp-filters are handled
-        for _ignore in [x for x in vcalfilter.children if not isinstance(x, caldavxml.ComponentFilter)]:
+        for _ignore in [x for x in vcalfilter.filters if not isinstance(x, queryfilter.ComponentFilter)]:
             raise ValueError
         
-        return compfilterListExpression(vcalfilter.children)
+        return compfilterListExpression(vcalfilter.filters)
     else:
         return expression.allExpression()
 
@@ -98,13 +96,13 @@
         expressions.append(expression.inExpression(FIELD_TYPE, compfilter.filter_name, True))
     
     # Handle time-range    
-    if compfilter.qualifier and isinstance(compfilter.qualifier, caldavxml.TimeRange):
+    if compfilter.qualifier and isinstance(compfilter.qualifier, queryfilter.TimeRange):
         start, end, startfloat, endfloat = getTimerangeArguments(compfilter.qualifier)
         expressions.append(expression.timerangeExpression(start, end, startfloat, endfloat))
         
     # Handle properties - we can only do UID right now
     props = []
-    for p in [x for x in compfilter.filters if isinstance(x, caldavxml.PropertyFilter)]:
+    for p in [x for x in compfilter.filters if isinstance(x, queryfilter.PropertyFilter)]:
         props.append(propfilterExpression(p))
     if len(props) > 1:
         propsExpression = expression.orExpression[props]
@@ -115,7 +113,7 @@
         
     # Handle embedded components - we do not right now as our Index does not handle them
     comps = []
-    for _ignore in [x for x in compfilter.filters if isinstance(x, caldavxml.ComponentFilter)]:
+    for _ignore in [x for x in compfilter.filters if isinstance(x, queryfilter.ComponentFilter)]:
         raise ValueError
     if len(comps) > 1:
         compsExpression = expression.orExpression[comps]
@@ -153,16 +151,16 @@
         return expression.isExpression(FIELD_UID, "", True)
     
     # Handle time-range - we cannot do this with our Index right now
-    if propfilter.qualifier and isinstance(propfilter.qualifier, caldavxml.TimeRange):
+    if propfilter.qualifier and isinstance(propfilter.qualifier, queryfilter.TimeRange):
         raise ValueError
     
     # Handle text-match
     tm = None
-    if propfilter.qualifier and isinstance(propfilter.qualifier, caldavxml.TextMatch):
+    if propfilter.qualifier and isinstance(propfilter.qualifier, queryfilter.TextMatch):
         if propfilter.qualifier.negate:
-            tm = expression.notcontainsExpression(propfilter.filter_name, str(propfilter.qualifier), propfilter.qualifier)
+            tm = expression.notcontainsExpression(propfilter.filter_name, propfilter.qualifier.text, propfilter.qualifier.caseless)
         else:
-            tm = expression.containsExpression(propfilter.filter_name, str(propfilter.qualifier), propfilter.qualifier)
+            tm = expression.containsExpression(propfilter.filter_name, propfilter.qualifier.text, propfilter.qualifier.caseless)
     
     # Handle embedded parameters - we do not right now as our Index does not handle them
     params = []
@@ -227,79 +225,3 @@
         return sql.generate()
     except ValueError:
         return None
-
-
-if __name__ == "__main__":
-    import datetime
-
-    filter = caldavxml.Filter(
-                 caldavxml.ComponentFilter(
-                     *[caldavxml.ComponentFilter(
-                           *[caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"})],
-                           **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
-                       )],
-                     **{"name":"VCALENDAR"}
-                 )
-             )
-
-    # A complete implementation of current DST rules for major US time zones.
-    
-    def first_sunday_on_or_after(dt):
-        days_to_go = 6 - dt.weekday()
-        if days_to_go:
-            dt += datetime.timedelta(days_to_go)
-        return dt
-    
-    # In the US, DST starts at 2am (standard time) on the first Sunday in April.
-    DSTSTART = datetime.datetime(1, 4, 1, 2)
-    # and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct.
-    # which is the first Sunday on or after Oct 25.
-    DSTEND = datetime.datetime(1, 10, 25, 1)
-    
-    ZERO = datetime.timedelta(0)
-    HOUR = datetime.timedelta(hours=1)
-
-    class USTimeZone(datetime.tzinfo):
-    
-        def __init__(self, hours, reprname, stdname, dstname):
-            self.stdoffset = datetime.timedelta(hours=hours)
-            self.reprname = reprname
-            self.stdname = stdname
-            self.dstname = dstname
-    
-        def __repr__(self):
-            return self.reprname
-    
-        def tzname(self, dt):
-            if self.dst(dt):
-                return self.dstname
-            else:
-                return self.stdname
-    
-        def utcoffset(self, dt):
-            return self.stdoffset + self.dst(dt)
-    
-        def dst(self, dt):
-            if dt is None or dt.tzinfo is None:
-                # An exception may be sensible here, in one or both cases.
-                # It depends on how you want to treat them.  The default
-                # fromutc() implementation (called by the default astimezone()
-                # implementation) passes a datetime with dt.tzinfo is self.
-                return ZERO
-            assert dt.tzinfo is self
-    
-            # Find first Sunday in April & the last in October.
-            start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year))
-            end = first_sunday_on_or_after(DSTEND.replace(year=dt.year))
-    
-            # Can't compare naive to aware objects, so strip the timezone from
-            # dt first.
-            if start <= dt.replace(tzinfo=None) < end:
-                return HOUR
-            else:
-                return ZERO
-
-    Eastern  = USTimeZone(-5, "Eastern",  "EST", "EDT")
-    filter.children[0].settzinfo(Eastern)
-    
-    print sqlcalendarquery(filter)

Copied: CalendarServer/trunk/twistedcaldav/query/queryfilter.py (from rev 5440, CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/query/queryfilter.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/query/queryfilter.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/query/queryfilter.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -0,0 +1,650 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Object model of CALDAV:filter element used in a calendar-query.
+"""
+
+__all__ = [
+    "Filter",
+]
+
+from twext.python.log import Logger
+
+from twistedcaldav.caldavxml import caldav_namespace, CalDAVTimeZoneElement
+from twistedcaldav.dateops import timeRangesOverlap
+from twistedcaldav.ical import Component, Property, parse_date_or_datetime
+from vobject.icalendar import utc
+import datetime
+
+log = Logger()
+
+class FilterBase(object):
+    """
+    Determines which matching components are returned.
+    """
+
+    def __init__(self, xml_element):
+        self.xmlelement = xml_element
+
+    def match(self, item, access=None):
+        raise NotImplementedError
+
+    def valid(self, level=0):
+        raise NotImplementedError
+
+class Filter(FilterBase):
+    """
+    Determines which matching components are returned.
+    """
+
+    def __init__(self, xml_element):
+
+        super(Filter, self).__init__(xml_element)
+
+        # One comp-filter element must be present
+        if len(xml_element.children) != 1 or xml_element.children[0].qname() != (caldav_namespace, "comp-filter"):
+            raise ValueError("Invalid CALDAV:filter element: %s" % (xml_element,))
+        
+        self.child = ComponentFilter(xml_element.children[0])
+
+    def match(self, component, access=None):
+        """
+        Returns True if the given calendar component matches this filter, False
+        otherwise.
+        """
+        
+        # We only care about certain access restrictions.
+        if access not in (Component.ACCESS_CONFIDENTIAL, Component.ACCESS_RESTRICTED):
+            access = None
+
+        # We need to prepare ourselves for a time-range query by pre-calculating
+        # the set of instances up to the latest time-range limit. That way we can
+        # avoid having to do some form of recurrence expansion for each query sub-part.
+        maxend, isStartTime = self.getmaxtimerange()
+        if maxend:
+            if isStartTime:
+                if component.isRecurringUnbounded():
+                    # Unbounded recurrence is always within a start-only time-range
+                    instances = None
+                else:
+                    # Expand the instances up to infinity
+                    instances = component.expandTimeRanges(datetime.datetime(2100, 1, 1, 0, 0, 0, tzinfo=utc), ignoreInvalidInstances=True)
+            else:
+                instances = component.expandTimeRanges(maxend, ignoreInvalidInstances=True)
+        else:
+            instances = None
+        self.child.setInstances(instances)
+
+        # <filter> contains exactly one <comp-filter>
+        return self.child.match(component, access)
+
+    def valid(self):
+        """
+        Indicate whether this filter element's structure is valid wrt iCalendar
+        data object model.
+        
+        @return: True if valid, False otherwise
+        """
+        
+        # Must have one child element for VCALENDAR
+        return self.child.valid(0)
+        
+    def settimezone(self, tzelement):
+        """
+        Set the default timezone to use with this query.
+        @param calendar: a L{Component} for the VCALENDAR containing the one
+            VTIMEZONE that we want
+        @return: the L{datetime.tzinfo} derived from the VTIMEZONE or utc.
+        """
+        assert tzelement is None or isinstance(tzelement, CalDAVTimeZoneElement)
+
+        if tzelement is not None:
+            calendar = tzelement.calendar()
+            if calendar is not None:
+                for subcomponent in calendar.subcomponents():
+                    if subcomponent.name() == "VTIMEZONE":
+                        # <filter> contains exactly one <comp-filter>
+                        tzinfo = subcomponent.gettzinfo()
+                        self.child.settzinfo(tzinfo)
+                        return tzinfo
+
+        # Default to using utc tzinfo
+        self.child.settzinfo(utc)
+        return utc
+
+    def getmaxtimerange(self):
+        """
+        Get the date farthest into the future in any time-range elements
+        """
+        
+        return self.child.getmaxtimerange(None, False)
+
+class FilterChildBase(FilterBase):
+    """
+    CalDAV filter element.
+    """
+
+    def __init__(self, xml_element):
+
+        super(FilterChildBase, self).__init__(xml_element)
+
+        qualifier = None
+        filters = []
+
+        for child in xml_element.children:
+            qname = child.qname()
+            
+            if qname in (
+                (caldav_namespace, "is-not-defined"),
+                (caldav_namespace, "time-range"),
+                (caldav_namespace, "text-match"),
+            ):
+                if qualifier is not None:
+                    raise ValueError("Only one of CalDAV:time-range, CalDAV:text-match allowed")
+                
+                if qname == (caldav_namespace, "is-not-defined"):
+                    qualifier = IsNotDefined(child)
+                elif qname == (caldav_namespace, "time-range"):
+                    qualifier = TimeRange(child)
+                elif qname == (caldav_namespace, "text-match"):
+                    qualifier = TextMatch(child)
+
+            elif qname == (caldav_namespace, "comp-filter"):
+                filters.append(ComponentFilter(child))
+            elif qname == (caldav_namespace, "prop-filter"):
+                filters.append(PropertyFilter(child))
+            elif qname == (caldav_namespace, "param-filter"):
+                filters.append(ParameterFilter(child))
+            else:
+                raise ValueError("Unknown child element: %s" % (qname,))
+
+        if qualifier and isinstance(qualifier, IsNotDefined) and (len(filters) != 0):
+            raise ValueError("No other tests allowed when CalDAV:is-not-defined is present")
+            
+        self.qualifier = qualifier
+        self.filters = filters
+        self.filter_name = xml_element.attributes["name"]
+        if isinstance(self.filter_name, unicode):
+            self.filter_name = self.filter_name.encode("utf-8")
+        self.defined = not self.qualifier or not isinstance(qualifier, IsNotDefined)
+
+    def match(self, item, access=None):
+        """
+        Returns True if the given calendar item (either a component, property or parameter value)
+        matches this filter, False otherwise.
+        """
+        
+        # Always return True for the is-not-defined case as the result of this will
+        # be negated by the caller
+        if not self.defined: return True
+
+        if self.qualifier and not self.qualifier.match(item, access): return False
+
+        if len(self.filters) > 0:
+            for filter in self.filters:
+                if filter._match(item, access):
+                    return True
+            return False
+        else:
+            return True
+
+class ComponentFilter (FilterChildBase):
+    """
+    Limits a search to only the chosen component types.
+    """
+
+    def match(self, item, access):
+        """
+        Returns True if the given calendar item (which is a component)
+        matches this filter, False otherwise.
+        This specialization uses the instance matching option of the time-range filter
+        to minimize instance expansion.
+        """
+
+        # Always return True for the is-not-defined case as the result of this will
+        # be negated by the caller
+        if not self.defined: return True
+
+        if self.qualifier and not self.qualifier.matchinstance(item, self.instances): return False
+
+        if len(self.filters) > 0:
+            for filter in self.filters:
+                if filter._match(item, access):
+                    return True
+            return False
+        else:
+            return True
+
+    def _match(self, component, access):
+        # At least one subcomponent must match (or is-not-defined is set)
+        for subcomponent in component.subcomponents():
+            # If access restrictions are in force, restrict matching to specific components only.
+            # In particular do not match VALARM.
+            if access and subcomponent.name() not in ("VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VTIMEZONE",):
+                continue
+            
+            # Try to match the component name
+            if isinstance(self.filter_name, str):
+                if subcomponent.name() != self.filter_name: continue
+            else:
+                if subcomponent.name() not in self.filter_name: continue
+            if self.match(subcomponent, access): break
+        else:
+            return not self.defined
+        return self.defined
+        
+    def setInstances(self, instances):
+        """
+        Give the list of instances to each comp-filter element.
+        @param instances: the list of instances.
+        """
+        self.instances = instances
+        for compfilter in [x for x in self.filters if isinstance(x, ComponentFilter)]:
+            compfilter.setInstances(instances)
+        
+    def valid(self, level):
+        """
+        Indicate whether this filter element's structure is valid wrt iCalendar
+        data object model.
+        
+        @param level: the nesting level of this filter element, 0 being the top comp-filter.
+        @return:      True if valid, False otherwise
+        """
+        
+        # Check for time-range
+        timerange = self.qualifier and isinstance(self.qualifier, TimeRange)
+
+        if level == 0:
+            # Must have VCALENDAR at the top
+            if (self.filter_name != "VCALENDAR") or timerange:
+                log.msg("Top-level comp-filter must be VCALENDAR, instead: %s" % (self.filter_name,))
+                return False
+        elif level == 1:
+            # Disallow VCALENDAR, VALARM, STANDARD, DAYLIGHT, AVAILABLE at the top, everything else is OK
+            if self.filter_name in ("VCALENDAR", "VALARM", "STANDARD", "DAYLIGHT", "AVAILABLE"):
+                log.msg("comp-filter wrong component type: %s" % (self.filter_name,))
+                return False
+            
+            # time-range only on VEVENT, VTODO, VJOURNAL, VFREEBUSY, VAVAILABILITY
+            if timerange and self.filter_name not in ("VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VAVAILABILITY"):
+                log.msg("time-range cannot be used with component %s" % (self.filter_name,))
+                return False
+        elif level == 2:
+            # Disallow VCALENDAR, VTIMEZONE, VEVENT, VTODO, VJOURNAL, VFREEBUSY, VAVAILABILITY at the top, everything else is OK
+            if (self.filter_name in ("VCALENDAR", "VTIMEZONE", "VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VAVAILABILITY")):
+                log.msg("comp-filter wrong sub-component type: %s" % (self.filter_name,))
+                return False
+            
+            # time-range only on VALARM, AVAILABLE
+            if timerange and self.filter_name not in ("VALARM", "AVAILABLE",):
+                log.msg("time-range cannot be used with sub-component %s" % (self.filter_name,))
+                return False
+        else:
+            # Disallow all standard iCal components anywhere else
+            if (self.filter_name in ("VCALENDAR", "VTIMEZONE", "VEVENT", "VTODO", "VJOURNAL", "VFREEBUSY", "VALARM", "STANDARD", "DAYLIGHT", "AVAILABLE")) or timerange:
+                log.msg("comp-filter wrong standard component type: %s" % (self.filter_name,))
+                return False
+        
+        # Test each property
+        for propfilter in [x for x in self.filters if isinstance(x, PropertyFilter)]:
+            if not propfilter.valid():
+                return False
+
+        # Test each component
+        for compfilter in [x for x in self.filters if isinstance(x, ComponentFilter)]:
+            if not compfilter.valid(level + 1):
+                return False
+
+        # Test the time-range
+        if timerange:
+            if not self.qualifier.valid():
+                return False
+
+        return True
+
+    def settzinfo(self, tzinfo):
+        """
+        Set the default timezone to use with this query.
+        @param tzinfo: a L{datetime.tzinfo} to use.
+        """
+        
+        # Give tzinfo to any TimeRange we have
+        if isinstance(self.qualifier, TimeRange):
+            self.qualifier.settzinfo(tzinfo)
+        
+        # Pass down to sub components/properties
+        for x in self.filters:
+            x.settzinfo(tzinfo)
+
+    def getmaxtimerange(self, currentMaximum, currentIsStartTime):
+        """
+        Get the date furthest into the future in any time-range elements
+        
+        @param currentMaximum: current future value to compare with
+        @type currentMaximum: L{datetime.datetime}
+        """
+        
+        # Give tzinfo to any TimeRange we have
+        isStartTime = False
+        if isinstance(self.qualifier, TimeRange):
+            isStartTime = self.qualifier.end is None
+            compareWith = self.qualifier.start if isStartTime else self.qualifier.end
+            if currentMaximum is None or currentMaximum < compareWith:
+                currentMaximum = compareWith
+                currentIsStartTime = isStartTime
+        
+        # Pass down to sub components/properties
+        for x in self.filters:
+            currentMaximum, currentIsStartTime = x.getmaxtimerange(currentMaximum, currentIsStartTime)
+
+        return currentMaximum, currentIsStartTime
+
+class PropertyFilter (FilterChildBase):
+    """
+    Limits a search to specific properties.
+    """
+
+    def _match(self, component, access):
+        # When access restriction is in force, we need to only allow matches against the properties
+        # allowed by the access restriction level.
+        if access:
+            allowedProperties = Component.confidentialPropertiesMap.get(component.name(), None)
+            if allowedProperties and access == Component.ACCESS_RESTRICTED:
+                allowedProperties += Component.extraRestrictedProperties
+        else:
+            allowedProperties = None
+
+        # At least one property must match (or is-not-defined is set)
+        for property in component.properties():
+            # Apply access restrictions, if any.
+            if allowedProperties is not None and property.name() not in allowedProperties:
+                continue
+            if property.name() == self.filter_name and self.match(property, access): break
+        else:
+            return not self.defined
+        return self.defined
+
+    def valid(self):
+        """
+        Indicate whether this filter element's structure is valid wrt iCalendar
+        data object model.
+        
+        @return:      True if valid, False otherwise
+        """
+        
+        # Check for time-range
+        timerange = self.qualifier and isinstance(self.qualifier, TimeRange)
+        
+        # time-range only on COMPLETED, CREATED, DTSTAMP, LAST-MODIFIED
+        if timerange and self.filter_name not in ("COMPLETED", "CREATED", "DTSTAMP", "LAST-MODIFIED"):
+            log.msg("time-range cannot be used with property %s" % (self.filter_name,))
+            return False
+
+        # Test the time-range
+        if timerange:
+            if not self.qualifier.valid():
+                return False
+
+        # No other tests
+        return True
+
+    def settzinfo(self, tzinfo):
+        """
+        Set the default timezone to use with this query.
+        @param tzinfo: a L{datetime.tzinfo} to use.
+        """
+        
+        # Give tzinfo to any TimeRange we have
+        if isinstance(self.qualifier, TimeRange):
+            self.qualifier.settzinfo(tzinfo)
+
+    def getmaxtimerange(self, currentMaximum, currentIsStartTime):
+        """
+        Get the date furthest into the future in any time-range elements
+        
+        @param currentMaximum: current future value to compare with
+        @type currentMaximum: L{datetime.datetime}
+        """
+        
+        # Give tzinfo to any TimeRange we have
+        isStartTime = False
+        if isinstance(self.qualifier, TimeRange):
+            isStartTime = self.qualifier.end is None
+            compareWith = self.qualifier.start if isStartTime else self.qualifier.end
+            if currentMaximum is None or currentMaximum < compareWith:
+                currentMaximum = compareWith
+                currentIsStartTime = isStartTime
+
+        return currentMaximum, currentIsStartTime
+
+class ParameterFilter (FilterChildBase):
+    """
+    Limits a search to specific parameters.
+    """
+
+    def _match(self, property, access):
+
+        # We have to deal with the problem that the 'Native' form of a property
+        # will be missing the TZID parameter due to the conversion performed. Converting
+        # to non-native for the entire calendar object causes problems elsewhere, so its
+        # best to do it here for this one special case.
+        if self.filter_name == "TZID":
+            transformed = property.transformAllFromNative()
+        else:
+            transformed = False
+
+        # At least one property must match (or is-not-defined is set)
+        result = not self.defined
+        for parameterName in property.params().keys():
+            if parameterName == self.filter_name and self.match(property.params()[parameterName], access):
+                result = self.defined
+                break
+
+        if transformed:
+            property.transformAllToNative()
+        return result
+
+class IsNotDefined (FilterBase):
+    """
+    Specifies that the named iCalendar item does not exist.
+    """
+
+    def match(self, component, access=None):
+        # Oddly, this needs always to return True so that it appears there is
+        # a match - but we then "negate" the result if is-not-defined is set.
+        # Actually this method should never be called as we special case the
+        # is-not-defined option.
+        return True
+
+class TextMatch (FilterBase):
+    """
+    Specifies a substring match on a property or parameter value.
+    (CalDAV-access-09, section 9.6.4)
+    """
+    def __init__(self, xml_element):
+
+        super(TextMatch, self).__init__(xml_element)
+
+        self.text = str(xml_element)
+        if "caseless" in xml_element.attributes:
+            caseless = xml_element.attributes["caseless"]
+            if caseless == "yes":
+                self.caseless = True
+            elif caseless == "no":
+                self.caseless = False
+        else:
+            self.caseless = True
+
+        if "negate-condition" in xml_element.attributes:
+            negate = xml_element.attributes["negate-condition"]
+            if negate == "yes":
+                self.negate = True
+            elif caseless == "no":
+                self.negate = False
+        else:
+            self.negate = False
+
+    def match(self, item, access):
+        """
+        Match the text for the item.
+        If the item is a property, then match the property value,
+        otherwise it may be a list of parameter values - try to match anyone of those
+        """
+        if item is None: return False
+
+        if isinstance(item, Property):
+            values = [item.value()]
+        else:
+            values = item
+
+        test = unicode(self.text, "utf-8")
+        if self.caseless:
+            test = test.lower()
+
+        def _textCompare(s):
+            if self.caseless:
+                if s.lower().find(test) != -1:
+                    return True, not self.negate
+            else:
+                if s.find(test) != -1:
+                    return True, not self.negate
+            return False, False
+
+        for value in values:
+            # NB Its possible that we have a text list value which appears as a Python list,
+            # so we need to check for that an iterate over the list.
+            if isinstance(value, list):
+                for subvalue in value:
+                    matched, result = _textCompare(subvalue)
+                    if matched:
+                        return result
+            else:
+                matched, result = _textCompare(value)
+                if matched:
+                    return result
+        
+        return self.negate
+
+class TimeRange (FilterBase):
+    """
+    Specifies a time for testing components against.
+    """
+
+    def __init__(self, xml_element):
+
+        super(TimeRange, self).__init__(xml_element)
+
+        # One of start or end must be present
+        if "start" not in xml_element.attributes and "end" not in xml_element.attributes:
+            raise ValueError("One of 'start' or 'end' must be present in CALDAV:time-range")
+        
+        self.start = parse_date_or_datetime(xml_element.attributes["start"]) if "start" in xml_element.attributes else None
+        self.end = parse_date_or_datetime(xml_element.attributes["end"]) if "end" in xml_element.attributes else None
+        self.tzinfo = None
+
+    def settzinfo(self, tzinfo):
+        """
+        Set the default timezone to use with this query.
+        @param tzinfo: a L{datetime.tzinfo} to use.
+        """
+        
+        # Give tzinfo to any TimeRange we have
+        self.tzinfo = tzinfo
+
+    def valid(self, level=0):
+        """
+        Indicate whether the time-range is valid (must be date-time in UTC).
+        
+        @return:      True if valid, False otherwise
+        """
+        
+        if self.start is not None and not isinstance(self.start, datetime.datetime):
+            log.msg("start attribute in <time-range> is not a date-time: %s" % (self.start,))
+            return False
+        if self.end is not None and not isinstance(self.end, datetime.datetime):
+            log.msg("end attribute in <time-range> is not a date-time: %s" % (self.end,))
+            return False
+        if self.start is not None and self.start.tzinfo != utc:
+            log.msg("start attribute in <time-range> is not UTC: %s" % (self.start,))
+            return False
+        if self.end is not None and self.end.tzinfo != utc:
+            log.msg("end attribute in <time-range> is not UTC: %s" % (self.end,))
+            return False
+
+        # No other tests
+        return True
+
+    def match(self, property, access=None):
+        """
+        NB This is only called when doing a time-range match on a property.
+        """
+        if property is None:
+            return False
+        else:
+            return property.containsTimeRange(self.start, self.end, self.tzinfo)
+
+    def matchinstance(self, component, instances):
+        """
+        Test whether this time-range element causes a match to the specified component
+        using the specified set of instances to determine the expanded time ranges.
+        @param component: the L{Component} to test.
+        @param instances: the list of expanded instances.
+        @return: True if the time-range query matches, False otherwise.
+        """
+        if component is None:
+            return False
+        
+        assert instances is not None or self.end is None, "Failure to expand instance for time-range filter: %r" % (self,)
+        
+        # Special case open-ended unbounded
+        if instances is None:
+            if component.getRecurrenceIDUTC() is None:
+                return True
+            else:
+                # See if the overridden component's start is past the start
+                start, _ignore_end = component.getEffectiveStartEnd()
+                if start is None:
+                    return True
+                else:
+                    return start >= self.start
+
+        # Handle alarms as a special case
+        alarms = (component.name() == "VALARM")
+        if alarms:
+            testcomponent = component._parent
+        else:
+            testcomponent = component
+            
+        for key in instances:
+            instance = instances[key]
+            
+            # First make sure components match
+            if not testcomponent.same(instance.component):
+                continue
+
+            if alarms:
+                # Get all the alarm triggers for this instance and test each one
+                triggers = instance.getAlarmTriggers()
+                for trigger in triggers:
+                    if timeRangesOverlap(trigger, None, self.start, self.end, self.tzinfo):
+                        return True
+            else:
+                # Regular instance overlap test
+                if timeRangesOverlap(instance.start, instance.end, self.start, self.end, self.tzinfo):
+                    return True
+
+        return False

Modified: CalendarServer/trunk/twistedcaldav/query/sqlgenerator.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/query/sqlgenerator.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/query/sqlgenerator.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -30,23 +30,26 @@
 
 class sqlgenerator(object):
     
-    FROM          =" from "
-    WHERE         =" where "
-    RESOURCEDB    = "RESOURCE"
-    TIMESPANDB    = "TIMESPAN"
-    NOTOP         = "NOT "
-    ANDOP         = " AND "
-    OROP          = " OR "
-    CONTAINSOP    = " GLOB "
-    NOTCONTAINSOP = " NOT GLOB "
-    ISOP          = " == "
-    ISNOTOP       = " != "
-    INOP          = " IN "
-    NOTINOP       = " NOT IN "
+    FROM           =" from "
+    WHERE          =" where "
+    RESOURCEDB     = "RESOURCE"
+    TIMESPANDB     = "TIMESPAN"
+    TRANSPARENCYDB = "TRANSPARENCY"
+    PERUSERDB      = "PERUSER"
+    NOTOP          = "NOT "
+    ANDOP          = " AND "
+    OROP           = " OR "
+    CONTAINSOP     = " GLOB "
+    NOTCONTAINSOP  = " NOT GLOB "
+    ISOP           = " == "
+    ISNOTOP        = " != "
+    INOP           = " IN "
+    NOTINOP        = " NOT IN "
 
-    TIMESPANTEST         = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.START < %s AND TIMESPAN.END > %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.START < %s AND TIMESPAN.END > %s)) AND TIMESPAN.NAME == RESOURCE.NAME"
-    TIMESPANTEST_NOEND   = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.END > %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.END > %s)) AND TIMESPAN.NAME == RESOURCE.NAME"
-    TIMESPANTEST_NOSTART = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.START < %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.START < %s)) AND TIMESPAN.NAME == RESOURCE.NAME"
+    TIMESPANTEST         = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.START < %s AND TIMESPAN.END > %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.START < %s AND TIMESPAN.END > %s))"
+    TIMESPANTEST_NOEND   = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.END > %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.END > %s))"
+    TIMESPANTEST_NOSTART = "((TIMESPAN.FLOAT == 'N' AND TIMESPAN.START < %s) OR (TIMESPAN.FLOAT == 'Y' AND TIMESPAN.START < %s))"
+    TIMESPANTEST_TAIL_PIECE = " AND TIMESPAN.RESOURCEID == RESOURCE.RESOURCEID AND TIMESPAN.INSTANCEID == TRANSPARENCY.INSTANCEID"
 
     def __init__(self, expr):
         self.expression = expr
@@ -72,7 +75,7 @@
         # Prefix with ' from ...' partial statement
         select = self.FROM + self.RESOURCEDB
         if self.usedtimespan:
-            select += ", " + self.TIMESPANDB
+            select += ", %s, %s, %s" % (self.TIMESPANDB, self.TRANSPARENCYDB, self.PERUSERDB,)
         select += self.sout.getvalue()
         return select, self.arguments
         
@@ -134,6 +137,7 @@
                 arg1 = self.setArgument(expr.end)
                 arg2 = self.setArgument(expr.endfloat)
                 test = self.TIMESPANTEST_NOSTART % (arg1, arg2)
+            test += self.TIMESPANTEST_TAIL_PIECE
             self.sout.write(test)
             self.usedtimespan = True
         

Deleted: CalendarServer/trunk/twistedcaldav/query/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/query/test/__init__.py	2010-04-07 19:28:58 UTC (rev 5440)
+++ CalendarServer/trunk/twistedcaldav/query/test/__init__.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -1,19 +0,0 @@
-##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-"""
-Tests for the twistedcaldav.query module.
-"""

Copied: CalendarServer/trunk/twistedcaldav/query/test/__init__.py (from rev 5440, CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/query/test/__init__.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/query/test/__init__.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/query/test/__init__.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -0,0 +1,19 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Tests for the twistedcaldav.query module.
+"""

Deleted: CalendarServer/trunk/twistedcaldav/query/test/test_calendarquery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/query/test/test_calendarquery.py	2010-04-07 19:28:58 UTC (rev 5440)
+++ CalendarServer/trunk/twistedcaldav/query/test/test_calendarquery.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -1,99 +0,0 @@
-##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twistedcaldav import caldavxml
-from twistedcaldav.query import queryfilter
-from twistedcaldav.query.calendarquery import sqlcalendarquery
-import datetime
-import twistedcaldav.test.util
-
-class Tests(twistedcaldav.test.util.TestCase):
-
-    def test_query(self):
-
-        filter = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                *[caldavxml.ComponentFilter(
-                    *[caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"})],
-                    **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
-                )],
-                **{"name":"VCALENDAR"}
-            )
-        )
-        filter = queryfilter.Filter(filter)
-    
-        # A complete implementation of current DST rules for major US time zones.
-        
-        def first_sunday_on_or_after(dt):
-            days_to_go = 6 - dt.weekday()
-            if days_to_go:
-                dt += datetime.timedelta(days_to_go)
-            return dt
-        
-        # In the US, DST starts at 2am (standard time) on the first Sunday in April.
-        DSTSTART = datetime.datetime(1, 4, 1, 2)
-        # and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct.
-        # which is the first Sunday on or after Oct 25.
-        DSTEND = datetime.datetime(1, 10, 25, 1)
-        
-        ZERO = datetime.timedelta(0)
-        HOUR = datetime.timedelta(hours=1)
-    
-        class USTimeZone(datetime.tzinfo):
-        
-            def __init__(self, hours, reprname, stdname, dstname):
-                self.stdoffset = datetime.timedelta(hours=hours)
-                self.reprname = reprname
-                self.stdname = stdname
-                self.dstname = dstname
-        
-            def __repr__(self):
-                return self.reprname
-        
-            def tzname(self, dt):
-                if self.dst(dt):
-                    return self.dstname
-                else:
-                    return self.stdname
-        
-            def utcoffset(self, dt):
-                return self.stdoffset + self.dst(dt)
-        
-            def dst(self, dt):
-                if dt is None or dt.tzinfo is None:
-                    # An exception may be sensible here, in one or both cases.
-                    # It depends on how you want to treat them.  The default
-                    # fromutc() implementation (called by the default astimezone()
-                    # implementation) passes a datetime with dt.tzinfo is self.
-                    return ZERO
-                assert dt.tzinfo is self
-        
-                # Find first Sunday in April & the last in October.
-                start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year))
-                end = first_sunday_on_or_after(DSTEND.replace(year=dt.year))
-        
-                # Can't compare naive to aware objects, so strip the timezone from
-                # dt first.
-                if start <= dt.replace(tzinfo=None) < end:
-                    return HOUR
-                else:
-                    return ZERO
-    
-        Eastern  = USTimeZone(-5, "Eastern",  "EST", "EDT")
-        filter.child.settzinfo(Eastern)
-        
-        print sqlcalendarquery(filter)
-        
\ No newline at end of file

Copied: CalendarServer/trunk/twistedcaldav/query/test/test_calendarquery.py (from rev 5440, CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/query/test/test_calendarquery.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/query/test/test_calendarquery.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/query/test/test_calendarquery.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -0,0 +1,99 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav import caldavxml
+from twistedcaldav.query import queryfilter
+from twistedcaldav.query.calendarquery import sqlcalendarquery
+import datetime
+import twistedcaldav.test.util
+
+class Tests(twistedcaldav.test.util.TestCase):
+
+    def test_query(self):
+
+        filter = caldavxml.Filter(
+            caldavxml.ComponentFilter(
+                *[caldavxml.ComponentFilter(
+                    *[caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"})],
+                    **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
+                )],
+                **{"name":"VCALENDAR"}
+            )
+        )
+        filter = queryfilter.Filter(filter)
+    
+        # A complete implementation of current DST rules for major US time zones.
+        
+        def first_sunday_on_or_after(dt):
+            days_to_go = 6 - dt.weekday()
+            if days_to_go:
+                dt += datetime.timedelta(days_to_go)
+            return dt
+        
+        # In the US, DST starts at 2am (standard time) on the first Sunday in April.
+        DSTSTART = datetime.datetime(1, 4, 1, 2)
+        # and ends at 2am (DST time; 1am standard time) on the last Sunday of Oct.
+        # which is the first Sunday on or after Oct 25.
+        DSTEND = datetime.datetime(1, 10, 25, 1)
+        
+        ZERO = datetime.timedelta(0)
+        HOUR = datetime.timedelta(hours=1)
+    
+        class USTimeZone(datetime.tzinfo):
+        
+            def __init__(self, hours, reprname, stdname, dstname):
+                self.stdoffset = datetime.timedelta(hours=hours)
+                self.reprname = reprname
+                self.stdname = stdname
+                self.dstname = dstname
+        
+            def __repr__(self):
+                return self.reprname
+        
+            def tzname(self, dt):
+                if self.dst(dt):
+                    return self.dstname
+                else:
+                    return self.stdname
+        
+            def utcoffset(self, dt):
+                return self.stdoffset + self.dst(dt)
+        
+            def dst(self, dt):
+                if dt is None or dt.tzinfo is None:
+                    # An exception may be sensible here, in one or both cases.
+                    # It depends on how you want to treat them.  The default
+                    # fromutc() implementation (called by the default astimezone()
+                    # implementation) passes a datetime with dt.tzinfo is self.
+                    return ZERO
+                assert dt.tzinfo is self
+        
+                # Find first Sunday in April & the last in October.
+                start = first_sunday_on_or_after(DSTSTART.replace(year=dt.year))
+                end = first_sunday_on_or_after(DSTEND.replace(year=dt.year))
+        
+                # Can't compare naive to aware objects, so strip the timezone from
+                # dt first.
+                if start <= dt.replace(tzinfo=None) < end:
+                    return HOUR
+                else:
+                    return ZERO
+    
+        Eastern  = USTimeZone(-5, "Eastern",  "EST", "EDT")
+        filter.child.settzinfo(Eastern)
+        
+        print sqlcalendarquery(filter)
+        
\ No newline at end of file

Deleted: CalendarServer/trunk/twistedcaldav/query/test/test_queryfilter.py
===================================================================
--- CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/query/test/test_queryfilter.py	2010-04-07 19:28:58 UTC (rev 5440)
+++ CalendarServer/trunk/twistedcaldav/query/test/test_queryfilter.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -1,77 +0,0 @@
-##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-##
-
-from twistedcaldav import caldavxml
-from twistedcaldav.query import queryfilter
-import twistedcaldav.test.util
-
-class Tests(twistedcaldav.test.util.TestCase):
-
-    def test_allQuery(self):
-
-        xml_element = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                **{"name":"VCALENDAR"}
-            )
-        )
-
-        queryfilter.Filter(xml_element)
-        
-    def test_simpleSummaryRangeQuery(self):
-
-        xml_element = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                caldavxml.ComponentFilter(
-                    caldavxml.PropertyFilter(
-                        caldavxml.TextMatch.fromString("test"),
-                        **{"name":"SUMMARY",}
-                    ),
-                    **{"name":"VEVENT"}
-                ),
-                **{"name":"VCALENDAR"}
-            )
-        )
-
-        queryfilter.Filter(xml_element)
-        
-    def test_simpleTimeRangeQuery(self):
-
-        xml_element = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                caldavxml.ComponentFilter(
-                    caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"}),
-                    **{"name":"VEVENT"}
-                ),
-                **{"name":"VCALENDAR"}
-            )
-        )
-
-        queryfilter.Filter(xml_element)
-        
-    def test_multipleTimeRangeQuery(self):
-
-        xml_element = caldavxml.Filter(
-            caldavxml.ComponentFilter(
-                caldavxml.ComponentFilter(
-                    caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"}),
-                    **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
-                ),
-                **{"name":"VCALENDAR"}
-            )
-        )
-
-        queryfilter.Filter(xml_element)
-        
\ No newline at end of file

Copied: CalendarServer/trunk/twistedcaldav/query/test/test_queryfilter.py (from rev 5440, CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/query/test/test_queryfilter.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/query/test/test_queryfilter.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/query/test/test_queryfilter.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -0,0 +1,77 @@
+##
+# Copyright (c) 2009 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav import caldavxml
+from twistedcaldav.query import queryfilter
+import twistedcaldav.test.util
+
+class Tests(twistedcaldav.test.util.TestCase):
+
+    def test_allQuery(self):
+
+        xml_element = caldavxml.Filter(
+            caldavxml.ComponentFilter(
+                **{"name":"VCALENDAR"}
+            )
+        )
+
+        queryfilter.Filter(xml_element)
+        
+    def test_simpleSummaryRangeQuery(self):
+
+        xml_element = caldavxml.Filter(
+            caldavxml.ComponentFilter(
+                caldavxml.ComponentFilter(
+                    caldavxml.PropertyFilter(
+                        caldavxml.TextMatch.fromString("test"),
+                        **{"name":"SUMMARY",}
+                    ),
+                    **{"name":"VEVENT"}
+                ),
+                **{"name":"VCALENDAR"}
+            )
+        )
+
+        queryfilter.Filter(xml_element)
+        
+    def test_simpleTimeRangeQuery(self):
+
+        xml_element = caldavxml.Filter(
+            caldavxml.ComponentFilter(
+                caldavxml.ComponentFilter(
+                    caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"}),
+                    **{"name":"VEVENT"}
+                ),
+                **{"name":"VCALENDAR"}
+            )
+        )
+
+        queryfilter.Filter(xml_element)
+        
+    def test_multipleTimeRangeQuery(self):
+
+        xml_element = caldavxml.Filter(
+            caldavxml.ComponentFilter(
+                caldavxml.ComponentFilter(
+                    caldavxml.TimeRange(**{"start":"20060605T160000Z", "end":"20060605T170000Z"}),
+                    **{"name":("VEVENT", "VFREEBUSY", "VAVAILABILITY")}
+                ),
+                **{"name":"VCALENDAR"}
+            )
+        )
+
+        queryfilter.Filter(xml_element)
+        
\ No newline at end of file

Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -61,14 +61,37 @@
 from twistedcaldav.config import config
 from twistedcaldav.customxml import TwistedCalendarAccessProperty
 from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.extensions import DAVResource, DAVPrincipalResource
+from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
+from twistedcaldav.extensions import DAVResource, DAVPrincipalResource,\
+    PropertyNotFoundError
 from twistedcaldav.ical import Component
 from twistedcaldav.ical import Component as iComponent
 from twistedcaldav.ical import allowedComponents
 from twistedcaldav.icaldav import ICalDAVResource, ICalendarPrincipalResource
+from twistedcaldav.sharing import SharedCollectionMixin
 from twistedcaldav.vcard import Component as vComponent
 
 
+##
+# Sharing Conts
+##
+SHARE_ACCEPT_STATE_NEEDS_ACTION = "0"
+SHARE_ACCEPT_STATE_ACCEPTED = "1"
+SHARE_ACCEPT_STATE_DECLINED = "2"
+SHARE_ACCEPT_STATE_DELETED = "-1"
+
+shareAccpetStates = {}
+shareAccpetStates[SHARE_ACCEPT_STATE_NEEDS_ACTION] = "NEEDS-ACTION"
+shareAccpetStates[SHARE_ACCEPT_STATE_ACCEPTED] = "ACCEPTED"
+shareAccpetStates[SHARE_ACCEPT_STATE_DECLINED] = "DECLINED"
+shareAccpetStates[SHARE_ACCEPT_STATE_DELETED] = "DELETED"
+
+shareAcceptStatesByXML = {}
+shareAcceptStatesByXML["NEEDS-ACTION"] = customxml.InviteStatusNoResponse()
+shareAcceptStatesByXML["ACCEPTED"] = customxml.InviteStatusAccepted()
+shareAcceptStatesByXML["DECLINED"] = customxml.InviteStatusDeclined()
+shareAcceptStatesByXML["DELETED"] = customxml.InviteStatusDeleted()
+
 class CalDAVComplianceMixIn(object):
     def davComplianceClasses(self):
         return (
@@ -77,7 +100,7 @@
         )
 
 
-class CalDAVResource (CalDAVComplianceMixIn, DAVResource, LoggingMixIn):
+class CalDAVResource (CalDAVComplianceMixIn, SharedCollectionMixin, DAVResource, LoggingMixIn):
     """
     CalDAV resource.
 
@@ -137,15 +160,55 @@
     ##
 
     liveProperties = DAVResource.liveProperties + (
-        (dav_namespace,    "owner"),               # Private Events needs this but it is also OK to return empty
-        (caldav_namespace, "supported-calendar-component-set"),
-        (caldav_namespace, "supported-calendar-data"         ),
+        davxml.Owner.qname(),               # Private Events needs this but it is also OK to return empty
+        caldavxml.SupportedCalendarComponentSet.qname(),
+        caldavxml.SupportedCalendarData.qname(),
     )
 
     supportedCalendarComponentSet = caldavxml.SupportedCalendarComponentSet(
         *[caldavxml.CalendarComponent(name=item) for item in allowedComponents]
     )
 
+    @classmethod
+    def enableSharing(clz, enable):
+        qname = (calendarserver_namespace, "invite" )
+        if enable and qname not in clz.liveProperties:
+            clz.liveProperties += (qname,)
+        elif not enable and qname in clz.liveProperties:
+            clz.liveProperties = tuple([p for p in clz.liveProperties if p != qname])
+
+    def isShadowableProperty(self, qname):
+        """
+        Shadowable properties are ones on shared resources where a "default" exists until
+        a user overrides with their own value.
+        """
+        return qname in (
+            caldavxml.CalendarDescription.qname(),
+            caldavxml.CalendarTimeZone.qname(),
+        )
+
+    def isGlobalProperty(self, qname):
+        """
+        A global property is one that is the same for all users.
+        """
+        if qname in self.liveProperties:
+            if qname in (
+                davxml.DisplayName.qname(),
+                customxml.Invite.qname(),
+            ):
+                return False
+            else:
+                return True
+        elif qname in (
+            customxml.GETCTag.qname(),
+            caldavxml.MaxResourceSize.qname(),
+            caldavxml.MaxAttendeesPerInstance.qname(),
+        ):
+            return True
+        else:
+            return False
+
+    @inlineCallbacks
     def hasProperty(self, property, request):
         """
         Need to special case schedule-calendar-transp for backwards compatability.
@@ -156,8 +219,34 @@
         else:
             qname = property.qname()
 
+        isvirt = (yield self.isVirtualShare(request))
+        if isvirt:
+            if self.isShadowableProperty(qname):
+                ownerPrincipal = (yield self.resourceOwnerPrincipal(request))
+                p = self.deadProperties().contains(qname, uid=ownerPrincipal.principalUID())
+                if p:
+                    returnValue(p)
+                
+            elif (not self.isGlobalProperty(qname)):
+                ownerPrincipal = (yield self.resourceOwnerPrincipal(request))
+                p = self.deadProperties().contains(qname, uid=ownerPrincipal.principalUID())
+                returnValue(p)
+
+        res = (yield self._hasGlobalProperty(property, request))
+        returnValue(res)
+
+    def _hasGlobalProperty(self, property, request):
+        """
+        Need to special case schedule-calendar-transp for backwards compatability.
+        """
+        
+        if type(property) is tuple:
+            qname = property
+        else:
+            qname = property.qname()
+
         # Force calendar collections to always appear to have the property
-        if qname == (caldav_namespace, "schedule-calendar-transp") and self.isCalendarCollection():
+        if qname == caldavxml.ScheduleCalendarTransp.qname() and self.isCalendarCollection():
             return succeed(True)
         else:
             return super(CalDAVResource, self).hasProperty(property, request)
@@ -169,52 +258,75 @@
         else:
             qname = property.qname()
 
-        namespace, name = qname
+        isvirt = (yield self.isVirtualShare(request))
+        if isvirt:
+            if self.isShadowableProperty(qname):
+                ownerPrincipal = (yield self.resourceOwnerPrincipal(request))
+                try:
+                    p = self.deadProperties().get(qname, uid=ownerPrincipal.principalUID())
+                    returnValue(p)
+                except PropertyNotFoundError:
+                    pass
+                
+            elif (not self.isGlobalProperty(qname)):
+                ownerPrincipal = (yield self.resourceOwnerPrincipal(request))
+                p = self.deadProperties().get(qname, uid=ownerPrincipal.principalUID())
+                returnValue(p)
 
-        if namespace == dav_namespace:
-            if name == "owner":
-                owner = (yield self.owner(request))
-                returnValue(davxml.Owner(owner))
+        res = (yield self._readGlobalProperty(qname, property, request))
+        returnValue(res)
 
-        elif namespace == caldav_namespace:
-            if name == "supported-calendar-component-set":
-                # CalDAV-access-09, section 5.2.3
-                if self.hasDeadProperty(qname):
-                    returnValue(self.readDeadProperty(qname))
-                returnValue(self.supportedCalendarComponentSet)
-            elif name == "supported-calendar-data":
-                # CalDAV-access-09, section 5.2.4
-                returnValue(caldavxml.SupportedCalendarData(
-                    caldavxml.CalendarData(**{
-                        "content-type": "text/calendar",
-                        "version"     : "2.0",
-                    }),
+    @inlineCallbacks
+    def _readGlobalProperty(self, qname, property, request):
+
+        if qname == davxml.Owner.qname():
+            owner = (yield self.owner(request))
+            returnValue(davxml.Owner(owner))
+
+        elif qname == caldavxml.SupportedCalendarComponentSet.qname():
+            # CalDAV-access-09, section 5.2.3
+            if self.hasDeadProperty(qname):
+                returnValue(self.readDeadProperty(qname))
+            returnValue(self.supportedCalendarComponentSet)
+
+        elif qname == caldavxml.SupportedCalendarData.qname():
+            # CalDAV-access-09, section 5.2.4
+            returnValue(caldavxml.SupportedCalendarData(
+                caldavxml.CalendarData(**{
+                    "content-type": "text/calendar",
+                    "version"     : "2.0",
+                }),
+            ))
+
+        elif qname == caldavxml.MaxResourceSize.qname():
+            # CalDAV-access-15, section 5.2.5
+            if config.MaximumAttachmentSize:
+                returnValue(caldavxml.MaxResourceSize.fromString(
+                    str(config.MaximumAttachmentSize)
                 ))
-            elif name == "max-resource-size":
-                # CalDAV-access-15, section 5.2.5
-                if config.MaximumAttachmentSize:
-                    returnValue(caldavxml.MaxResourceSize.fromString(
-                        str(config.MaximumAttachmentSize)
-                    ))
 
-            elif name == "max-attendees-per-instance":
-                # CalDAV-access-15, section 5.2.9
-                if config.MaxAttendeesPerInstance:
-                    returnValue(caldavxml.MaxAttendeesPerInstance.fromString(
-                        str(config.MaxAttendeesPerInstance)
-                    ))
+        elif qname == caldavxml.MaxAttendeesPerInstance.qname():
+            # CalDAV-access-15, section 5.2.9
+            if config.MaxAttendeesPerInstance:
+                returnValue(caldavxml.MaxAttendeesPerInstance.fromString(
+                    str(config.MaxAttendeesPerInstance)
+                ))
 
-            elif name == "schedule-calendar-transp":
-                # For backwards compatibility, if the property does not exist we need to create
-                # it and default to the old free-busy-set value.
-                if self.isCalendarCollection() and not self.hasDeadProperty(property):
-                    # For backwards compatibility we need to sync this up with the calendar-free-busy-set on the inbox
-                    principal = (yield self.ownerPrincipal(request))
-                    fbset = (yield principal.calendarFreeBusyURIs(request))
-                    url = (yield self.canonicalURL(request))
-                    opaque = url in fbset
-                    self.writeDeadProperty(caldavxml.ScheduleCalendarTransp(caldavxml.Opaque() if opaque else caldavxml.Transparent()))
+        elif qname == caldavxml.ScheduleCalendarTransp.qname():
+            # For backwards compatibility, if the property does not exist we need to create
+            # it and default to the old free-busy-set value.
+            if self.isCalendarCollection() and not self.hasDeadProperty(property):
+                # For backwards compatibility we need to sync this up with the calendar-free-busy-set on the inbox
+                principal = (yield self.resourceOwnerPrincipal(request))
+                fbset = (yield principal.calendarFreeBusyURIs(request))
+                url = (yield self.canonicalURL(request))
+                opaque = url in fbset
+                self.writeDeadProperty(caldavxml.ScheduleCalendarTransp(caldavxml.Opaque() if opaque else caldavxml.Transparent()))
 
+        elif qname == customxml.Invite.qname():
+            result = (yield self.inviteProperty(request))
+            returnValue(result)
+
         result = (yield super(CalDAVResource, self).readProperty(property, request))
         returnValue(result)
 
@@ -223,8 +335,21 @@
         assert isinstance(property, davxml.WebDAVElement), (
             "%r is not a WebDAVElement instance" % (property,)
         )
+        
+        # Per-user Dav props currently only apply to a sharee's copy of a calendar
+        isvirt = (yield self.isVirtualShare(request))
+        if isvirt and (self.isShadowableProperty(property.qname()) or (not self.isGlobalProperty(property.qname()))):
+            yield self._preProcessWriteProperty(property, request)
+            ownerPrincipal = (yield self.resourceOwnerPrincipal(request))
+            p = self.deadProperties().set(property, uid=ownerPrincipal.principalUID())
+            returnValue(p)
+ 
+        res = (yield self._writeGlobalProperty(property, request))
+        returnValue(res)
 
-        if property.qname() == (caldav_namespace, "supported-calendar-component-set"):
+    @inlineCallbacks
+    def _preProcessWriteProperty(self, property, request):
+        if property.qname() == caldavxml.SupportedCalendarComponentSet.qname():
             if not self.isPseudoCalendarCollection():
                 raise HTTPError(StatusResponse(
                     responsecode.FORBIDDEN,
@@ -241,7 +366,7 @@
         # server enforces what can be stored, however it need not actually
         # exist so we cannot list it in liveProperties on this resource, since its
         # its presence there means that hasProperty will always return True for it.
-        elif property.qname() == (caldav_namespace, "calendar-timezone"):
+        elif property.qname() == caldavxml.CalendarTimeZone.qname():
             if not self.isCalendarCollection():
                 raise HTTPError(StatusResponse(
                     responsecode.FORBIDDEN,
@@ -254,7 +379,7 @@
                     description="Invalid property"
                 ))
 
-        elif property.qname() == (caldav_namespace, "schedule-calendar-transp"):
+        elif property.qname() == caldavxml.ScheduleCalendarTransp.qname():
             if not self.isCalendarCollection():
                 raise HTTPError(StatusResponse(
                     responsecode.FORBIDDEN,
@@ -262,7 +387,7 @@
                 ))
 
             # For backwards compatibility we need to sync this up with the calendar-free-busy-set on the inbox
-            principal = (yield self.ownerPrincipal(request))
+            principal = (yield self.resourceOwnerPrincipal(request))
             
             # Map owner to their inbox
             inboxURL = principal.scheduleInboxURL()
@@ -271,15 +396,50 @@
                 myurl = (yield self.canonicalURL(request))
                 inbox.processFreeBusyCalendar(myurl, property.children[0] == caldavxml.Opaque())
 
-        result = (yield super(CalDAVResource, self).writeProperty(property, request))
-        returnValue(result)
+    @inlineCallbacks
+    def _writeGlobalProperty(self, property, request):
 
-    def writeDeadProperty(self, property):
-        val = super(CalDAVResource, self).writeDeadProperty(property)
+        yield self._preProcessWriteProperty(property, request)
 
-        return val
+        if property.qname() == davxml.ResourceType.qname():
+            if self.isCalendarCollection():
+                sawShare = [child for child in property.children if child.qname() == (calendarserver_namespace, "shared-owner")]
+                if not (config.Sharing.Enabled and config.Sharing.Calendars.Enabled):
+                    raise HTTPError(StatusResponse(
+                        responsecode.FORBIDDEN,
+                        "Cannot create shared calendars on this server.",
+                    ))
 
+                # Check if adding or removing share
+                shared = (yield self.isShared(request))
+                for child in property.children:
+                    if child.qname() == davxml.Collection.qname():
+                        break
+                else:
+                    raise HTTPError(StatusResponse(
+                        responsecode.FORBIDDEN,
+                        "Protected property %s may not be set." % (property.sname(),)
+                    ))
+                for child in property.children:
+                    if child.qname() == caldavxml.Calendar.qname():
+                        break
+                else:
+                    raise HTTPError(StatusResponse(
+                        responsecode.FORBIDDEN,
+                        "Protected property %s may not be set." % (property.sname(),)
+                    ))
+                sawShare = [child for child in property.children if child.qname() == (calendarserver_namespace, "shared-owner")]
+                if not shared and sawShare:
+                    # Owner is trying to share a collection
+                    yield self.upgradeToShare(request)
+                elif shared and not sawShare:
+                    # Remove share
+                    yield self.downgradeFromShare(request)
+                returnValue(None)
 
+        result = (yield super(CalDAVResource, self).writeProperty(property, request))
+        returnValue(result)
+
     ##
     # ACL
     ##
@@ -287,8 +447,13 @@
     # FIXME: Perhaps this is better done in authorize() instead.
     @inlineCallbacks
     def accessControlList(self, request, *args, **kwargs):
-        acls = (yield super(CalDAVResource, self).accessControlList(request, *args, **kwargs))
 
+        isvirt = (yield self.isVirtualShare(request))
+        if isvirt:
+            acls = self.shareeAccessControlList()
+        else:
+            acls = (yield super(CalDAVResource, self).accessControlList(request, *args, **kwargs))
+
         # Look for private events access classification
         if self.hasDeadProperty(TwistedCalendarAccessProperty):
             access = self.readDeadProperty(TwistedCalendarAccessProperty)
@@ -333,31 +498,57 @@
  
         returnValue(acls)
 
+    @inlineCallbacks
     def owner(self, request):
         """
         Return the DAV:owner property value (MUST be a DAV:href or None).
         """
         
-        def _gotParent(parent):
-            if parent and isinstance(parent, CalDAVResource):
-                return parent.owner(request)
+        isVirt = (yield self.isVirtualShare(request))
+        if isVirt:
+            parent = (yield self.locateParent(request, self._share.hosturl))
+        else:
+            parent = (yield self.locateParent(request, request.urlForResource(self)))
+        if parent and isinstance(parent, CalDAVResource):
+            result = (yield parent.owner(request))
+            returnValue(result)
+        else:
+            returnValue(None)
 
-        d = self.locateParent(request, request.urlForResource(self))
-        d.addCallback(_gotParent)
-        return d
-
+    @inlineCallbacks
     def ownerPrincipal(self, request):
         """
         Return the DAV:owner property value (MUST be a DAV:href or None).
         """
-        def _gotParent(parent):
-            if parent and isinstance(parent, CalDAVResource):
-                return parent.ownerPrincipal(request)
+        isVirt = (yield self.isVirtualShare(request))
+        if isVirt:
+            parent = (yield self.locateParent(request, self._share.hosturl))
+        else:
+            parent = (yield self.locateParent(request, request.urlForResource(self)))
+        if parent and isinstance(parent, CalDAVResource):
+            result = (yield parent.ownerPrincipal(request))
+            returnValue(result)
+        else:
+            returnValue(None)
 
-        d = self.locateParent(request, request.urlForResource(self))
-        d.addCallback(_gotParent)
-        return d
+    @inlineCallbacks
+    def resourceOwnerPrincipal(self, request):
+        """
+        This is the owner of the resource based on the URI used to access it. For a shared
+        collection it will be the sharee, otherwise it will be the regular the ownerPrincipal.
+        """
 
+        isVirt = (yield self.isVirtualShare(request))
+        if isVirt:
+            returnValue(self._shareePrincipal)
+        else:
+            parent = (yield self.locateParent(request, request.urlForResource(self)))
+        if parent and isinstance(parent, CalDAVResource):
+            result = (yield parent.resourceOwnerPrincipal(request))
+            returnValue(result)
+        else:
+            returnValue(None)
+
     def isOwner(self, request, adminprincipals=False, readprincipals=False):
         """
         Determine whether the DAV:owner of this resource matches the currently authorized principal
@@ -514,7 +705,7 @@
         """
         
         # For backwards compatibility we need to sync this up with the calendar-free-busy-set on the inbox
-        principal = (yield self.ownerPrincipal(request))
+        principal = (yield self.resourceOwnerPrincipal(request))
         inboxURL = principal.scheduleInboxURL()
         if inboxURL:
             inbox = (yield request.locateResource(inboxURL))
@@ -527,7 +718,7 @@
         """
         
         # For backwards compatibility we need to sync this up with the calendar-free-busy-set on the inbox
-        principal = (yield self.ownerPrincipal(request))
+        principal = (yield self.resourceOwnerPrincipal(request))
         inboxURL = principal.scheduleInboxURL()
         if inboxURL:
             (_ignore_scheme, _ignore_host, destination_path, _ignore_query, _ignore_fragment) = urlsplit(normalizeURL(destination_uri))
@@ -556,7 +747,7 @@
         assert self.isCalendarCollection()
         
         # Not allowed to delete the default calendar
-        principal = (yield self.ownerPrincipal(request))
+        principal = (yield self.resourceOwnerPrincipal(request))
         inboxURL = principal.scheduleInboxURL()
         if inboxURL:
             inbox = (yield request.locateResource(inboxURL))
@@ -588,6 +779,19 @@
         except ValueError:
             return None
 
+    @inlineCallbacks
+    def iCalendarForUser(self, request, name=None):
+        
+        caldata = self.iCalendar(name)
+        
+        accessUID = (yield self.resourceOwnerPrincipal(request))
+        if accessUID is None:
+            accessUID = ""
+        else:
+            accessUID = accessUID.principalUID()
+
+        returnValue(PerUserDataFilter(accessUID).filter(caldata))
+
     def iCalendarRolledup(self, request):
         """
         See L{ICalDAVResource.iCalendarRolledup}.
@@ -611,14 +815,6 @@
         """
         return str(self.iCalendar(name))
 
-    def iCalendarXML(self, name=None):
-        """
-        See L{ICalDAVResource.iCalendarXML}.
-        This implementation returns an XML element constructed from the object
-        returned by L{iCalendar} when given the same arguments.
-        """
-        return caldavxml.CalendarData.fromCalendar(self.iCalendar(name))
-
     def iCalendarAddressDoNormalization(self, ical):
         """
         Normalize calendar user addresses in the supplied iCalendar object into their
@@ -770,8 +966,11 @@
             lastpath = path.split("/")[-1]
             
             parent = (yield request.locateResource(parentForURL(myurl)))
-            canonical_parent = (yield parent.canonicalURL(request))
-            self._canonical_url = joinURL(canonical_parent, lastpath)
+            if parent:
+                canonical_parent = (yield parent.canonicalURL(request))
+                self._canonical_url = joinURL(canonical_parent, lastpath)
+            else:
+                self._canonical_url = myurl
 
         returnValue(self._canonical_url)
 
@@ -789,7 +988,7 @@
         """
         Quota root only ever set on calendar homes.
         """
-        return None
+        return None 
 
 class CalendarPrincipalCollectionResource (DAVPrincipalCollectionResource, CalDAVResource):
     """
@@ -874,6 +1073,14 @@
             clz.liveProperties = tuple([p for p in clz.liveProperties if p != qname])
 
     @classmethod
+    def enableSharing(clz, enable):
+        qname = (calendarserver_namespace, "notification-URL" )
+        if enable and qname not in clz.liveProperties:
+            clz.liveProperties += (qname,)
+        elif not enable and qname in clz.liveProperties:
+            clz.liveProperties = tuple([p for p in clz.liveProperties if p != qname])
+
+    @classmethod
     def enableAddressBooks(clz, enable):
         qname = (carddav_namespace, "addressbook-home-set" )
         if enable and qname not in clz.liveProperties:
@@ -929,6 +1136,13 @@
                 else:
                     returnValue(customxml.DropBoxHomeURL(davxml.HRef(url)))
 
+            elif name == "notification-URL" and config.Sharing.Enabled:
+                url = yield self.notificationURL()
+                if url is None:
+                    returnValue(None)
+                else:
+                    returnValue(customxml.NotificationURL(davxml.HRef(url)))
+
             elif name == "calendar-proxy-read-for":
                 results = (yield self.proxyFor(False))
                 returnValue(customxml.CalendarProxyReadFor(
@@ -1026,6 +1240,13 @@
         else:
             return None
 
+    def notificationURL(self, request=None):
+        if self.hasDeadProperty((calendarserver_namespace, "notification-URL")):
+            notification = self.readDeadProperty((calendarserver_namespace, "notification-URL"))
+            return succeed(str(notification.children[0]))
+        else:
+            return succeed(None)
+
     def addressBookHomeURLs(self):
         if self.hasDeadProperty((carddav_namespace, "addressbook-home-set")):
             home_set = self.readDeadProperty((carddav_namespace, "addressbook-home-set"))
@@ -1067,8 +1288,8 @@
 
         self.parent = parent
 
-    def resourceType(self):
-        return davxml.ResourceType.searchaddressbook #@UndefinedVariable
+    def resourceType(self, request):
+        return succeed(davxml.ResourceType.searchaddressbook)
 
     def renderHTTP(self, request):
         return RedirectResponse(request.unparseURL(path="/directory/"))
@@ -1088,8 +1309,8 @@
 
         self.parent = parent
 
-    def resourceType(self):
-        return davxml.ResourceType.searchalladdressbook #@UndefinedVariable
+    def resourceType(self, request):
+        return succeed(davxml.ResourceType.searchalladdressbook)
 
     def renderHTTP(self, request):
         

Modified: CalendarServer/trunk/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/schedule.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/schedule.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -26,7 +26,7 @@
 
 from twext.web2.dav.http import ErrorResponse
 
-from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
 from twext.web2 import responsecode
 from twext.web2.dav import davxml
 from twext.web2.dav.util import joinURL, normalizeURL
@@ -87,8 +87,8 @@
         (caldav_namespace, "schedule-default-calendar-URL"),
     )
 
-    def resourceType(self):
-        return davxml.ResourceType.scheduleInbox
+    def resourceType(self, request):
+        return succeed(davxml.ResourceType.scheduleInbox)
 
     def defaultAccessControlList(self):
         
@@ -253,8 +253,8 @@
         else:
             return super(ScheduleOutboxResource, self).defaultAccessControlList()
 
-    def resourceType(self):
-        return davxml.ResourceType.scheduleOutbox
+    def resourceType(self, request):
+        return succeed(davxml.ResourceType.scheduleOutbox)
 
     @inlineCallbacks
     def http_POST(self, request):
@@ -310,8 +310,8 @@
             ),
         )
 
-    def resourceType(self):
-        return davxml.ResourceType.ischeduleinbox
+    def resourceType(self, request):
+        return succeed(davxml.ResourceType.ischeduleinbox)
 
     def isCollection(self):
         return False

Modified: CalendarServer/trunk/twistedcaldav/scheduling/caldav.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -249,6 +249,8 @@
         # Now process free-busy set calendars
         matchtotal = 0
         for calendarResourceURL in fbset:
+            if not calendarResourceURL.endswith('/'):
+                calendarResourceURL += '/'
             calendarResource = (yield self.scheduler.request.locateResource(calendarResourceURL))
             if calendarResource is None or not calendarResource.exists() or not isCalendarCollectionResource(calendarResource):
                 # We will ignore missing calendars. If the recipient has failed to

Modified: CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/implicit.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -71,7 +71,8 @@
         self.internal_request = internal_request
 
         existing_resource = resource.exists()
-        existing_type = "schedule" if self.checkSchedulingObjectResource(resource) else "calendar"
+        is_scheduling_object = (yield self.checkSchedulingObjectResource(resource))
+        existing_type = "schedule" if is_scheduling_object else "calendar"
         new_type = "schedule" if (yield self.checkImplicitState()) else "calendar"
 
         if existing_type == "calendar":
@@ -88,7 +89,7 @@
             # Also make sure that we return the new calendar being written rather than the old one
             # when the implicit action is executed
             self.return_calendar = calendar
-            self.calendar = resource.iCalendar()
+            self.calendar = (yield resource.iCalendarForUser(request))
             yield self.checkImplicitState()
         
         # Attendees are not allowed to overwrite one type with another
@@ -108,8 +109,8 @@
         new_type = "schedule" if (yield self.checkImplicitState()) else "calendar"
 
         dest_exists = destresource.exists()
-        dest_is_implicit = self.checkSchedulingObjectResource(destresource)
-        src_is_implicit = self.checkSchedulingObjectResource(srcresource) or new_type == "schedule"
+        dest_is_implicit = (yield self.checkSchedulingObjectResource(destresource))
+        src_is_implicit = (yield self.checkSchedulingObjectResource(srcresource)) or new_type == "schedule"
 
         if srccal and destcal:
             if src_is_implicit and dest_exists or dest_is_implicit:
@@ -138,8 +139,8 @@
 
         new_type = "schedule" if (yield self.checkImplicitState()) else "calendar"
 
-        dest_is_implicit = self.checkSchedulingObjectResource(destresource)
-        src_is_implicit = self.checkSchedulingObjectResource(srcresource) or new_type == "schedule"
+        dest_is_implicit = (yield self.checkSchedulingObjectResource(destresource))
+        src_is_implicit = (yield self.checkSchedulingObjectResource(srcresource)) or new_type == "schedule"
 
         if srccal and destcal:
             if src_is_implicit or dest_is_implicit:
@@ -167,11 +168,13 @@
 
         yield self.checkImplicitState()
 
-        resource_type = "schedule" if self.checkSchedulingObjectResource(resource) else "calendar"
+        is_scheduling_object = (yield self.checkSchedulingObjectResource(resource))
+        resource_type = "schedule" if is_scheduling_object else "calendar"
         self.action = "remove" if resource_type == "schedule" else "none"
 
         returnValue((self.action != "none", False,))
 
+    @inlineCallbacks
     def checkSchedulingObjectResource(self, resource):
         
         if resource and resource.exists():
@@ -180,24 +183,24 @@
             except HTTPError:
                 implicit = None
             if implicit is not None:
-                return implicit != "false"
+                returnValue(implicit != "false")
             else:
-                calendar = resource.iCalendar()
+                calendar = (yield resource.iCalendarForUser(self.request))
                 # Get the ORGANIZER and verify it is the same for all components
                 try:
                     organizer = calendar.validOrganizerForScheduling()
                 except ValueError:
                     # We have different ORGANIZERs in the same iCalendar object - this is an error
-                    return False
+                    returnValue(False)
                 organizerPrincipal = resource.principalForCalendarUserAddress(organizer) if organizer else None
                 resource.writeDeadProperty(TwistedSchedulingObjectResource("true" if organizerPrincipal != None else "false"))
                 log.debug("Implicit - checked scheduling object resource state for UID: '%s', result: %s" % (
                     calendar.resourceUID(),
                     "true" if organizerPrincipal != None else "false",
                 ))
-                return organizerPrincipal != None
+                returnValue(organizerPrincipal != None)
 
-        return False
+        returnValue(False)
         
     @inlineCallbacks
     def checkImplicitState(self):
@@ -379,7 +382,7 @@
             returnValue(None)
 
         # Get owner's calendar-home
-        calendar_owner_principal = (yield self.resource.ownerPrincipal(self.request))
+        calendar_owner_principal = (yield self.resource.resourceOwnerPrincipal(self.request))
         calendar_home = calendar_owner_principal.calendarHome()
         
         check_parent_uri = parentForURL(check_uri)[:-1] if check_uri else None
@@ -397,7 +400,8 @@
                 child = (yield self.request.locateResource(joinURL(collection_uri, rname)))
                 if child == check_resource:
                     returnValue(True)
-                matched_type = "schedule" if self.checkSchedulingObjectResource(child) else "calendar"
+                is_scheduling_object = (yield self.checkSchedulingObjectResource(child))
+                matched_type = "schedule" if is_scheduling_object else "calendar"
                 if (
                     collection_uri != check_parent_uri and
                     (type == "schedule" or matched_type == "schedule")
@@ -494,7 +498,7 @@
         elif self.action == "modify":
 
             # Read in existing data
-            self.oldcalendar = self.resource.iCalendar()
+            self.oldcalendar = (yield self.resource.iCalendarForUser(self.request))
             
             # Significant change
             no_change, self.changed_rids, reinvites, recurrence_reschedule = self.isOrganizerChangeInsignificant()
@@ -780,7 +784,7 @@
         else:
             # Make sure ORGANIZER is not changed
             if self.resource.exists():
-                self.oldcalendar = self.resource.iCalendar()
+                self.oldcalendar = (yield self.resource.iCalendarForUser(self.request))
                 oldOrganizer = self.oldcalendar.getOrganizer()
                 newOrganizer = self.calendar.getOrganizer()
                 if oldOrganizer != newOrganizer:
@@ -883,7 +887,7 @@
         self.organizer_calendar = None
         calendar_resource, _ignore_name, _ignore_collection, _ignore_uri = (yield getCalendarObjectForPrincipals(self.request, self.organizerPrincipal, self.uid))
         if calendar_resource:
-            self.organizer_calendar = calendar_resource.iCalendar()
+            self.organizer_calendar = (yield calendar_resource.iCalendarForUser(self.request))
         elif isinstance(self.organizerAddress, PartitionedCalendarUser):
             # For partitioning where the organizer is on a different node, we will assume that the attendee's copy
             # of the event is up to date and "authoritative". So we pretend that is the organizer copy

Modified: CalendarServer/trunk/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/itip.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/scheduling/itip.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -667,12 +667,15 @@
         Remove properties and parameters that should not be sent in an iTIP message
         """
 
+        # All X- components go away
+        itip.removeXComponents()
+
         # Alarms
         itip.removeAlarms()
 
         # Top-level properties - remove all X-
         itip.removeXProperties(do_subcomponents=False)
-                
+
         # Component properties - remove all X- except for those specified
         if not reply:
             # Organizer properties that need to go to the Attendees

Modified: CalendarServer/trunk/twistedcaldav/scheduling/processing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/processing.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/scheduling/processing.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -18,7 +18,7 @@
 import time
 from hashlib import md5
 
-from vobject.icalendar import utc
+from vobject.icalendar import dateTimeToString, utc
 
 from twext.python.log import Logger
 
@@ -136,7 +136,7 @@
         self.recipient_calendar_name = None
         calendar_resource, resource_name, calendar_collection, calendar_collection_uri = (yield getCalendarObjectForPrincipals(self.request, self.recipient.principal, self.uid))
         if calendar_resource:
-            self.recipient_calendar = calendar_resource.iCalendar()
+            self.recipient_calendar = (yield calendar_resource.iCalendarForUser(self.request))
             self.recipient_calendar_collection = calendar_collection
             self.recipient_calendar_collection_uri = calendar_collection_uri
             self.recipient_calendar_name = resource_name
@@ -504,9 +504,10 @@
                                 dt = dt.replace(tzinfo=tzinfo).astimezone(utc)
                             return dt
                         
-                        tr = caldavxml.TimeRange(start="20000101", end="20000101")
-                        tr.start = makeTimedUTC(instance.start)
-                        tr.end = makeTimedUTC(instance.end)
+                        tr = caldavxml.TimeRange(
+                            start=dateTimeToString(makeTimedUTC(instance.start)),
+                            end=dateTimeToString(makeTimedUTC(instance.end)),
+                        )
 
                         yield report_common.generateFreeBusyInfo(self.request, testcal, fbinfo, tr, 0, uid, servertoserver=True)
                         
@@ -708,7 +709,7 @@
         calendar_resource, _ignore_name, _ignore_collection, _ignore_uri = (yield getCalendarObjectForPrincipals(self.request, self.originator.principal, self.uid))
         if not calendar_resource:
             raise ImplicitProcessorException("5.1;Service unavailable")
-        originator_calendar = calendar_resource.iCalendar()
+        originator_calendar = (yield calendar_resource.iCalendarForUser(self.request))
 
         # Get attendee's view of that
         originator_calendar.attendeesView((self.recipient.cuaddr,))

Modified: CalendarServer/trunk/twistedcaldav/scheduling/utils.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/utils.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/scheduling/utils.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -16,6 +16,7 @@
 
 from twisted.internet.defer import inlineCallbacks, succeed, returnValue
 from twistedcaldav.method import report_common
+from twext.web2.dav.util import joinURL
 
 @inlineCallbacks
 def getCalendarObjectForPrincipals(request, principal, uid):
@@ -43,7 +44,10 @@
         def queryCalendarCollection(collection, uri):
             rname = collection.index().resourceNameForUID(uid)
             if rname:
-                result["resource"] = collection.getChild(rname)
+                resource = collection.getChild(rname)
+                request._rememberResource(resource, joinURL(uri, rname))
+
+                result["resource"] = resource
                 result["resource_name"] = rname
                 result["calendar_collection"] = collection
                 result["calendar_collection_uri"] = uri

Copied: CalendarServer/trunk/twistedcaldav/sharedcalendar.py (from rev 5440, CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/sharedcalendar.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/sharedcalendar.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/sharedcalendar.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -0,0 +1,85 @@
+##
+# Copyright (c) 2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.python.log import LoggingMixIn
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+
+from twistedcaldav.extensions import DAVResource
+from twistedcaldav.resource import CalDAVComplianceMixIn
+from twistedcaldav.sharing import SharedCollectionMixin
+
+__all__ = [
+    "SharedCalendarResource",
+]
+
+"""
+Sharing behavior
+"""
+
+class SharedCalendarResource(CalDAVComplianceMixIn, SharedCollectionMixin, DAVResource, LoggingMixIn):
+    """
+    This is similar to a WrapperResource except that we locate our shared calendar resource dynamically. 
+    """
+    
+    def __init__(self, parent, share):
+        self.parent = parent
+        self.share = share
+        super(SharedCalendarResource, self).__init__(self.parent.principalCollections())
+
+    @inlineCallbacks
+    def hostedResource(self, request):
+        
+        if not hasattr(self, "_hostedResource"):
+            self._hostedResource = (yield request.locateResource(self.share.hosturl))
+            ownerPrincipal = (yield self.parent.ownerPrincipal(request))
+            self._hostedResource.setVirtualShare(ownerPrincipal, self.share)
+        returnValue(self._hostedResource)
+
+    def isCollection(self):
+        return True
+
+    def locateChild(self, request, segments):
+        
+        def _defer(result):
+            return (result, segments)
+        d = self.hostedResource(request)
+        d.addCallback(_defer)
+        return d
+
+    def renderHTTP(self, request):
+        return self.hostedResource(request)
+
+    def getChild(self, name):
+        return self._hostedResource.getChild(name)
+
+    @inlineCallbacks
+    def hasProperty(self, property, request):
+        hosted = (yield self.hostedResource(request))
+        result = (yield hosted.hasProperty(property, request))
+        returnValue(result)
+
+    @inlineCallbacks
+    def readProperty(self, property, request):
+        hosted = (yield self.hostedResource(request))
+        result = (yield hosted.readProperty(property, request))
+        returnValue(result)
+
+    @inlineCallbacks
+    def writeProperty(self, property, request):
+        hosted = (yield self.hostedResource(request))
+        result = (yield hosted.writeProperty(property, request))
+        returnValue(result)

Copied: CalendarServer/trunk/twistedcaldav/sharing.py (from rev 5440, CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/sharing.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/sharing.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/sharing.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -0,0 +1,1091 @@
+##
+# Copyright (c) 2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+__all__ = [
+    "SharedCollectionMixin",
+]
+
+from twext.python.log import LoggingMixIn
+from twext.web2 import responsecode
+from twext.web2.dav import davxml
+from twext.web2.dav.http import ErrorResponse, MultiStatusResponse
+from twext.web2.dav.resource import TwistedACLInheritable
+from twext.web2.dav.util import allDataFromStream, joinURL
+from twext.web2.http import HTTPError, Response, StatusResponse, XMLResponse
+from twisted.internet.defer import succeed, inlineCallbacks, DeferredList,\
+    returnValue
+from twistedcaldav import customxml, caldavxml
+from twistedcaldav.config import config
+from twistedcaldav.customxml import SharedCalendar
+from twistedcaldav.sql import AbstractSQLDatabase, db_prefix
+from uuid import uuid4
+from vobject.icalendar import dateTimeToString, utc
+import datetime
+import os
+import types
+
+"""
+Sharing behavior
+"""
+
+class SharedCollectionMixin(object):
+    
+    def invitesDB(self):
+        
+        if not hasattr(self, "_invitesDB"):
+            self._invitesDB = InvitesDatabase(self)
+        return self._invitesDB
+
+    def inviteProperty(self, request):
+        
+        # Build the CS:invite property from our DB
+        def sharedOK(isShared):
+            if config.Sharing.Enabled and isShared:
+                self.validateInvites()
+                return customxml.Invite(
+                    *[record.makePropertyElement() for record in self.invitesDB().allRecords()]
+                )
+            else:
+                return None
+        return self.isShared(request).addCallback(sharedOK)
+
+    @inlineCallbacks
+    def upgradeToShare(self, request):
+        """ Upgrade this collection to a shared state """
+        
+        # For calendars we only allow upgrades is shared-scheduling is on
+        if request.method not in ("MKCALENDAR", "MKCOL") and self.isCalendarCollection() and \
+            not config.Sharing.Calendars.AllowScheduling and len(self.listChildren()) != 0:
+            raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Cannot upgrade to shared calendar"))
+
+        # Change resourcetype
+        rtype = (yield self.resourceType(request))
+        rtype = davxml.ResourceType(*(rtype.children + (customxml.SharedOwner(),)))
+        self.writeDeadProperty(rtype)
+        
+        # Create invites database
+        self.invitesDB().create()
+
+        returnValue(True)
+    
+    @inlineCallbacks
+    def downgradeFromShare(self, request):
+        
+        # Change resource type (note this might be called after deleting a resource
+        # so we have to cope with that)
+        rtype = (yield self.resourceType(request))
+        rtype = davxml.ResourceType(*([child for child in rtype.children if child != customxml.SharedOwner()]))
+        self.writeDeadProperty(rtype)
+        
+        # Remove all invitees
+        records = self.invitesDB().allRecords()
+        yield self.uninviteUserToShare([record.userid for record in records], None, request)
+
+        # Remove invites database
+        self.invitesDB().remove()
+        delattr(self, "_invitesDB")
+    
+        returnValue(True)
+
+    def removeUserFromInvite(self, userid, request):
+        """ Remove a user from this shared calendar """
+        self.invitesDB().removeRecordForUserID(userid)            
+
+        return succeed(True)
+
+    @inlineCallbacks
+    def changeUserInviteState(self, request, inviteUID, userid, state, summary=None):
+        
+        shared = (yield self.isShared(request))
+        if not shared:
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (customxml.calendarserver_namespace, "valid-request"),
+                "invalid share",
+            ))
+            
+        record = self.invitesDB().recordForInviteUID(inviteUID)
+        if record is None or record.userid != userid:
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (customxml.calendarserver_namespace, "valid-request"),
+                "invalid invitation uid: %s" % (inviteUID,),
+            ))
+        
+        # Only certain states are sharer controlled
+        if record.state in ("NEEDS-ACTION", "ACCEPTED", "DECLINED",):
+            record.state = state
+            if summary is not None:
+                record.summary = summary
+            self.invitesDB().addOrUpdateRecord(record)
+
+    def isShared(self, request):
+        """ Return True if this is an owner shared calendar collection """
+        return succeed(self.isSpecialCollection(customxml.SharedOwner))
+
+    def setVirtualShare(self, shareePrincipal, share):
+        self._isVirtualShare = True
+        self._shareePrincipal = shareePrincipal
+        self._share = share
+
+    def isVirtualShare(self, request):
+        """ Return True if this is a shared calendar collection """
+        return succeed(hasattr(self, "_isVirtualShare"))
+
+    def removeVirtualShare(self, request):
+        """ Return True if this is a shared calendar collection """
+        
+        # Remove from sharee's calendar home
+        shareeHome = self._shareePrincipal.calendarHome()
+        return shareeHome.removeShare(request, self._share)
+
+    @inlineCallbacks
+    def resourceType(self, request):
+        
+        rtype = (yield super(SharedCollectionMixin, self).resourceType(request))
+        isVirt = (yield self.isVirtualShare(request))
+        if isVirt:
+            rtype = davxml.ResourceType(
+                *(
+                    tuple([child for child in rtype.children if child.qname() != customxml.SharedOwner.qname()]) +
+                    (customxml.Shared(),)
+                )
+            )
+        returnValue(rtype)
+        
+    def sharedResourceType(self):
+        """
+        Return the DAV:resourcetype stripped of any shared elements.
+        """
+        
+        if self.isCalendarCollection():
+            return "calendar"
+        elif self.isAddressBookCollection():
+            return "addressbook"
+        else:
+            return ""
+
+    def shareeAccessControlList(self):
+
+        assert self._isVirtualShare, "Only call this fort a virtual share"
+
+        # Get the invite for this sharee
+        invite = self.invitesDB().recordForInviteUID(self._share.inviteuid)
+        if invite is None:
+            return davxml.ACL()
+        
+        userprivs = [
+        ]
+        if invite.access in ("read-only", "read-write", "read-write-schedule",):
+            userprivs.append(davxml.Privilege(davxml.Read()))
+            userprivs.append(davxml.Privilege(davxml.ReadACL()))
+            userprivs.append(davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()))
+        if invite.access in ("read-only",):
+            userprivs.append(davxml.Privilege(davxml.WriteProperties()))
+        if invite.access in ("read-write", "read-write-schedule",):
+            userprivs.append(davxml.Privilege(davxml.Write()))
+        proxyprivs = list(userprivs)
+        proxyprivs.remove(davxml.Privilege(davxml.ReadACL()))
+
+        aces = (
+            # Inheritable specific access for the resource's associated principal.
+            davxml.ACE(
+                davxml.Principal(davxml.HRef(self._shareePrincipal.principalURL())),
+                davxml.Grant(*userprivs),
+                davxml.Protected(),
+                TwistedACLInheritable(),
+            ),
+            # Inheritable CALDAV:read-free-busy access for authenticated users.
+            davxml.ACE(
+                davxml.Principal(davxml.Authenticated()),
+                davxml.Grant(davxml.Privilege(caldavxml.ReadFreeBusy())),
+                TwistedACLInheritable(),
+            ),
+        )
+
+        # Give read access to config.ReadPrincipals
+        aces += config.ReadACEs
+
+        # Give all access to config.AdminPrincipals
+        aces += config.AdminACEs
+        
+        if config.EnableProxyPrincipals:
+            aces += (
+                # DAV:read/DAV:read-current-user-privilege-set access for this principal's calendar-proxy-read users.
+                davxml.ACE(
+                    davxml.Principal(davxml.HRef(joinURL(self._shareePrincipal.principalURL(), "calendar-proxy-read/"))),
+                    davxml.Grant(
+                        davxml.Privilege(davxml.Read()),
+                        davxml.Privilege(davxml.ReadCurrentUserPrivilegeSet()),
+                    ),
+                    davxml.Protected(),
+                    TwistedACLInheritable(),
+                ),
+                # DAV:read/DAV:read-current-user-privilege-set/DAV:write access for this principal's calendar-proxy-write users.
+                davxml.ACE(
+                    davxml.Principal(davxml.HRef(joinURL(self._shareePrincipal.principalURL(), "calendar-proxy-write/"))),
+                    davxml.Grant(
+                        davxml.Privilege(*proxyprivs),
+                    ),
+                    davxml.Protected(),
+                    TwistedACLInheritable(),
+                ),
+            )
+
+        return davxml.ACL(*aces)
+
+    def validUserIDForShare(self, userid):
+        """
+        Test the user id to see if it is a valid identifier for sharing and return a "normalized"
+        form for our own use (e.g. convert mailto: to urn:uuid).
+
+        @param userid: the userid to test
+        @type userid: C{str}
+        
+        @return: C{str} of normalized userid or C{None} if
+            userid is not allowed.
+        """
+        
+        # First try to resolve as a principal
+        principal = self.principalForCalendarUserAddress(userid)
+        if principal:
+            return principal.principalURL()
+        
+        # TODO: we do not support external users right now so this is being hard-coded
+        # off in spite of the config option.
+        #elif config.Sharing.AllowExternalUsers:
+        #    return userid
+        else:
+            return None
+
+    def validateInvites(self):
+        """
+        Make sure each userid in an invite is valid - if not re-write status.
+        """
+        
+        records = self.invitesDB().allRecords()
+        for record in records:
+            if self.validUserIDForShare(record.userid) is None and record.state != "INVALID":
+                record.state = "INVALID"
+                self.invitesDB().addOrUpdateRecord(record)
+                
+    def getInviteUsers(self, request):
+        return succeed(True)
+
+    def sendNotificationOnChange(self, icalendarComponent, request, state="added"):
+        """ Possibly send a push and or email notification on a change to a resource in a shared collection """
+        return succeed(True)
+
+    def inviteUserToShare(self, userid, ace, summary, request, commonName="", shareName="", add=True):
+        """ Send out in invite first, and then add this user to the share list
+            @param userid: 
+            @param ace: Must be one of customxml.ReadWriteAccess or customxml.ReadAccess
+        """
+        
+        # Check for valid userid first
+        userid = self.validUserIDForShare(userid)
+        if userid is None:
+            return succeed(False)
+
+        # TODO: Check if this collection is shared, and error out if it isn't
+        if type(userid) is not list:
+            userid = [userid]
+        if type(commonName) is not list:
+            commonName = [commonName]
+        if type(shareName) is not list:
+            shareName = [shareName]
+            
+        dl = [self.inviteSingleUserToShare(user, ace, summary, request, cn=cn, sn=sn) for user, cn, sn in zip(userid, commonName, shareName)]
+        return DeferredList(dl).addCallback(lambda _:True)
+
+    def uninviteUserToShare(self, userid, ace, request):
+        """ Send out in uninvite first, and then remove this user from the share list."""
+        
+        # Do not validate the userid - we want to allow invalid users to be removed because they
+        # may have been valid when added, but no longer valid now. Clients should be able to clear out
+        # anything known to be invalid.
+
+        # TODO: Check if this collection is shared, and error out if it isn't
+        if type(userid) is not list:
+            userid = [userid]
+        return DeferredList([self.uninviteSingleUserFromShare(user, ace, request) for user in userid]).addCallback(lambda _:True)
+
+    def inviteUserUpdateToShare(self, userid, aceOLD, aceNEW, summary, request, commonName="", shareName=""):
+
+        # Check for valid userid first
+        userid = self.validUserIDForShare(userid)
+        if userid is None:
+            return succeed(False)
+
+        if type(userid) is not list:
+            userid = [userid]
+        if type(commonName) is not list:
+            commonName = [commonName]
+        if type(shareName) is not list:
+            shareName = [shareName]
+        dl = [self.inviteSingleUserUpdateToShare(user, aceOLD, aceNEW, summary, request, commonName=cn, shareName=sn) for user, cn, sn in zip(userid, commonName, shareName)]
+        return DeferredList(dl).addCallback(lambda _:True)
+
+    @inlineCallbacks
+    def inviteSingleUserToShare(self, userid, ace, summary, request, cn="", sn=""):
+        
+        # Look for existing invite and update its fields or create new one
+        record = self.invitesDB().recordForUserID(userid)
+        if record:
+            record.access = inviteAccessMapFromXML[type(ace)]
+            record.summary = summary
+        else:
+            record = Invite(str(uuid4()), userid, inviteAccessMapFromXML[type(ace)], "NEEDS-ACTION", summary)
+        
+        # Send invite
+        yield self.sendInvite(record, request)
+        
+        # Add to database
+        self.invitesDB().addOrUpdateRecord(record)
+        
+        returnValue(True)            
+
+    @inlineCallbacks
+    def uninviteSingleUserFromShare(self, userid, aces, request):
+        
+        newuserid = self.validUserIDForShare(userid)
+        if newuserid:
+            userid = newuserid
+
+        # Cancel invites
+        record = self.invitesDB().recordForUserID(userid)
+        
+        # Remove any shared calendar
+        sharee = self.principalForCalendarUserAddress(record.userid)
+        if sharee is None:
+            raise ValueError("sharee is None but userid was valid before")
+        shareeHome = sharee.calendarHome()
+        yield shareeHome.removeShareByUID(request, record.inviteuid)
+
+        # If current user state is accepted then we send an invite with the new state, otherwise
+        # we cancel any existing invites for the user
+        if record and record.state != "ACCEPTED":
+            yield self.removeInvite(record, request)
+        elif record:
+            record.state = "DELETED"
+            yield self.sendInvite(record, request)
+
+        # Remove from database
+        self.invitesDB().removeRecordForUserID(userid)
+        
+        returnValue(True)            
+
+    def inviteSingleUserUpdateToShare(self, userid, acesOLD, aceNEW, summary, request, commonName="", shareName=""):
+        
+        # Just update existing
+        return self.inviteSingleUserToShare(userid, aceNEW, summary, request, commonName, shareName) 
+
+    @inlineCallbacks
+    def sendInvite(self, record, request):
+        
+        owner = (yield self.ownerPrincipal(request))
+        owner = owner.principalURL()
+        hosturl = (yield self.canonicalURL(request))
+
+        # Locate notifications collection for user
+        sharee = self.principalForCalendarUserAddress(record.userid)
+        if sharee is None:
+            raise ValueError("sharee is None but userid was valid before")
+        notifications = (yield request.locateResource(sharee.notificationURL()))
+        
+        # Look for existing notification
+        oldnotification = (yield notifications.getNotifictionMessageByUID(request, record.inviteuid))
+        if oldnotification:
+            # TODO: rollup changes?
+            pass
+        
+        # Generate invite XML
+        typeAttr = {'shared-type':self.sharedResourceType()}
+        xmltype = customxml.InviteNotification(**typeAttr)
+        xmldata = customxml.Notification(
+            customxml.DTStamp.fromString(dateTimeToString(datetime.datetime.now(tz=utc))),
+            customxml.InviteNotification(
+                customxml.UID.fromString(record.inviteuid),
+                davxml.HRef.fromString(record.userid),
+                inviteStatusMapToXML[record.state](),
+                customxml.InviteAccess(inviteAccessMapToXML[record.access]()),
+                customxml.HostURL(
+                    davxml.HRef.fromString(hosturl),
+                ),
+                customxml.Organizer(
+                    davxml.HRef.fromString(owner),
+                ),
+                customxml.InviteSummary.fromString(record.summary),
+                **typeAttr
+            ),
+        ).toxml()
+        
+        # Add to collections
+        yield notifications.addNotification(request, record.inviteuid, xmltype, xmldata)
+
+    @inlineCallbacks
+    def removeInvite(self, record, request):
+        
+        # Locate notifications collection for user
+        sharee = self.principalForCalendarUserAddress(record.userid)
+        if sharee is None:
+            raise ValueError("sharee is None but userid was valid before")
+        notifications = (yield request.locateResource(sharee.notificationURL()))
+        
+        # Add to collections
+        yield notifications.deleteNotifictionMessageByUID(request, record.inviteuid)
+
+    def xmlPOSTNoAuth(self, encoding, request):
+        def _handleErrorResponse(error):
+            if isinstance(error.value, HTTPError) and hasattr(error.value, "response"):
+                return error.value.response
+            return Response(code=responsecode.BAD_REQUEST)
+
+        def _handleInvite(invitedoc):
+            def _handleInviteSet(inviteset):
+                userid = None
+                access = None
+                summary = None
+                for item in inviteset.children:
+                    if isinstance(item, davxml.HRef):
+                        userid = str(item)
+                        continue
+                    if isinstance(item, customxml.InviteSummary):
+                        summary = str(item)
+                        continue
+                    if isinstance(item, customxml.ReadAccess) or isinstance(item, customxml.ReadWriteAccess):
+                        access = item
+                        continue
+                if userid and access and summary:
+                    return (userid, access, summary)
+                else:
+                    if userid is None:
+                        raise HTTPError(ErrorResponse(
+                            responsecode.FORBIDDEN,
+                            (customxml.calendarserver_namespace, "valid-request"),
+                            "missing href: %s" % (inviteset,),
+                        ))
+                    if access is None:
+                        raise HTTPError(ErrorResponse(
+                            responsecode.FORBIDDEN,
+                            (customxml.calendarserver_namespace, "valid-request"),
+                            "missing access: %s" % (inviteset,),
+                        ))
+                    if summary is None:
+                        raise HTTPError(ErrorResponse(
+                            responsecode.FORBIDDEN,
+                            (customxml.calendarserver_namespace, "valid-request"),
+                            "missing summary: %s" % (inviteset,),
+                        ))
+
+            def _handleInviteRemove(inviteremove):
+                userid = None
+                access = []
+                for item in inviteremove.children:
+                    if isinstance(item, davxml.HRef):
+                        userid = str(item)
+                        continue
+                    if isinstance(item, customxml.ReadAccess) or isinstance(item, customxml.ReadWriteAccess):
+                        access.append(item)
+                        continue
+                if userid is None:
+                    raise HTTPError(ErrorResponse(
+                        responsecode.FORBIDDEN,
+                        (customxml.calendarserver_namespace, "valid-request"),
+                        "missing href: %s" % (inviteremove,),
+                    ))
+                if len(access) == 0:
+                    access = None
+                else:
+                    access = set(access)
+                return (userid, access)
+
+            def _autoShare(isShared, request):
+                if not isShared:
+                    if not self.isCalendarCollection() or config.Sharing.Calendars.AllowScheduling or len(self.listChildren()) == 0:
+                        return self.upgradeToShare(request)
+                else:
+                    return succeed(True)
+                raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Cannot upgrade to shared calendar"))
+
+            @inlineCallbacks
+            def _processInviteDoc(_, request):
+                setDict, removeDict, updateinviteDict = {}, {}, {}
+                for item in invitedoc.children:
+                    if isinstance(item, customxml.InviteSet):
+                        userid, access, summary = _handleInviteSet(item)
+                        setDict[userid] = (access, summary)
+                    elif isinstance(item, customxml.InviteRemove):
+                        userid, access = _handleInviteRemove(item)
+                        removeDict[userid] = access
+
+                # Special case removing and adding the same user and treat that as an add
+                okusers = set()
+                badusers = set()
+                sameUseridInRemoveAndSet = [u for u in removeDict.keys() if u in setDict]
+                for u in sameUseridInRemoveAndSet:
+                    removeACL = removeDict[u]
+                    newACL, summary = setDict[u]
+                    updateinviteDict[u] = (removeACL, newACL, summary)
+                    del removeDict[u]
+                    del setDict[u]
+                for userid, access in removeDict.iteritems():
+                    result = (yield self.uninviteUserToShare(userid, access, request))
+                    (okusers if result else badusers).add(userid)
+                for userid, (access, summary) in setDict.iteritems():
+                    result = (yield self.inviteUserToShare(userid, access, summary, request))
+                    (okusers if result else badusers).add(userid)
+                for userid, (removeACL, newACL, summary) in updateinviteDict.iteritems():
+                    result = (yield self.inviteUserUpdateToShare(userid, removeACL, newACL, summary, request))
+                    (okusers if result else badusers).add(userid)
+
+                # Do a final validation of the entire set of invites
+                self.validateInvites()
+                
+                # Create the multistatus response - only needed if some are bad
+                if badusers:
+                    xml_responses = []
+                    xml_responses.extend([
+                        davxml.StatusResponse(davxml.HRef(userid), davxml.Status.fromResponseCode(responsecode.OK))
+                        for userid in sorted(okusers)
+                    ])
+                    xml_responses.extend([
+                        davxml.StatusResponse(davxml.HRef(userid), davxml.Status.fromResponseCode(responsecode.FORBIDDEN))
+                        for userid in sorted(badusers)
+                    ])
+                
+                    #
+                    # Return response
+                    #
+                    returnValue(MultiStatusResponse(xml_responses))
+                else:
+                    returnValue(responsecode.OK)
+                    
+
+            return self.isShared(request).addCallback(_autoShare, request).addCallback(_processInviteDoc, request)
+
+        def _getData(data):
+            try:
+                doc = davxml.WebDAVDocument.fromString(data)
+            except ValueError, e:
+                self.log_error("Error parsing doc (%s) Doc:\n %s" % (str(e), data,))
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request")))
+
+            root = doc.root_element
+            xmlDocHanders = {
+                customxml.InviteShare: _handleInvite, 
+            }
+            if type(root) in xmlDocHanders:
+                return xmlDocHanders[type(root)](root).addErrback(_handleErrorResponse)
+            else:
+                self.log_error("Unsupported XML (%s)" % (root,))
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request")))
+
+        return allDataFromStream(request.stream).addCallback(_getData)
+
+    def xmlPOSTPreconditions(self, _, request):
+        if request.headers.hasHeader("Content-Type"):
+            mimetype = request.headers.getHeader("Content-Type")
+            if mimetype.mediaType in ("application", "text",) and mimetype.mediaSubtype == "xml":
+                encoding = mimetype.params["charset"] if "charset" in mimetype.params else "utf8"
+                return succeed(encoding)
+        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request")))
+
+    def xmlPOSTAuth(self, request):
+        d = self.authorize(request, (davxml.Read(), davxml.Write()))
+        d.addCallback(self.xmlPOSTPreconditions, request)
+        d.addCallback(self.xmlPOSTNoAuth, request)
+        return d
+    
+    def http_POST(self, request):
+        if self.isCollection():
+            contentType = request.headers.getHeader("content-type")
+            if contentType:
+                contentType = (contentType.mediaType, contentType.mediaSubtype)
+                if contentType in self._postHandlers:
+                    return self._postHandlers[contentType](self, request)
+                else:
+                    self.log_info("Get a POST of an unsupported content type on a collection type: %s" % (contentType,))
+            else:
+                self.log_info("Get a POST with no content type on a collection")
+        return responsecode.FORBIDDEN
+
+    _postHandlers = {
+        ("application", "xml") : xmlPOSTAuth,
+        ("text", "xml") : xmlPOSTAuth,
+    }
+
+inviteAccessMapToXML = {
+    "read-only"           : customxml.ReadAccess,
+    "read-write"          : customxml.ReadWriteAccess,
+    "read-write-schedule" : customxml.ReadWriteScheduleAccess,
+}
+inviteAccessMapFromXML = dict([(v,k) for k,v in inviteAccessMapToXML.iteritems()])
+
+inviteStatusMapToXML = {
+    "NEEDS-ACTION" : customxml.InviteStatusNoResponse,
+    "ACCEPTED"     : customxml.InviteStatusAccepted,
+    "DECLINED"     : customxml.InviteStatusDeclined,
+    "DELETED"      : customxml.InviteStatusDeleted,
+    "INVALID"      : customxml.InviteStatusInvalid,
+}
+inviteStatusMapFromXML = dict([(v,k) for k,v in inviteStatusMapToXML.iteritems()])
+
+class Invite(object):
+    
+    def __init__(self, inviteuid, userid, access, state, summary):
+        self.inviteuid = inviteuid
+        self.userid = userid
+        self.access = access
+        self.state = state
+        self.summary = summary
+        
+    def makePropertyElement(self):
+        
+        return customxml.InviteUser(
+            customxml.UID.fromString(self.inviteuid),
+            davxml.HRef.fromString(self.userid),
+            customxml.InviteAccess(inviteAccessMapToXML[self.access]()),
+            inviteStatusMapToXML[self.state](),
+        )
+
+class InvitesDatabase(AbstractSQLDatabase, LoggingMixIn):
+    
+    db_basename = db_prefix + "invites"
+    schema_version = "1"
+    db_type = "invites"
+
+    def __init__(self, resource):
+        """
+        @param resource: the L{twistedcaldav.static.CalDAVFile} resource for
+            the shared collection. C{resource} must be a calendar/addressbook collection.)
+        """
+        self.resource = resource
+        db_filename = os.path.join(self.resource.fp.path, InvitesDatabase.db_basename)
+        super(InvitesDatabase, self).__init__(db_filename, True, autocommit=True)
+
+    def create(self):
+        """
+        Create the index and initialize it.
+        """
+        self._db()
+
+    def allRecords(self):
+        
+        records = self._db_execute("select * from INVITE order by USERID")
+        return [self._makeRecord(row) for row in (records if records is not None else ())]
+    
+    def recordForUserID(self, userid):
+        
+        row = self._db_execute("select * from INVITE where USERID = :1", userid)
+        return self._makeRecord(row[0]) if row else None
+    
+    def recordForInviteUID(self, inviteUID):
+
+        row = self._db_execute("select * from INVITE where INVITEUID = :1", inviteUID)
+        return self._makeRecord(row[0]) if row else None
+    
+    def addOrUpdateRecord(self, record):
+
+        self._db_execute("""insert or replace into INVITE (INVITEUID, USERID, ACCESS, STATE, SUMMARY)
+            values (:1, :2, :3, :4, :5)
+            """, record.inviteuid, record.userid, record.access, record.state, record.summary,
+        )
+    
+    def removeRecordForUserID(self, userid):
+
+        self._db_execute("delete from INVITE where USERID = :1", userid)
+    
+    def removeRecordForInviteUID(self, inviteUID):
+
+        self._db_execute("delete from INVITE where INVITEUID = :1", inviteUID)
+    
+    def remove(self):
+        
+        self._db_close()
+        os.remove(self.dbpath)
+
+    def _db_version(self):
+        """
+        @return: the schema version assigned to this index.
+        """
+        return InvitesDatabase.schema_version
+
+    def _db_type(self):
+        """
+        @return: the collection type assigned to this index.
+        """
+        return InvitesDatabase.db_type
+
+    def _db_init_data_tables(self, q):
+        """
+        Initialise the underlying database tables.
+        @param q:           a database cursor to use.
+        """
+        #
+        # INVITE table is the primary table
+        #   INVITEUID: UID for this invite
+        #   NAME: identifier of invitee
+        #   ACCESS: Access mode for share
+        #   STATE: Invite response status
+        #   SUMMARY: Invite summary
+        #
+        q.execute(
+            """
+            create table INVITE (
+                INVITEUID      text unique,
+                USERID         text unique,
+                ACCESS         text,
+                STATE          text,
+                SUMMARY        text
+            )
+            """
+        )
+
+        q.execute(
+            """
+            create index USERID on INVITE (USERID)
+            """
+        )
+        q.execute(
+            """
+            create index INVITEUID on INVITE (INVITEUID)
+            """
+        )
+
+    def _db_upgrade_data_tables(self, q, old_version):
+        """
+        Upgrade the data from an older version of the DB.
+        """
+
+        # Nothing to do as we have not changed the schema
+        pass
+
+    def _makeRecord(self, row):
+        
+        return Invite(*[str(item) if type(item) == types.UnicodeType else item for item in row])
+
+class SharedHomeMixin(object):
+    """
+    A mix-in for calendar/addressbook homes that defines the operations for manipulating a sharee's
+    set of shared calendfars.
+    """
+    
+    def sharesDB(self):
+        
+        if not hasattr(self, "_sharesDB"):
+            self._sharesDB = SharedCalendarsDatabase(self)
+        return self._sharesDB
+
+    def provisionShares(self):
+        
+        if not hasattr(self, "_provisionedShares"):
+            from twistedcaldav.sharedcalendar import SharedCalendarResource
+            for share in self.sharesDB().allRecords():
+                child = SharedCalendarResource(self, share)
+                self.putChild(share.localname, child)
+            self._provisionedShares = True
+
+    @inlineCallbacks
+    def acceptShare(self, request, hostUrl, inviteUID, displayname=None):
+        
+        # Do this first to make sure we have a valid share
+        yield self._changeShare(request, "ACCEPTED", hostUrl, inviteUID, displayname)
+
+        # Add or update in DB
+        oldShare = self.sharesDB().recordForInviteUID(inviteUID)
+        if not oldShare:
+            oldShare = share = SharedCalendarRecord(inviteUID, hostUrl, str(uuid4()), displayname)
+            self.sharesDB().addOrUpdateRecord(share)
+        
+        # Return the URL of the shared calendar
+        returnValue(XMLResponse(
+            code = responsecode.OK,
+            element = SharedCalendar(
+                davxml.HRef.fromString(joinURL(self.url(), oldShare.localname))
+            )
+        ))
+
+    def wouldAcceptShare(self, hostUrl, request):
+        return succeed(True)
+
+    def removeShare(self, request, share):
+        """ Remove a shared calendar named in resourceName and send a decline """
+        return self.declineShare(request, share.hosturl, share.inviteuid)
+
+    @inlineCallbacks
+    def removeShareByUID(self, request, inviteuid):
+        """ Remove a shared calendar but do not send a decline back """
+
+        record = self.sharesDB().recordForInviteUID(inviteuid)
+        if record:
+            shareURL = joinURL(self.url(), record.localname)
+    
+            # For backwards compatibility we need to sync this up with the calendar-free-busy-set on the inbox
+            principal = (yield self.resourceOwnerPrincipal(request))
+            inboxURL = principal.scheduleInboxURL()
+            if inboxURL:
+                inbox = (yield request.locateResource(inboxURL))
+                inbox.processFreeBusyCalendar(shareURL, False)
+    
+            self.sharesDB().removeRecordForInviteUID(inviteuid)
+
+        returnValue(True)
+
+    @inlineCallbacks
+    def declineShare(self, request, hostUrl, inviteUID):
+
+        # Remove it if its in the DB
+        self.sharesDB().removeRecordForInviteUID(inviteUID)
+
+        yield self._changeShare(request, "DECLINED", hostUrl, inviteUID)
+        
+        returnValue(Response(code=responsecode.NO_CONTENT))
+
+    @inlineCallbacks
+    def _changeShare(self, request, state, hostUrl, replytoUID, displayname=None):
+        """ Accept an invite to a shared calendar """
+        
+        # Change state in sharer invite
+        owner = (yield self.ownerPrincipal(request))
+        owner = owner.principalURL()
+        sharedCalendar = (yield request.locateResource(hostUrl))
+        if sharedCalendar is None:
+            # Original shared calendar is gone - nothing we can do except ignore it
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                (customxml.calendarserver_namespace, "valid-request"),
+                "invalid shared calendar",
+            ))
+            
+        # Change the record
+        yield sharedCalendar.changeUserInviteState(request, replytoUID, owner, state, displayname)
+
+        yield self.sendReply(request, owner, sharedCalendar, state, hostUrl, replytoUID, displayname)
+
+    @inlineCallbacks
+    def sendReply(self, request, sharee, sharedCalendar, state, hostUrl, replytoUID, displayname=None):
+        
+
+        # Locate notifications collection for sharer
+        sharer = (yield sharedCalendar.ownerPrincipal(request))
+        notifications = (yield request.locateResource(sharer.notificationURL()))
+        
+        # Generate invite XML
+        notificationUID = "%s-reply" % (replytoUID,)
+        xmltype = customxml.InviteReply()
+        xmldata = customxml.Notification(
+            customxml.DTStamp.fromString(dateTimeToString(datetime.datetime.now(tz=utc))),
+            customxml.InviteReply(
+                *(
+                    (
+                        davxml.HRef.fromString(sharee),
+                        inviteStatusMapToXML[state](),
+                        customxml.HostURL(
+                            davxml.HRef.fromString(hostUrl),
+                        ),
+                        customxml.InReplyTo.fromString(replytoUID),
+                    ) + ((customxml.InviteSummary.fromString(displayname),) if displayname is not None else ())
+                )
+            ),
+        ).toxml()
+        
+        # Add to collections
+        yield notifications.addNotification(request, notificationUID, xmltype, xmldata)
+
+    def xmlPOSTNoAuth(self, encoding, request):
+
+        def _handleErrorResponse(error):
+            if isinstance(error.value, HTTPError) and hasattr(error.value, "response"):
+                return error.value.response
+            return Response(code=responsecode.BAD_REQUEST)
+
+        def _handleInviteReply(invitereplydoc):
+            """ Handle a user accepting or declining a sharing invite """
+            hostUrl = None
+            accepted = None
+            summary = None
+            replytoUID = None
+            for item in invitereplydoc.children:
+                if isinstance(item, customxml.InviteStatusAccepted):
+                    accepted = True
+                elif isinstance(item, customxml.InviteStatusDeclined):
+                    accepted = False
+                elif isinstance(item, customxml.InviteSummary):
+                    summary = str(item)
+                elif isinstance(item, customxml.HostURL):
+                    for hosturlItem in item.children:
+                        if isinstance(hosturlItem, davxml.HRef):
+                            hostUrl = str(hosturlItem)
+                elif isinstance(item, customxml.InReplyTo):
+                    replytoUID = str(item)
+            
+            if accepted is None or hostUrl is None or replytoUID is None:
+                raise HTTPError(ErrorResponse(
+                    responsecode.FORBIDDEN,
+                    (customxml.calendarserver_namespace, "valid-request"),
+                    "missing required XML elements",
+                ))
+            if accepted:
+                return self.acceptShare(request, hostUrl, replytoUID, displayname=summary)
+            else:
+                return self.declineShare(request, hostUrl, replytoUID)
+
+        def _getData(data):
+            try:
+                doc = davxml.WebDAVDocument.fromString(data)
+            except ValueError, e:
+                print "Error parsing doc (%s) Doc:\n %s" % (str(e), data,)
+                raise
+
+            root = doc.root_element
+            xmlDocHanders = {
+                customxml.InviteReply: _handleInviteReply,          
+            }
+            if type(root) in xmlDocHanders:
+                return xmlDocHanders[type(root)](root).addErrback(_handleErrorResponse)
+            else:
+                self.log_error("Unsupported XML (%s)" % (root,))
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (customxml.calendarserver_namespace, "valid-request")))
+
+        return allDataFromStream(request.stream).addCallback(_getData)
+
+class SharedCalendarRecord(object):
+    
+    def __init__(self, inviteuid, hosturl, localname, summary):
+        self.inviteuid = inviteuid
+        self.hosturl = hosturl
+        self.localname = localname
+        self.summary = summary
+
+class SharedCalendarsDatabase(AbstractSQLDatabase, LoggingMixIn):
+    
+    db_basename = db_prefix + "shares"
+    schema_version = "1"
+    db_type = "shares"
+
+    def __init__(self, resource):
+        """
+        @param resource: the L{twistedcaldav.static.CalDAVFile} resource for
+            the shared collection. C{resource} must be a calendar/addressbook home collection.)
+        """
+        self.resource = resource
+        db_filename = os.path.join(self.resource.fp.path, SharedCalendarsDatabase.db_basename)
+        super(SharedCalendarsDatabase, self).__init__(db_filename, True, autocommit=True)
+
+    def create(self):
+        """
+        Create the index and initialize it.
+        """
+        self._db()
+
+    def allRecords(self):
+        
+        records = self._db_execute("select * from SHARES order by LOCALNAME")
+        return [self._makeRecord(row) for row in (records if records is not None else ())]
+    
+    def recordForLocalName(self, localname):
+        
+        row = self._db_execute("select * from SHARES where LOCALNAME = :1", localname)
+        return self._makeRecord(row[0]) if row else None
+    
+    def recordForInviteUID(self, inviteUID):
+
+        row = self._db_execute("select * from SHARES where INVITEUID = :1", inviteUID)
+        return self._makeRecord(row[0]) if row else None
+    
+    def addOrUpdateRecord(self, record):
+
+        self._db_execute("""insert or replace into SHARES (INVITEUID, HOSTURL, LOCALNAME, SUMMARY)
+            values (:1, :2, :3, :4)
+            """, record.inviteuid, record.hosturl, record.localname, record.summary,
+        )
+    
+    def removeRecordForLocalName(self, localname):
+
+        self._db_execute("delete from SHARES where LOCALNAME = :1", localname)
+    
+    def removeRecordForInviteUID(self, inviteUID):
+
+        self._db_execute("delete from SHARES where INVITEUID = :1", inviteUID)
+    
+    def remove(self):
+        
+        self._db_close()
+        os.remove(self.dbpath)
+
+    def _db_version(self):
+        """
+        @return: the schema version assigned to this index.
+        """
+        return SharedCalendarsDatabase.schema_version
+
+    def _db_type(self):
+        """
+        @return: the collection type assigned to this index.
+        """
+        return SharedCalendarsDatabase.db_type
+
+    def _db_init_data_tables(self, q):
+        """
+        Initialise the underlying database tables.
+        @param q:           a database cursor to use.
+        """
+        #
+        # SHARES table is the primary table
+        #   INVITEUID: UID for this invite
+        #   HOSTURL: URL for data source
+        #   LOCALNAME: local path name
+        #   SUMMARY: Invite summary
+        #
+        q.execute(
+            """
+            create table SHARES (
+                INVITEUID      text unique,
+                HOSTURL        text,
+                LOCALNAME      text,
+                SUMMARY        text
+            )
+            """
+        )
+
+        q.execute(
+            """
+            create index INVITEUID on SHARES (INVITEUID)
+            """
+        )
+        q.execute(
+            """
+            create index HOSTURL on SHARES (HOSTURL)
+            """
+        )
+        q.execute(
+            """
+            create index LOCALNAME on SHARES (LOCALNAME)
+            """
+        )
+
+    def _db_upgrade_data_tables(self, q, old_version):
+        """
+        Upgrade the data from an older version of the DB.
+        """
+
+        # Nothing to do as we have not changed the schema
+        pass
+
+    def _makeRecord(self, row):
+        
+        return SharedCalendarRecord(*[str(item) if type(item) == types.UnicodeType else item for item in row])

Modified: CalendarServer/trunk/twistedcaldav/sql.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/sql.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/sql.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -327,6 +327,7 @@
         q = self._db().cursor()
         try:
             q.execute(sql, query_params)
+            self.lastrowid = q.lastrowid
             return q.fetchall()
         except DatabaseError:
             log.err("Exception while executing SQL on DB %s: %r %r" % (self, sql, query_params))

Modified: CalendarServer/trunk/twistedcaldav/static.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/static.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/static.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -32,6 +32,8 @@
     "DropBoxCollectionFile",
     "DropBoxChildFile",
     "TimezoneServiceFile",
+    "NotificationCollectionFile",
+    "NotificationFile",
     "AddressBookHomeProvisioningFile",
     "AddressBookHomeUIDProvisioningFile",
     "AddressBookHomeFile",
@@ -45,7 +47,6 @@
 from uuid import uuid4
 
 from twext.python.log import Logger
-from twext.web2.dav.http import ErrorResponse
 
 from twisted.internet.defer import fail, succeed, inlineCallbacks, returnValue, maybeDeferred
 from twisted.python.failure import Failure
@@ -54,12 +55,14 @@
 from twext.web2.http import HTTPError, StatusResponse
 from twext.web2.dav import davxml
 from twext.web2.dav.element.base import dav_namespace
-from twext.web2.dav.fileop import mkcollection, rmdir
+from twext.web2.dav.fileop import mkcollection, rmdir, delete
+from twext.web2.dav.http import ErrorResponse
 from twext.web2.dav.idav import IDAVResource
 from twext.web2.dav.noneprops import NonePropertyStore
 from twext.web2.dav.resource import AccessDeniedError
 from twext.web2.dav.resource import davPrivilegeSet
 from twext.web2.dav.util import parentForURL, bindMethods, joinURL
+from twext.web2.http_headers import generateContentType, MimeType
 
 from twistedcaldav import caldavxml
 from twistedcaldav import carddavxml
@@ -68,6 +71,7 @@
 from twistedcaldav.client.reverseproxy import ReverseProxyResource
 from twistedcaldav.config import config
 from twistedcaldav.customxml import TwistedCalendarAccessProperty, TwistedScheduleMatchETags
+from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
 from twistedcaldav.extensions import DAVFile, CachingPropertyStore
 from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
 from twistedcaldav.memcacheprops import MemcachePropertyCollection
@@ -78,6 +82,7 @@
 from twistedcaldav.resource import CalDAVResource, isCalendarCollectionResource, isPseudoCalendarCollectionResource
 from twistedcaldav.resource import isAddressBookCollectionResource, SearchAddressBookResource, SearchAllAddressBookResource
 from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource, IScheduleInboxResource
+from twistedcaldav.datafilters.privateevents import PrivateEventFilter
 from twistedcaldav.dropbox import DropBoxHomeResource, DropBoxCollectionResource
 from twistedcaldav.directorybackedaddressbook import DirectoryBackedAddressBookResource
 from twistedcaldav.directory.addressbook import uidsResourceName as uidsResourceNameAddressBook
@@ -91,11 +96,14 @@
 from twistedcaldav.directory.calendar import DirectoryCalendarHomeUIDProvisioningResource
 from twistedcaldav.directory.calendar import DirectoryCalendarHomeResource
 from twistedcaldav.directory.resource import AutoProvisioningResourceMixIn
+from twistedcaldav.sharing import SharedHomeMixin
 from twistedcaldav.timezoneservice import TimezoneServiceResource
 from twistedcaldav.vcardindex import AddressBookIndex
 from twistedcaldav.notify import getPubSubConfiguration, getPubSubXMPPURI
 from twistedcaldav.notify import getPubSubHeartbeatURI, getPubSubPath
 from twistedcaldav.notify import ClientNotifier, getNodeCacher
+from twistedcaldav.notifications import NotificationCollectionResource,\
+    NotificationResource
 
 log = Logger()
 
@@ -176,12 +184,6 @@
     # CalDAV
     ##
 
-    def resourceType(self):
-        if self.isCalendarCollection():
-            return davxml.ResourceType.calendar
-        else:
-            return super(CalDAVFile, self).resourceType()
-
     def createCalendar(self, request):
         #
         # request object is required because we need to validate against parent
@@ -275,6 +277,7 @@
 
             tzids = set()
             isowner = (yield self.isOwner(request, adminprincipals=True, readprincipals=True))
+            accessPrincipal = (yield self.resourceOwnerPrincipal(request))
 
             for name, uid, type in self.index().bruteForceSearch(): #@UnusedVariable
                 try:
@@ -291,7 +294,7 @@
                         continue
 
                     # Get the access filtered view of the data
-                    caldata = child.iCalendarTextFiltered(isowner)
+                    caldata = child.iCalendarTextFiltered(isowner, accessPrincipal.principalUID() if accessPrincipal else "")
                     try:
                         subcalendar = iComponent.fromString(caldata)
                     except ValueError:
@@ -313,21 +316,18 @@
 
         raise HTTPError(ErrorResponse(responsecode.BAD_REQUEST))
 
-    def iCalendarTextFiltered(self, isowner):
+    def iCalendarTextFiltered(self, isowner, accessUID=None):
         try:
             access = self.readDeadProperty(TwistedCalendarAccessProperty)
         except HTTPError:
             access = None
 
-        if access in (iComponent.ACCESS_CONFIDENTIAL, iComponent.ACCESS_RESTRICTED):
+        # Now "filter" the resource calendar data
+        caldata = PrivateEventFilter(access, isowner).filter(self.iCalendarText())
+        if accessUID:
+            caldata = PerUserDataFilter(accessUID).filter(caldata)
+        return str(caldata)
 
-            if not isowner:
-                # Now "filter" the resource calendar data through the CALDAV:calendar-data element and apply
-                # access restrictions to the data.
-                return caldavxml.CalendarData().elementFromResourceWithAccessRestrictions(self, access).calendarData()
-
-        return self.iCalendarText()
-
     def iCalendarText(self, name=None):
         if self.isPseudoCalendarCollection():
             if name is None:
@@ -356,9 +356,6 @@
 
         return calendar_data
 
-    def iCalendarXML(self, name=None):
-        return caldavxml.CalendarData.fromCalendar(self.iCalendarText(name))
-
     def createAddressBook(self, request):
         #
         # request object is required because we need to validate against parent
@@ -906,7 +903,7 @@
     def url(self):
         return joinURL(self.parent.url(), self.record.uid)
 
-class CalendarHomeFile (AutoProvisioningFileMixIn, DirectoryCalendarHomeResource, CalDAVFile):
+class CalendarHomeFile (AutoProvisioningFileMixIn, SharedHomeMixin, DirectoryCalendarHomeResource, CalDAVFile):
     """
     Calendar home collection resource.
     """
@@ -924,6 +921,12 @@
         CalDAVFile.__init__(self, path)
         DirectoryCalendarHomeResource.__init__(self, parent, record)
 
+    def provision(self):
+        result = super(CalendarHomeFile, self).provision()
+        if config.Sharing.Enabled:
+            self.provisionShares()
+        return result
+
     def provisionChild(self, name):
         if config.EnableDropBox:
             DropBoxHomeFileClass = DropBoxHomeFile
@@ -935,18 +938,23 @@
         else:
             FreeBusyURLFileClass = None
             
+        if config.Sharing.Enabled:
+            NotificationCollectionFileClass = NotificationCollectionFile
+        else:
+            NotificationCollectionFileClass = None
+
         cls = {
             "inbox"        : ScheduleInboxFile,
             "outbox"       : ScheduleOutboxFile,
             "dropbox"      : DropBoxHomeFileClass,
             "freebusy"     : FreeBusyURLFileClass,
+            "notification" : NotificationCollectionFileClass,
         }.get(name, None)
 
         if cls is not None:
             child = cls(self.fp.child(name).path, self)
             child.clientNotifier = self.clientNotifier
             return child
-
         return self.createSimilarFile(self.fp.child(name).path)
 
     def createSimilarFile(self, path):
@@ -1275,6 +1283,60 @@
     def checkPrivileges(self, request, privileges, recurse=False, principal=None, inherited_aces=None):
         return succeed(None)
 
+class NotificationCollectionFile(AutoProvisioningFileMixIn, NotificationCollectionResource, CalDAVFile):
+    """
+    Notification collection resource.
+    """
+    def __init__(self, path, parent):
+        NotificationCollectionResource.__init__(self)
+        CalDAVFile.__init__(self, path, principalCollections=parent.principalCollections())
+        self.parent = parent
+
+    def createSimilarFile(self, path):
+        if self.comparePath(path):
+            return self
+        else:
+            return NotificationFile(path, self)
+
+    def __repr__(self):
+        return "<%s (notification collection): %s>" % (self.__class__.__name__, self.fp.path)
+
+    def _writeNotification(self, request, uid, rname, xmltype, xmldata):
+        
+        # TODO: use the generic StoreObject api so that quota, sync-token etc all get changed properly
+        child = self.createSimilarFile(self.fp.child(rname).path)
+        child.fp.setContent(xmldata)
+        child.writeDeadProperty(davxml.GETContentType.fromString(generateContentType(MimeType("text", "xml", params={"charset":"utf-8"}))))
+        child.writeDeadProperty(customxml.NotificationType(xmltype))
+        
+        return succeed(True)
+
+    def _deleteNotification(self, request, rname):
+        
+        # TODO: use the generic DeleteResource api so that quota, sync-token etc all get changed properly
+        childfp = self.fp.child(rname)
+        return delete("", childfp)
+
+class NotificationFile(NotificationResource, CalDAVFile):
+
+    def __init__(self, path, parent):
+        NotificationResource.__init__(self, parent)
+        CalDAVFile.__init__(self, path, principalCollections=parent.principalCollections())
+
+        assert self.fp.isfile() or not self.fp.exists()
+
+    def createSimilarFile(self, path):
+        if self.comparePath(path):
+            return self
+        else:
+            return responsecode.NOT_FOUND
+
+    def __repr__(self):
+        return "<%s (notification file): %s>" % (self.__class__.__name__, self.fp.path)
+        
+    def resourceName(self):
+        return self.fp.basename()
+
 class AddressBookHomeProvisioningFile (DirectoryAddressBookHomeProvisioningResource, DAVFile):
     """
     Resource which provisions address book home collections as needed.

Modified: CalendarServer/trunk/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/stdconfig.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/stdconfig.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -339,7 +339,20 @@
     "EnableDropBox"           : False, # Calendar Drop Box
     "EnablePrivateEvents"     : False, # Private Events
     "EnableTimezoneService"   : False, # Timezone service
+    
+    "Sharing": {
+        "Enabled"             : False, # Overall on/off switch
+        "AllowExternalUsers"  : False, # External (non-principal) sharees allowed
 
+        "Calendars" : {
+            "Enabled"         : False, # Calendar on/off switch
+            "AllowScheduling" : False, # Scheduling in shared calendars
+        },
+        "AddressBooks" : {
+            "Enabled"         : False, # Address Books on/off switch
+        }        
+    },
+
     #
     # Web-based administration
     #
@@ -901,6 +914,14 @@
                     log.info("iMIP %s password not found in keychain" %
                         (direction,))
 
+def _updateSharing(configDict):
+    #
+    # FIXME: Use the config object instead of doing this here
+    #
+    from twistedcaldav.resource import CalDAVResource, CalendarPrincipalResource
+    CalDAVResource.enableSharing(configDict.Sharing.Enabled)
+    CalendarPrincipalResource.enableSharing(configDict.Sharing.Enabled)
+
 def _updatePartitions(configDict):
     if configDict.Partitioning.Enabled:
         partitions.setSelfPartition(configDict.Partitioning.ServerPartitionID)
@@ -925,8 +946,11 @@
     if configDict.EnableCardDAV:
         compliance += carddavxml.carddav_compliance
 
-    compliance += customxml.calendarserver_principal_property_search
+    compliance += customxml.calendarserver_principal_property_search_compliance
 
+    if config.Sharing.Enabled:
+        compliance += customxml.calendarserver_sharing_compliance
+
     configDict.CalDAVComplianceClasses = compliance
 
 
@@ -946,6 +970,7 @@
     _updateLogLevels,
     _updateNotifications,
     _updateScheduling,
+    _updateSharing,
     _updatePartitions,
     _updateCompliance,
     )

Modified: CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/test/test_calendarquery.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -30,6 +30,7 @@
 from twistedcaldav import caldavxml
 from twistedcaldav import ical
 from twistedcaldav.index import db_basename
+from twistedcaldav.query import queryfilter
 
 class CalendarQuery (twistedcaldav.test.util.TestCase):
     """
@@ -116,7 +117,7 @@
                             cal = property.calendar()
                             instances = cal.expandTimeRanges(query_timerange.end)
                             vevents = [x for x in cal.subcomponents() if x.name() == "VEVENT"]
-                            if not query_timerange.matchinstance(vevents[0], instances):
+                            if not queryfilter.TimeRange(query_timerange).matchinstance(vevents[0], instances):
                                 self.fail("REPORT property %r returned calendar %s outside of request time range %r"
                                           % (property, property.calendar, query_timerange))
 

Modified: CalendarServer/trunk/twistedcaldav/test/test_icalendar.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_icalendar.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/test/test_icalendar.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -3124,6 +3124,245 @@
             elif changed:
                 self.fail("Truncation happened when not expected: %s" % (title,))
 
+    def test_valid_recurrence(self):
+        
+        data = (
+            (
+                "1.1 - no recurrence",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    (None, True),
+                    (datetime.datetime(2007, 11, 14, 0, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2009, 10, 4, 0, 0, 0, tzinfo=tzutc()), False),
+                )
+            ),
+            (
+                "1.2 - rdate",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+RDATE:20091004T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    (None, True),
+                    (datetime.datetime(2007, 11, 14, 0, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2009, 10, 4, 0, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2009, 10, 5, 0, 0, 0, tzinfo=tzutc()), False),
+                )
+            ),
+            (
+                "1.3 - rrule no overrides",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    (None, True),
+                    (datetime.datetime(2007, 11, 14, 0, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2007, 11, 15, 0, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2009, 10, 4, 0, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2009, 10, 4, 1, 0, 0, tzinfo=tzutc()), False),
+                )
+            ),
+            (
+                "1.4 - rrule no overrides + rdate",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+RRULE:FREQ=DAILY
+RDATE:20091004T010000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    (None, True),
+                    (datetime.datetime(2007, 11, 14, 0, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2007, 11, 15, 0, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2009, 10, 4, 0, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2009, 10, 4, 1, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2009, 10, 4, 2, 0, 0, tzinfo=tzutc()), False),
+                )
+            ),
+            (
+                "1.5 - rrule no overrides + rdate + exdate",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+RRULE:FREQ=DAILY
+RDATE:20091004T010000Z
+EXDATE:20091003T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    (None, True),
+                    (datetime.datetime(2007, 11, 14, 0, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2007, 11, 15, 0, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2009, 10, 4, 0, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2009, 10, 4, 1, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2009, 10, 4, 2, 0, 0, tzinfo=tzutc()), False),
+                    (datetime.datetime(2009, 10, 3, 0, 0, 0, tzinfo=tzutc()), False),
+                )
+            ),
+            (
+                "1.6 - rrule with override",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071115T000000Z
+DTSTART:20071115T010000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    (None, True),
+                    (datetime.datetime(2007, 11, 14, 0, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2007, 11, 15, 0, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2007, 11, 15, 1, 0, 0, tzinfo=tzutc()), False),
+                    (datetime.datetime(2009, 10, 4, 0, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2009, 10, 4, 1, 0, 0, tzinfo=tzutc()), False),
+                )
+            ),
+            (
+                "1.7 - rrule + rdate with override",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+RRULE:FREQ=DAILY
+RDATE:20071115T010000Z
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071115T010000Z
+DTSTART:20071115T020000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    (None, True),
+                    (datetime.datetime(2007, 11, 14, 0, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2007, 11, 15, 0, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2007, 11, 15, 1, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2007, 11, 15, 2, 0, 0, tzinfo=tzutc()), False),
+                    (datetime.datetime(2009, 10, 4, 0, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2009, 10, 4, 1, 0, 0, tzinfo=tzutc()), False),
+                )
+            ),
+            (
+                "1.8 - override only",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071115T000000Z
+DTSTART:20071115T010000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    (None, False),
+                    (datetime.datetime(2007, 11, 14, 0, 0, 0, tzinfo=tzutc()), False),
+                    (datetime.datetime(2007, 11, 15, 0, 0, 0, tzinfo=tzutc()), True),
+                    (datetime.datetime(2009, 10, 4, 0, 0, 0, tzinfo=tzutc()), False),
+                )
+            ),
+            (
+                "1.9 - no recurrence one test master",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    (None, True),
+                )
+            ),
+            (
+                "1.10 - no recurrence one test master",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    (datetime.datetime(2007, 11, 14, 0, 0, 0, tzinfo=tzutc()), True),
+                )
+            ),
+            (
+                "1.11 - no recurrence one test missing",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    (datetime.datetime(2007, 11, 15, 0, 0, 0, tzinfo=tzutc()), False),
+                )
+            ),
+        )
+        
+        for clear_cache in (True, False):
+            for title, calendar, tests in data:
+                ical = Component.fromString(calendar)
+                for ctr, item in enumerate(tests):
+                    rid, result = item
+                    self.assertEqual(ical.validInstance(rid, clear_cache=clear_cache), result, "Failed comparison: %s #%d" % (title, ctr+1,))
+
+        for title, calendar, tests in data:
+            ical = Component.fromString(calendar)
+            rids = set([rid for rid, result in tests])
+            expected_results = set([rid for rid, result in tests if result==True])
+            actual_results = ical.validInstances(rids)
+            self.assertEqual(actual_results, expected_results, "Failed comparison: %s %s" % (title, actual_results,))
+
     def test_mismatched_until(self):
         invalid = (
             """BEGIN:VCALENDAR
@@ -3220,3 +3459,519 @@
                 calendar.validateForCalDAV()
             except:
                 self.fail("Valid calendar should validate")
+
+    def test_allperuseruids(self):
+        data = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:OPAQUE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+        calendar = Component.fromString(data)
+        self.assertEqual(calendar.allPerUserUIDs(), set((
+            "user01",
+            "user02",
+        )))
+
+    def test_perUserTransparency(self):
+        data = (
+                    (
+                        "No per-user, not recurring 1.1",
+                        """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:TRANSPARENT
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                        (
+                            (
+                                None,
+                                (
+                                    ("", True,),
+                                ),
+                            ),
+                        ),
+                    ),
+                    (
+                        "Single user, not recurring 1.2",
+                        """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:OPAQUE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                        (
+                            (
+                                None,
+                                (
+                                    ("", False,),
+                                    ("user01", False,),
+                                ),
+                            ),
+                        ),
+                    ),
+                    (
+                        "Two users, not recurring 1.3",
+                        """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:OPAQUE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                        (
+                            (
+                                None,
+                                (
+                                    ("", False,),
+                                    ("user01", False,),
+                                    ("user02", True,),
+                                ),
+                            ),
+                        ),
+                    ),
+                    (
+                        "No per-user, simple recurring 2.1",
+                        """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                        (
+                            (
+                                None,
+                                (
+                                    ("", False,),
+                                ),
+                            ),
+                            (
+                                datetime.datetime(2008, 6, 2, 12, 0, 0, tzinfo=tzutc()),
+                                (
+                                    ("", False,),
+                                ),
+                            ),
+                        ),
+                    ),
+                    (
+                        "Single user, simple recurring 2.2",
+                        """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:OPAQUE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                        (
+                            (
+                                None,
+                                (
+                                    ("", False,),
+                                    ("user01", False,),
+                                ),
+                            ),
+                            (
+                                datetime.datetime(2008, 6, 2, 12, 0, 0, tzinfo=tzutc()),
+                                (
+                                    ("", False,),
+                                    ("user01", False,),
+                                ),
+                            ),
+                        ),
+                    ),
+                    (
+                        "Two users, simple recurring 2.3",
+                        """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:OPAQUE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                        (
+                            (
+                                None,
+                                (
+                                    ("", False,),
+                                    ("user01", False,),
+                                    ("user02", True,),
+                                ),
+                            ),
+                            (
+                                datetime.datetime(2008, 6, 2, 12, 0, 0, tzinfo=tzutc()),
+                                (
+                                    ("", False,),
+                                    ("user01", False,),
+                                    ("user02", True,),
+                                ),
+                            ),
+                        ),
+                    ),
+                    (
+                        "No per-user, complex recurring 3.1",
+                        """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+TRANSP:TRANSPARENT
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+TRANSP:OPAQUE
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                        (
+                            (
+                                None,
+                                (
+                                    ("", True,),
+                                ),
+                            ),
+                            (
+                                datetime.datetime(2008, 6, 2, 12, 0, 0, tzinfo=tzutc()),
+                                (
+                                    ("", False,),
+                                ),
+                            ),
+                            (
+                                datetime.datetime(2008, 6, 3, 12, 0, 0, tzinfo=tzutc()),
+                                (
+                                    ("", True,),
+                                ),
+                            ),
+                            (
+                                datetime.datetime(2008, 6, 4, 12, 0, 0, tzinfo=tzutc()),
+                                (
+                                    ("", True,),
+                                ),
+                            ),
+                        ),
+                    ),
+                    (
+                        "Single user, complex recurring 3.2",
+                        """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:OPAQUE
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080603T120000Z
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                        (
+                            (
+                                None,
+                                (
+                                    ("", False,),
+                                    ("user01", False,),
+                                ),
+                            ),
+                            (
+                                datetime.datetime(2008, 6, 2, 12, 0, 0, tzinfo=tzutc()),
+                                (
+                                    ("", False,),
+                                    ("user01", True,),
+                                ),
+                            ),
+                            (
+                                datetime.datetime(2008, 6, 3, 12, 0, 0, tzinfo=tzutc()),
+                                (
+                                    ("", False,),
+                                    ("user01", True,),
+                                ),
+                            ),
+                            (
+                                datetime.datetime(2008, 6, 4, 12, 0, 0, tzinfo=tzutc()),
+                                (
+                                    ("", False,),
+                                    ("user01", False,),
+                                ),
+                            ),
+                        ),
+                    ),
+                    (
+                        "Two users, complex recurring 3.3",
+                        """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:OPAQUE
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080603T120000Z
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:OPAQUE
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080604T120000Z
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""".replace("\n", "\r\n"),
+                        (
+                            (
+                                None,
+                                (
+                                    ("", False,),
+                                    ("user01", False,),
+                                    ("user02", True,),
+                                ),
+                            ),
+                            (
+                                datetime.datetime(2008, 6, 2, 12, 0, 0, tzinfo=tzutc()),
+                                (
+                                    ("", False,),
+                                    ("user01", True,),
+                                    ("user02", False,),
+                                ),
+                            ),
+                            (
+                                datetime.datetime(2008, 6, 3, 12, 0, 0, tzinfo=tzutc()),
+                                (
+                                    ("", False,),
+                                    ("user01", True,),
+                                    ("user02", True,),
+                                ),
+                            ),
+                            (
+                                datetime.datetime(2008, 6, 4, 12, 0, 0, tzinfo=tzutc()),
+                                (
+                                    ("", False,),
+                                    ("user01", False,),
+                                    ("user02", True,),
+                                ),
+                            ),
+                        ),
+                    ),
+                )
+
+        for title, text, results in data:
+            calendar = Component.fromString(text)
+            for rid, result in results:
+                self.assertEqual(calendar.perUserTransparency(rid), result, "Failed comparison: %s %s" % (title, rid,))

Modified: CalendarServer/trunk/twistedcaldav/test/test_index.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_index.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/test/test_index.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -17,16 +17,17 @@
 from twisted.internet import reactor
 from twisted.internet.task import deferLater
 
+from twistedcaldav import caldavxml
+from twistedcaldav.caldavxml import TimeRange
 from twistedcaldav.ical import Component
 from twistedcaldav.index import Index, default_future_expansion_duration,\
     maximum_future_expansion_duration, IndexedSearchException,\
     AbstractCalendarIndex, icalfbtype_to_indexfbtype
 from twistedcaldav.index import ReservationError, MemcachedUIDReserver
 from twistedcaldav.instance import InvalidOverriddenInstanceError
+from twistedcaldav.query import queryfilter
 from twistedcaldav.test.util import InMemoryMemcacheProtocol
 import twistedcaldav.test.util
-from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import TimeRange
 from vobject.icalendar import utc
 import sqlite3
 
@@ -278,7 +279,7 @@
 """,
                 "20080601T000000Z", "20080602T000000Z",
                 "mailto:user1 at example.com",
-                (('N', "2008-06-01 12:00:00+00:00", "2008-06-01 13:00:00+00:00", 'B'),),
+                (('N', "2008-06-01 12:00:00+00:00", "2008-06-01 13:00:00+00:00", 'B', 'F'),),
             ),
             (
                 "#1.2 Simple component - transparent",
@@ -299,7 +300,7 @@
 """,
                 "20080602T000000Z", "20080603T000000Z",
                 "mailto:user1 at example.com",
-                (('N', "2008-06-02 12:00:00+00:00", "2008-06-02 13:00:00+00:00", 'F'),),
+                (('N', "2008-06-02 12:00:00+00:00", "2008-06-02 13:00:00+00:00", 'B', 'T'),),
             ),
             (
                 "#1.3 Simple component - canceled",
@@ -320,7 +321,7 @@
 """,
                 "20080603T000000Z", "20080604T000000Z",
                 "mailto:user1 at example.com",
-                (('N', "2008-06-03 12:00:00+00:00", "2008-06-03 13:00:00+00:00", 'F'),),
+                (('N', "2008-06-03 12:00:00+00:00", "2008-06-03 13:00:00+00:00", 'F', 'F'),),
             ),
             (
                 "#1.4 Simple component - tentative",
@@ -341,7 +342,7 @@
 """,
                 "20080604T000000Z", "20080605T000000Z",
                 "mailto:user1 at example.com",
-                (('N', "2008-06-04 12:00:00+00:00", "2008-06-04 13:00:00+00:00", 'T'),),
+                (('N', "2008-06-04 12:00:00+00:00", "2008-06-04 13:00:00+00:00", 'T', 'F'),),
             ),
             (
                 "#2.1 Recurring component - busy",
@@ -363,8 +364,8 @@
                 "20080605T000000Z", "20080607T000000Z",
                 "mailto:user1 at example.com",
                 (
-                    ('N', "2008-06-05 12:00:00+00:00", "2008-06-05 13:00:00+00:00", 'B'),
-                    ('N', "2008-06-06 12:00:00+00:00", "2008-06-06 13:00:00+00:00", 'B'),
+                    ('N', "2008-06-05 12:00:00+00:00", "2008-06-05 13:00:00+00:00", 'B', 'F'),
+                    ('N', "2008-06-06 12:00:00+00:00", "2008-06-06 13:00:00+00:00", 'B', 'F'),
                 ),
             ),
             (
@@ -397,8 +398,8 @@
                 "20080607T000000Z", "20080609T000000Z",
                 "mailto:user1 at example.com",
                 (
-                    ('N', "2008-06-07 12:00:00+00:00", "2008-06-07 13:00:00+00:00", 'B'),
-                    ('N', "2008-06-08 14:00:00+00:00", "2008-06-08 15:00:00+00:00", 'F'),
+                    ('N', "2008-06-07 12:00:00+00:00", "2008-06-07 13:00:00+00:00", 'B', 'F'),
+                    ('N', "2008-06-08 14:00:00+00:00", "2008-06-08 15:00:00+00:00", 'B', 'T'),
                 ),
             ),
         )
@@ -428,15 +429,419 @@
                       name="VCALENDAR",
                    )
               )
+            filter = queryfilter.Filter(filter)
 
             resources = self.db.indexedSearch(filter, fbtype=True)
             index_results = set()
-            for _ignore_name, _ignore_uid, type, test_organizer, float, start, end, fbtype in resources:
+            for _ignore_name, _ignore_uid, type, test_organizer, float, start, end, fbtype, transp in resources:
                 self.assertEqual(test_organizer, organizer, msg=description)
-                index_results.add((float, start, end, fbtype,))
+                index_results.add((float, start, end, fbtype, transp,))
 
             self.assertEqual(set(instances), index_results, msg=description)
 
+    def test_index_timespan_per_user(self):
+        data = (
+            (
+                "#1.1 Single per-user non-recurring component",
+                "1.1",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1.1
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890-1.1
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""",
+                "20080601T000000Z", "20080602T000000Z",
+                "mailto:user1 at example.com",
+                (
+                    (
+                        "user01",
+                        (('N', "2008-06-01 12:00:00+00:00", "2008-06-01 13:00:00+00:00", 'B', 'T'),),
+                    ),
+                    (
+                        "user02",
+                        (('N', "2008-06-01 12:00:00+00:00", "2008-06-01 13:00:00+00:00", 'B', 'F'),),
+                    ),
+                ),
+            ),
+            (
+                "#1.2 Two per-user non-recurring component",
+                "1.2",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1.2
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890-1.2
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890-1.2
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""",
+                "20080601T000000Z", "20080602T000000Z",
+                "mailto:user1 at example.com",
+                (
+                    (
+                        "user01",
+                        (('N', "2008-06-01 12:00:00+00:00", "2008-06-01 13:00:00+00:00", 'B', 'T'),),
+                    ),
+                    (
+                        "user02",
+                        (('N', "2008-06-01 12:00:00+00:00", "2008-06-01 13:00:00+00:00", 'B', 'F'),),
+                    ),
+                    (
+                        "user03",
+                        (('N', "2008-06-01 12:00:00+00:00", "2008-06-01 13:00:00+00:00", 'B', 'F'),),
+                    ),
+                ),
+            ),
+            (
+                "#2.1 Single per-user simple recurring component",
+                "2.1",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1.1
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY;COUNT=10
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890-1.1
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""",
+                "20080601T000000Z", "20080603T000000Z",
+                "mailto:user1 at example.com",
+                (
+                    (
+                        "user01",
+                        (
+                            ('N', "2008-06-01 12:00:00+00:00", "2008-06-01 13:00:00+00:00", 'B', 'T'),
+                            ('N', "2008-06-02 12:00:00+00:00", "2008-06-02 13:00:00+00:00", 'B', 'T'),
+                        ),
+                    ),
+                    (
+                        "user02",
+                        (
+                            ('N', "2008-06-01 12:00:00+00:00", "2008-06-01 13:00:00+00:00", 'B', 'F'),
+                            ('N', "2008-06-02 12:00:00+00:00", "2008-06-02 13:00:00+00:00", 'B', 'F'),
+                        ),
+                    ),
+                ),
+            ),
+            (
+                "#2.2 Two per-user simple recurring component",
+                "2.2",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1.2
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY;COUNT=10
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890-1.2
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890-1.2
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""",
+                "20080601T000000Z", "20080603T000000Z",
+                "mailto:user1 at example.com",
+                (
+                    (
+                        "user01",
+                        (
+                            ('N', "2008-06-01 12:00:00+00:00", "2008-06-01 13:00:00+00:00", 'B', 'T'),
+                            ('N', "2008-06-02 12:00:00+00:00", "2008-06-02 13:00:00+00:00", 'B', 'T'),
+                        ),
+                    ),
+                    (
+                        "user02",
+                        (
+                            ('N', "2008-06-01 12:00:00+00:00", "2008-06-01 13:00:00+00:00", 'B', 'F'),
+                            ('N', "2008-06-02 12:00:00+00:00", "2008-06-02 13:00:00+00:00", 'B', 'F'),
+                        ),
+                    ),
+                    (
+                        "user03",
+                        (
+                            ('N', "2008-06-01 12:00:00+00:00", "2008-06-01 13:00:00+00:00", 'B', 'F'),
+                            ('N', "2008-06-02 12:00:00+00:00", "2008-06-02 13:00:00+00:00", 'B', 'F'),
+                        ),
+                    ),
+                ),
+            ),
+            (
+                "#3.1 Single per-user complex recurring component",
+                "3.1",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1.1
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY;COUNT=10
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1.1
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890-1.1
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:OPAQUE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""",
+                "20080601T000000Z", "20080604T000000Z",
+                "mailto:user1 at example.com",
+                (
+                    (
+                        "user01",
+                        (
+                            ('N', "2008-06-01 12:00:00+00:00", "2008-06-01 13:00:00+00:00", 'B', 'T'),
+                            ('N', "2008-06-02 13:00:00+00:00", "2008-06-02 14:00:00+00:00", 'B', 'F'),
+                            ('N', "2008-06-03 12:00:00+00:00", "2008-06-03 13:00:00+00:00", 'B', 'T'),
+                        ),
+                    ),
+                    (
+                        "user02",
+                        (
+                            ('N', "2008-06-01 12:00:00+00:00", "2008-06-01 13:00:00+00:00", 'B', 'F'),
+                            ('N', "2008-06-02 13:00:00+00:00", "2008-06-02 14:00:00+00:00", 'B', 'F'),
+                            ('N', "2008-06-03 12:00:00+00:00", "2008-06-03 13:00:00+00:00", 'B', 'F'),
+                        ),
+                    ),
+                ),
+            ),
+            (
+                "#3.2 Two per-user complex recurring component",
+                "3.2",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1.2
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=DAILY;COUNT=10
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1.2
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T130000Z
+DTEND:20080602T140000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890-1.2
+X-CALENDARSERVER-PERUSER-UID:user01
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080602T120000Z
+TRANSP:OPAQUE
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+BEGIN:X-CALENDARSERVER-PERUSER
+UID:12345-67890-1.2
+X-CALENDARSERVER-PERUSER-UID:user02
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:X-CALENDARSERVER-PERINSTANCE
+BEGIN:X-CALENDARSERVER-PERINSTANCE
+RECURRENCE-ID:20080603T120000Z
+TRANSP:TRANSPARENT
+END:X-CALENDARSERVER-PERINSTANCE
+END:X-CALENDARSERVER-PERUSER
+END:VCALENDAR
+""",
+                "20080601T000000Z", "20080604T000000Z",
+                "mailto:user1 at example.com",
+                (
+                    (
+                        "user01",
+                        (
+                            ('N', "2008-06-01 12:00:00+00:00", "2008-06-01 13:00:00+00:00", 'B', 'T'),
+                            ('N', "2008-06-02 13:00:00+00:00", "2008-06-02 14:00:00+00:00", 'B', 'F'),
+                            ('N', "2008-06-03 12:00:00+00:00", "2008-06-03 13:00:00+00:00", 'B', 'T'),
+                        ),
+                    ),
+                    (
+                        "user02",
+                        (
+                            ('N', "2008-06-01 12:00:00+00:00", "2008-06-01 13:00:00+00:00", 'B', 'F'),
+                            ('N', "2008-06-02 13:00:00+00:00", "2008-06-02 14:00:00+00:00", 'B', 'F'),
+                            ('N', "2008-06-03 12:00:00+00:00", "2008-06-03 13:00:00+00:00", 'B', 'T'),
+                        ),
+                    ),
+                    (
+                        "user03",
+                        (
+                            ('N', "2008-06-01 12:00:00+00:00", "2008-06-01 13:00:00+00:00", 'B', 'F'),
+                            ('N', "2008-06-02 13:00:00+00:00", "2008-06-02 14:00:00+00:00", 'B', 'F'),
+                            ('N', "2008-06-03 12:00:00+00:00", "2008-06-03 13:00:00+00:00", 'B', 'F'),
+                        ),
+                    ),
+                ),
+            ),
+        )
+
+        revision = 0
+        for description, name, calendar_txt, trstart, trend, organizer, peruserinstances in data:
+            revision += 1
+            calendar = Component.fromString(calendar_txt)
+
+            f = open(os.path.join(self.site.resource.fp.path, name), "w")
+            f.write(calendar_txt)
+            del f
+
+            self.db.addResource(name, calendar, revision)
+            self.assertTrue(self.db.resourceExists(name), msg=description)
+
+            # Create fake filter element to match time-range
+            filter =  caldavxml.Filter(
+                  caldavxml.ComponentFilter(
+                      caldavxml.ComponentFilter(
+                          TimeRange(
+                              start=trstart,
+                              end=trend,
+                          ),
+                          name=("VEVENT", "VFREEBUSY", "VAVAILABILITY"),
+                      ),
+                      name="VCALENDAR",
+                   )
+              )
+            filter = queryfilter.Filter(filter)
+
+            for useruid, instances in peruserinstances:
+                resources = self.db.indexedSearch(filter, useruid=useruid, fbtype=True)
+                index_results = set()
+                for _ignore_name, _ignore_uid, type, test_organizer, float, start, end, fbtype, transp in resources:
+                    self.assertEqual(test_organizer, organizer, msg=description)
+                    index_results.add((str(float), str(start), str(end), str(fbtype), str(transp),))
+    
+                self.assertEqual(set(instances), index_results, msg="%s, user:%s" % (description, useruid,))
+
+            revision += 1
+            self.db.deleteResource(name, revision)
+
     def test_index_revisions(self):
         data1 = """BEGIN:VCALENDAR
 VERSION:2.0
@@ -500,424 +905,6 @@
         for revision, results in tests:
             self.assertEquals(self.db.whatchanged(revision), results, "Mismatched results for whatchanged with revision %d" % (revision,))
 
-class SQLIndexUpgradeTests (twistedcaldav.test.util.TestCase):
-    """
-    Test abstract SQL DB class
-    """
-
-    class OldIndexv6(Index):
-
-        def _db_version(self):
-            """
-            @return: the schema version assigned to this index.
-            """
-            return "6"
-
-        def _db_init_data_tables_base(self, q, uidunique):
-            """
-            Initialise the underlying database tables.
-            @param q:           a database cursor to use.
-            """
-            #
-            # RESOURCE table is the primary index table
-            #   NAME: Last URI component (eg. <uid>.ics, RESOURCE primary key)
-            #   UID: iCalendar UID (may or may not be unique)
-            #   TYPE: iCalendar component type
-            #   RECURRANCE_MAX: Highest date of recurrence expansion
-            #
-            if uidunique:
-                q.execute(
-                    """
-                    create table RESOURCE (
-                        NAME           text unique,
-                        UID            text unique,
-                        TYPE           text,
-                        RECURRANCE_MAX date
-                    )
-                    """
-                )
-            else:
-                q.execute(
-                    """
-                    create table RESOURCE (
-                        NAME           text unique,
-                        UID            text,
-                        TYPE           text,
-                        RECURRANCE_MAX date
-                    )
-                    """
-                )
-    
-            #
-            # TIMESPAN table tracks (expanded) timespans for resources
-            #   NAME: Related resource (RESOURCE foreign key)
-            #   FLOAT: 'Y' if start/end are floating, 'N' otherwise
-            #   START: Start date
-            #   END: End date
-            #
-            q.execute(
-                """
-                create table TIMESPAN (
-                    NAME  text,
-                    FLOAT text(1),
-                    START date,
-                    END   date
-                )
-                """
-            )
-    
-            if uidunique:
-                #
-                # RESERVED table tracks reserved UIDs
-                #   UID: The UID being reserved
-                #   TIME: When the reservation was made
-                #
-                q.execute(
-                    """
-                    create table RESERVED (
-                        UID  text unique,
-                        TIME date
-                    )
-                    """
-                )
-
-        def _db_upgrade(self, old_version):
-            """
-            Upgrade the database tables.
-            """
-            
-            return super(AbstractCalendarIndex, self)._db_upgrade(old_version)
-
-        def _add_to_db(self, name, calendar, cursor = None, expand_until=None, reCreate=False):
-            """
-            Records the given calendar resource in the index with the given name.
-            Resource names and UIDs must both be unique; only one resource name may
-            be associated with any given UID and vice versa.
-            NB This method does not commit the changes to the db - the caller
-            MUST take care of that
-            @param name: the name of the resource to add.
-            @param calendar: a L{Calendar} object representing the resource
-                contents.
-            """
-            uid = calendar.resourceUID()
-    
-            # Decide how far to expand based on the component
-            master = calendar.masterComponent()
-            if master is None or not calendar.isRecurring() and not calendar.isRecurringUnbounded():
-                # When there is no master we have a set of overridden components - index them all.
-                # When there is one instance - index it.
-                # When bounded - index all.
-                expand = datetime.datetime(2100, 1, 1, 0, 0, 0, tzinfo=utc)
-            else:
-                if expand_until:
-                    expand = expand_until
-                else:
-                    expand = datetime.date.today() + default_future_expansion_duration
-        
-                if expand > (datetime.date.today() + maximum_future_expansion_duration):
-                    raise IndexedSearchException
-    
-            try:
-                instances = calendar.expandTimeRanges(expand, ignoreInvalidInstances=reCreate)
-            except InvalidOverriddenInstanceError:
-                raise
-    
-            self._delete_from_db(name, uid, None)
-    
-            for key in instances:
-                instance = instances[key]
-                start = instance.start.replace(tzinfo=utc)
-                end = instance.end.replace(tzinfo=utc)
-                float = 'Y' if instance.start.tzinfo is None else 'N'
-                self._db_execute(
-                    """
-                    insert into TIMESPAN (NAME, FLOAT, START, END)
-                    values (:1, :2, :3, :4)
-                    """, name, float, start, end
-                )
-    
-            # Special - for unbounded recurrence we insert a value for "infinity"
-            # that will allow an open-ended time-range to always match it.
-            if calendar.isRecurringUnbounded():
-                start = datetime.datetime(2100, 1, 1, 0, 0, 0, tzinfo=utc)
-                end = datetime.datetime(2100, 1, 1, 1, 0, 0, tzinfo=utc)
-                float = 'N'
-                self._db_execute(
-                    """
-                    insert into TIMESPAN (NAME, FLOAT, START, END)
-                    values (:1, :2, :3, :4)
-                    """, name, float, start, end
-                )
-                 
-            self._db_execute(
-                """
-                insert into RESOURCE (NAME, UID, TYPE, RECURRANCE_MAX)
-                values (:1, :2, :3, :4)
-                """, name, uid, calendar.resourceType(), instances.limit
-            )
-
-    class OldIndexv7(Index):
-
-        def _db_version(self):
-            """
-            @return: the schema version assigned to this index.
-            """
-            return "7"
-
-        def _db_init_data_tables_base(self, q, uidunique):
-            """
-            Initialise the underlying database tables.
-            @param q:           a database cursor to use.
-            """
-            #
-            # RESOURCE table is the primary index table
-            #   NAME: Last URI component (eg. <uid>.ics, RESOURCE primary key)
-            #   UID: iCalendar UID (may or may not be unique)
-            #   TYPE: iCalendar component type
-            #   RECURRANCE_MAX: Highest date of recurrence expansion
-            #   ORGANIZER: cu-address of the Organizer of the event
-            #
-            if uidunique:
-                q.execute(
-                    """
-                    create table RESOURCE (
-                        NAME           text unique,
-                        UID            text unique,
-                        TYPE           text,
-                        RECURRANCE_MAX date,
-                        ORGANIZER      text
-                    )
-                    """
-                )
-            else:
-                q.execute(
-                    """
-                    create table RESOURCE (
-                        NAME           text unique,
-                        UID            text,
-                        TYPE           text,
-                        RECURRANCE_MAX date
-                    )
-                    """
-                )
-    
-            #
-            # TIMESPAN table tracks (expanded) time spans for resources
-            #   NAME: Related resource (RESOURCE foreign key)
-            #   FLOAT: 'Y' if start/end are floating, 'N' otherwise
-            #   START: Start date
-            #   END: End date
-            #   FBTYPE: FBTYPE value:
-            #     '?' - unknown
-            #     'F' - free
-            #     'B' - busy
-            #     'U' - busy-unavailable
-            #     'T' - busy-tentative
-            #
-            q.execute(
-                """
-                create table TIMESPAN (
-                    NAME  text,
-                    FLOAT text(1),
-                    START date,
-                    END   date,
-                    FBTYPE text(1)
-                )
-                """
-            )
-    
-            if uidunique:
-                #
-                # RESERVED table tracks reserved UIDs
-                #   UID: The UID being reserved
-                #   TIME: When the reservation was made
-                #
-                q.execute(
-                    """
-                    create table RESERVED (
-                        UID  text unique,
-                        TIME date
-                    )
-                    """
-                )
-
-        def _add_to_db(self, name, calendar, cursor = None, expand_until=None, reCreate=False):
-            """
-            Records the given calendar resource in the index with the given name.
-            Resource names and UIDs must both be unique; only one resource name may
-            be associated with any given UID and vice versa.
-            NB This method does not commit the changes to the db - the caller
-            MUST take care of that
-            @param name: the name of the resource to add.
-            @param calendar: a L{Calendar} object representing the resource
-                contents.
-            """
-            uid = calendar.resourceUID()
-            organizer = calendar.getOrganizer()
-            if not organizer:
-                organizer = ""
-    
-            # Decide how far to expand based on the component
-            master = calendar.masterComponent()
-            if master is None or not calendar.isRecurring() and not calendar.isRecurringUnbounded():
-                # When there is no master we have a set of overridden components - index them all.
-                # When there is one instance - index it.
-                # When bounded - index all.
-                expand = datetime.datetime(2100, 1, 1, 0, 0, 0, tzinfo=utc)
-            else:
-                if expand_until:
-                    expand = expand_until
-                else:
-                    expand = datetime.date.today() + default_future_expansion_duration
-        
-                if expand > (datetime.date.today() + maximum_future_expansion_duration):
-                    raise IndexedSearchException
-    
-            try:
-                instances = calendar.expandTimeRanges(expand, ignoreInvalidInstances=reCreate)
-            except InvalidOverriddenInstanceError:
-                raise
-    
-            self._delete_from_db(name, uid, None)
-    
-            for key in instances:
-                instance = instances[key]
-                start = instance.start.replace(tzinfo=utc)
-                end = instance.end.replace(tzinfo=utc)
-                float = 'Y' if instance.start.tzinfo is None else 'N'
-                self._db_execute(
-                    """
-                    insert into TIMESPAN (NAME, FLOAT, START, END, FBTYPE)
-                    values (:1, :2, :3, :4, :5)
-                    """, name, float, start, end, icalfbtype_to_indexfbtype.get(instance.component.getFBType(), 'F')
-                )
-    
-            # Special - for unbounded recurrence we insert a value for "infinity"
-            # that will allow an open-ended time-range to always match it.
-            if calendar.isRecurringUnbounded():
-                start = datetime.datetime(2100, 1, 1, 0, 0, 0, tzinfo=utc)
-                end = datetime.datetime(2100, 1, 1, 1, 0, 0, tzinfo=utc)
-                float = 'N'
-                self._db_execute(
-                    """
-                    insert into TIMESPAN (NAME, FLOAT, START, END, FBTYPE)
-                    values (:1, :2, :3, :4, :5)
-                    """, name, float, start, end, '?'
-                )
-                 
-            self._db_execute(
-                """
-                insert into RESOURCE (NAME, UID, TYPE, RECURRANCE_MAX, ORGANIZER)
-                values (:1, :2, :3, :4, :5)
-                """, name, uid, calendar.resourceType(), instances.limit, organizer
-            )
-
-    def setUp(self):
-        super(SQLIndexUpgradeTests, self).setUp()
-        self.site.resource.isCalendarCollection = lambda: True
-        self.db = Index(self.site.resource)
-        self.olddbv6 = SQLIndexUpgradeTests.OldIndexv6(self.site.resource)
-        self.olddbv7 = SQLIndexUpgradeTests.OldIndexv7(self.site.resource)
-
-    def prepareOldDB(self):
-        if os.path.exists(self.olddbv6.dbpath):
-            os.remove(self.olddbv6.dbpath)
-
-    def test_old_schema(self):
-        
-        for olddb in (self.olddbv6, self.olddbv7):
-            self.prepareOldDB()
-    
-            schema = olddb._db_value_for_sql(
-                """
-                select VALUE from CALDAV
-                 where KEY = 'SCHEMA_VERSION'
-                """)
-            self.assertEqual(schema, olddb._db_version())
-
-    def test_empty_upgrade(self):
-        
-        for olddb in (self.olddbv6, self.olddbv7):
-            self.prepareOldDB()
-    
-            schema = olddb._db_value_for_sql(
-                """
-                select VALUE from CALDAV
-                 where KEY = 'SCHEMA_VERSION'
-                """)
-            self.assertEqual(schema, olddb._db_version())
-    
-            if olddb._db_version() == "6":
-                self.assertRaises(sqlite3.OperationalError, olddb._db_value_for_sql, "select ORGANIZER from RESOURCE")
-                self.assertRaises(sqlite3.OperationalError, olddb._db_value_for_sql, "select FBTYPE from TIMESPAN")
-            elif olddb._db_version() == "7":
-                olddb._db_value_for_sql("select ORGANIZER from RESOURCE")
-                olddb._db_value_for_sql("select FBTYPE from TIMESPAN")
-            self.assertEqual(set([row[1] for row in olddb._db_execute("PRAGMA index_list(TIMESPAN)")]), set())
-    
-            schema = self.db._db_value_for_sql(
-                """
-                select VALUE from CALDAV
-                 where KEY = 'SCHEMA_VERSION'
-                """)
-            self.assertEqual(schema, self.db._db_version())
-    
-            value = self.db._db_value_for_sql("select ORGANIZER from RESOURCE")
-            self.assertEqual(value, None)
-            self.assertEqual(set([row[1] for row in self.db._db_execute("PRAGMA index_list(TIMESPAN)")]), set(("STARTENDFLOAT",)))
-
-    def test_basic_upgrade(self):
-        
-        for olddb in (self.olddbv6, self.olddbv7):
-            self.prepareOldDB()
-    
-            calendar_name = "1.ics"
-            calendar_data = """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890-1.1
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-"""
-    
-            olddb.addResource(calendar_name, Component.fromString(calendar_data), 1)
-            self.assertTrue(olddb.resourceExists(calendar_name))
-    
-            if olddb._db_version() == "6":
-                self.assertRaises(sqlite3.OperationalError, olddb._db_value_for_sql, "select ORGANIZER from RESOURCE")
-                self.assertRaises(sqlite3.OperationalError, olddb._db_value_for_sql, "select FBTYPE from TIMESPAN")
-            elif olddb._db_version() == "7":
-                olddb._db_value_for_sql("select ORGANIZER from RESOURCE")
-                olddb._db_value_for_sql("select FBTYPE from TIMESPAN")
-            self.assertEqual(set([row[1] for row in olddb._db_execute("PRAGMA index_list(TIMESPAN)")]), set())
-    
-            value = self.db._db_value_for_sql("select ORGANIZER from RESOURCE where NAME = :1", calendar_name)
-            if olddb._db_version() == "6":
-                self.assertEqual(value, "?")
-            else:
-                self.assertEqual(value, "mailto:user1 at example.com")
-    
-            value = self.db._db_value_for_sql("select FBTYPE from TIMESPAN where NAME = :1", calendar_name)
-            if olddb._db_version() == "6":
-                self.assertEqual(value, "?")
-            else:
-                self.assertEqual(value, "B")
-    
-            self.db.addResource(calendar_name, Component.fromString(calendar_data), 2)
-            self.assertTrue(olddb.resourceExists(calendar_name))
-    
-            value = self.db._db_value_for_sql("select ORGANIZER from RESOURCE where NAME = :1", calendar_name)
-            self.assertEqual(value, "mailto:user1 at example.com")
-    
-            value = self.db._db_value_for_sql("select FBTYPE from TIMESPAN where NAME = :1", calendar_name)
-            self.assertEqual(value, "B")
-
 class MemcacheTests(SQLIndexTests):
     def setUp(self):
         super(MemcacheTests, self).setUp()

Modified: CalendarServer/trunk/twistedcaldav/test/test_memcacheprops.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_memcacheprops.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/test/test_memcacheprops.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -189,3 +189,94 @@
             "val0")
         self.assertEquals(child2.deadProperties().get(("ns1:", "prop3")).value,
             "val0")
+
+    def test_setget_uids(self):
+
+        for uid in (None, "123", "456"):
+            child1 = self.getColl().getChild("a")
+            child1.deadProperties().set(StubProperty("ns1:", "prop1", value="val1%s" % (uid if uid else "",)), uid=uid)
+    
+            child2 = self.getColl().getChild("a")
+            self.assertEquals(child2.deadProperties().get(("ns1:", "prop1"), uid=uid).value,
+                "val1%s" % (uid if uid else "",))
+    
+            child2.deadProperties().set(StubProperty("ns1:", "prop1", value="val2%s" % (uid if uid else "",)), uid=uid)
+    
+            # force memcache to be consulted (once per collection per request)
+            child1 = self.getColl().getChild("a")
+    
+            self.assertEquals(child1.deadProperties().get(("ns1:", "prop1"), uid=uid).value,
+                "val2%s" % (uid if uid else "",))
+
+    def test_merge_uids(self):
+
+        for uid in (None, "123", "456"):
+            child1 = self.getColl().getChild("a")
+            child2 = self.getColl().getChild("a")
+            child1.deadProperties().set(StubProperty("ns1:", "prop1", value="val0%s" % (uid if uid else "",)), uid=uid)
+            child1.deadProperties().set(StubProperty("ns1:", "prop2", value="val0%s" % (uid if uid else "",)), uid=uid)
+            child1.deadProperties().set(StubProperty("ns1:", "prop3", value="val0%s" % (uid if uid else "",)), uid=uid)
+    
+            self.assertEquals(child2.deadProperties().get(("ns1:", "prop1"), uid=uid).value,
+                "val0%s" % (uid if uid else "",))
+            self.assertEquals(child1.deadProperties().get(("ns1:", "prop2"), uid=uid).value,
+                "val0%s" % (uid if uid else "",))
+            self.assertEquals(child1.deadProperties().get(("ns1:", "prop3"), uid=uid).value,
+                "val0%s" % (uid if uid else "",))
+    
+            child2.deadProperties().set(StubProperty("ns1:", "prop1", value="val1%s" % (uid if uid else "",)), uid=uid)
+            child1.deadProperties().set(StubProperty("ns1:", "prop3", value="val3%s" % (uid if uid else "",)), uid=uid)
+    
+            # force memcache to be consulted (once per collection per request)
+            child2 = self.getColl().getChild("a")
+    
+            # verify properties
+            self.assertEquals(child2.deadProperties().get(("ns1:", "prop1"), uid=uid).value,
+                "val1%s" % (uid if uid else "",))
+            self.assertEquals(child2.deadProperties().get(("ns1:", "prop2"), uid=uid).value,
+                "val0%s" % (uid if uid else "",))
+            self.assertEquals(child2.deadProperties().get(("ns1:", "prop3"), uid=uid).value,
+                "val3%s" % (uid if uid else "",))
+    
+            self.assertEquals(child1.deadProperties().get(("ns1:", "prop1"), uid=uid).value,
+                "val1%s" % (uid if uid else "",))
+            self.assertEquals(child1.deadProperties().get(("ns1:", "prop2"), uid=uid).value,
+                "val0%s" % (uid if uid else "",))
+            self.assertEquals(child1.deadProperties().get(("ns1:", "prop3"), uid=uid).value,
+                "val3%s" % (uid if uid else "",))
+
+    def test_delete_uids(self):
+
+        for uid in (None, "123", "456"):
+            child1 = self.getColl().getChild("a")
+            child2 = self.getColl().getChild("a")
+            child1.deadProperties().set(StubProperty("ns1:", "prop1", value="val0%s" % (uid if uid else "",)), uid=uid)
+            child1.deadProperties().set(StubProperty("ns1:", "prop2", value="val0%s" % (uid if uid else "",)), uid=uid)
+            child1.deadProperties().set(StubProperty("ns1:", "prop3", value="val0%s" % (uid if uid else "",)), uid=uid)
+    
+            self.assertEquals(child2.deadProperties().get(("ns1:", "prop1"), uid=uid).value,
+                "val0%s" % (uid if uid else "",))
+            self.assertEquals(child1.deadProperties().get(("ns1:", "prop2"), uid=uid).value,
+                "val0%s" % (uid if uid else "",))
+            self.assertEquals(child1.deadProperties().get(("ns1:", "prop3"), uid=uid).value,
+                "val0%s" % (uid if uid else "",))
+    
+            child2.deadProperties().set(StubProperty("ns1:", "prop1", value="val1%s" % (uid if uid else "",)), uid=uid)
+            child1.deadProperties().delete(("ns1:", "prop1"), uid=uid)
+            self.assertRaises(HTTPError, child1.deadProperties().get, ("ns1:", "prop1"), uid=uid)
+    
+            self.assertFalse(child1.deadProperties().contains(("ns1:", "prop1"), uid=uid)) 
+            self.assertEquals(child1.deadProperties().get(("ns1:", "prop2"), uid=uid).value,
+                "val0%s" % (uid if uid else "",))
+            self.assertEquals(child1.deadProperties().get(("ns1:", "prop3"), uid=uid).value,
+                "val0%s" % (uid if uid else "",))
+    
+            # force memcache to be consulted (once per collection per request)
+            child2 = self.getColl().getChild("a")
+    
+            # verify properties
+            self.assertFalse(child2.deadProperties().contains(("ns1:", "prop1"), uid=uid)) 
+            self.assertEquals(child2.deadProperties().get(("ns1:", "prop2"), uid=uid).value,
+                "val0%s" % (uid if uid else "",))
+            self.assertEquals(child2.deadProperties().get(("ns1:", "prop3"), uid=uid).value,
+                "val0%s" % (uid if uid else "",))

Modified: CalendarServer/trunk/twistedcaldav/test/test_resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_resource.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/test/test_resource.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -22,7 +22,7 @@
 
 class StubProperty(object):
     def qname(self):
-        return "StubQname"
+        return "StubQnamespace", "StubQname"
 
 
 class CalDAVResourceTests(TestCase):
@@ -34,5 +34,5 @@
     def test_writeDeadPropertyWritesProperty(self):
         prop = StubProperty()
         self.resource.writeDeadProperty(prop)
-        self.assertEquals(self.resource._dead_properties.get("StubQname"),
+        self.assertEquals(self.resource._dead_properties.get(("StubQnamespace", "StubQname")),
                           prop)

Copied: CalendarServer/trunk/twistedcaldav/test/test_sharing.py (from rev 5440, CalendarServer/branches/users/cdaboo/shared-calendars-5187/twistedcaldav/test/test_sharing.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_sharing.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/test/test_sharing.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -0,0 +1,502 @@
+##
+# Copyright (c) 2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+
+from twext.web2 import responsecode
+from twext.web2.dav import davxml
+from twext.web2.http_headers import MimeType
+from twext.web2.stream import MemoryStream
+from twext.web2.test.test_server import SimpleRequest
+from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from twistedcaldav import customxml
+from twistedcaldav.config import config
+from twistedcaldav.static import CalDAVFile
+from twistedcaldav.test.util import InMemoryPropertyStore
+from twistedcaldav.test.util import TestCase
+import os
+from twext.web2.http import HTTPError
+
+
+class SharingTests(TestCase):
+    def setUp(self):
+        super(SharingTests, self).setUp()
+        config.Sharing.Enabled = True
+        config.Sharing.Calendars.Enabled = True
+
+        collection = self.mktemp()
+        os.mkdir(collection)
+        self.resource = CalDAVFile(collection, self.site.resource)
+        self.resource._dead_properties = InMemoryPropertyStore()
+        self.resource.writeDeadProperty(davxml.ResourceType.calendar)
+        self.site.resource.putChild("calendar", self.resource)
+        
+        self.resource.validUserIDForShare = self._fakeValidUserID
+        self.resource.sendInvite = lambda record, request:succeed(True)
+        self.resource.removeInvite = lambda record, request:succeed(True)
+        
+        class FakePrincipal(object):
+            
+            def __init__(self, cuaddr):
+                self.path = "/principals/__uids__/%s" % (cuaddr[7:].split('@')[0],)
+                self.homepath = "/calendars/__uids__/%s" % (cuaddr[7:].split('@')[0],)
+
+            def calendarHome(self):
+                class FakeHome(object):
+                    def removeShareByUID(self, request, uid):pass
+                return FakeHome()
+            
+        self.resource.principalForCalendarUserAddress = lambda cuaddr: FakePrincipal(cuaddr)
+        
+    def _fakeValidUserID(self, userid):
+        if userid.endswith("@example.com"):
+            return userid
+        else:
+            return None
+
+    def _fakeInvalidUserID(self, userid):
+        if userid.endswith("@example.net"):
+            return userid
+        else:
+            return None
+
+    @inlineCallbacks
+    def _doPOST(self, body, resultcode = responsecode.OK):
+        request = SimpleRequest(self.site, "POST", "/calendar/")
+        request.headers.setHeader("content-type", MimeType("text", "xml"))
+        request.stream = MemoryStream(body)
+
+        response = (yield self.send(request, None))
+        self.assertEqual(response.code, resultcode)
+        returnValue(response)
+
+    def _clearUIDElementValue(self, xml):
+        
+        for user in xml.children:
+            for element in user.children:
+                if type(element) == customxml.UID:
+                    element.children[0].data = ""
+        return xml
+
+    @inlineCallbacks
+    def test_upgradeToShareOnCreate(self):
+        request = SimpleRequest(self.site, "MKCOL", "/calendar/")
+
+        rtype = (yield self.resource.resourceType(request))
+        self.assertEquals(rtype, davxml.ResourceType.calendar)
+        propInvite = (yield self.resource.readProperty(customxml.Invite, request))
+        self.assertEquals(propInvite, None)
+
+        yield self.resource.upgradeToShare(request)
+
+        rtype = (yield self.resource.resourceType(request))
+        self.assertEquals(rtype, davxml.ResourceType.sharedcalendar)
+        propInvite = (yield self.resource.readProperty(customxml.Invite, request))
+        self.assertEquals(propInvite, customxml.Invite())
+        
+        isShared = (yield self.resource.isShared(request))
+        self.assertTrue(isShared)
+        isVShared = (yield self.resource.isVirtualShare(request))
+        self.assertFalse(isVShared)
+
+    @inlineCallbacks
+    def test_upgradeToShareAfterCreate(self):
+        request = SimpleRequest(self.site, "PROPPATCH", "/calendar/")
+
+        rtype = (yield self.resource.resourceType(request))
+        self.assertEquals(rtype, davxml.ResourceType.calendar)
+        propInvite = (yield self.resource.readProperty(customxml.Invite, request))
+        self.assertEquals(propInvite, None)
+
+        yield self.resource.upgradeToShare(request)
+
+        rtype = (yield self.resource.resourceType(request))
+        self.assertEquals(rtype, davxml.ResourceType.sharedcalendar)
+        propInvite = (yield self.resource.readProperty(customxml.Invite, request))
+        self.assertEquals(propInvite, customxml.Invite())
+        
+        isShared = (yield self.resource.isShared(request))
+        self.assertTrue(isShared)
+        isVShared = (yield self.resource.isVirtualShare(request))
+        self.assertFalse(isVShared)
+
+    @inlineCallbacks
+    def test_downgradeFromShare(self):
+        request = SimpleRequest(self.site, "PROPPATCH", "/calendar/")
+
+        self.resource.writeDeadProperty(davxml.ResourceType.sharedcalendar)
+        self.resource.writeDeadProperty(customxml.Invite())
+        rtype = (yield self.resource.resourceType(request))
+        self.assertEquals(rtype, davxml.ResourceType.sharedcalendar)
+        propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+        self.assertEquals(propInvite, customxml.Invite())
+
+        yield self.resource.downgradeFromShare(None)
+
+        rtype = (yield self.resource.resourceType(request))
+        self.assertEquals(rtype, davxml.ResourceType.calendar)
+        propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+        self.assertEquals(propInvite, None)
+        
+        isShared = (yield self.resource.isShared(None))
+        self.assertFalse(isShared)
+        isVShared = (yield self.resource.isVirtualShare(None))
+        self.assertFalse(isVShared)
+
+    @inlineCallbacks
+    def test_POSTaddInviteeAlreadyShared(self):
+        
+        yield self.resource.upgradeToShare(SimpleRequest(self.site, "MKCOL", "/calendar/"))
+
+        yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+    <CS:set>
+        <D:href>mailto:user02 at example.com</D:href>
+        <CS:summary>My Shared Calendar</CS:summary>
+        <CS:read-write/>
+    </CS:set>
+</CS:share>
+""")
+
+        propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+        self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
+            customxml.InviteUser(
+                customxml.UID.fromString(""),
+                davxml.HRef.fromString("mailto:user02 at example.com"),
+                customxml.InviteAccess(customxml.ReadWriteAccess()),
+                customxml.InviteStatusNoResponse(),
+            )
+        ))
+        
+        isShared = (yield self.resource.isShared(None))
+        self.assertTrue(isShared)
+        isVShared = (yield self.resource.isVirtualShare(None))
+        self.assertFalse(isVShared)
+
+    @inlineCallbacks
+    def test_POSTaddInviteeNotAlreadyShared(self):
+        
+        yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+    <CS:set>
+        <D:href>mailto:user02 at example.com</D:href>
+        <CS:summary>My Shared Calendar</CS:summary>
+        <CS:read-write/>
+    </CS:set>
+</CS:share>
+"""
+        )
+
+        propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+        self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
+            customxml.InviteUser(
+                customxml.UID.fromString(""),
+                davxml.HRef.fromString("mailto:user02 at example.com"),
+                customxml.InviteAccess(customxml.ReadWriteAccess()),
+                customxml.InviteStatusNoResponse(),
+            )
+        ))
+        
+        isShared = (yield self.resource.isShared(None))
+        self.assertTrue(isShared)
+        isVShared = (yield self.resource.isVirtualShare(None))
+        self.assertFalse(isVShared)
+
+    @inlineCallbacks
+    def test_POSTupdateInvitee(self):
+        
+        yield self.resource.upgradeToShare(SimpleRequest(self.site, "MKCOL", "/calendar/"))
+
+        yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+    <CS:set>
+        <D:href>mailto:user02 at example.com</D:href>
+        <CS:summary>My Shared Calendar</CS:summary>
+        <CS:read-write/>
+    </CS:set>
+</CS:share>
+""")
+
+        yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+    <CS:set>
+        <D:href>mailto:user02 at example.com</D:href>
+        <CS:summary>My Shared Calendar</CS:summary>
+        <CS:read/>
+    </CS:set>
+</CS:share>
+""")
+
+        propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+        self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
+            customxml.InviteUser(
+                customxml.UID.fromString(""),
+                davxml.HRef.fromString("mailto:user02 at example.com"),
+                customxml.InviteAccess(customxml.ReadAccess()),
+                customxml.InviteStatusNoResponse(),
+            )
+        ))
+
+    @inlineCallbacks
+    def test_POSTremoveInvitee(self):
+        
+        yield self.resource.upgradeToShare(SimpleRequest(self.site, "MKCOL", "/calendar/"))
+
+        yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+    <CS:set>
+        <D:href>mailto:user02 at example.com</D:href>
+        <CS:summary>My Shared Calendar</CS:summary>
+        <CS:read-write/>
+    </CS:set>
+</CS:share>
+""")
+
+        yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+    <CS:remove>
+        <D:href>mailto:user02 at example.com</D:href>
+    </CS:remove>
+</CS:share>
+""")
+
+        propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+        self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite())
+
+    @inlineCallbacks
+    def test_POSTaddMoreInvitees(self):
+        
+        yield self.resource.upgradeToShare(SimpleRequest(self.site, "MKCOL", "/calendar/"))
+
+        yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+    <CS:set>
+        <D:href>mailto:user02 at example.com</D:href>
+        <CS:summary>My Shared Calendar</CS:summary>
+        <CS:read-write/>
+    </CS:set>
+</CS:share>
+""")
+        yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+    <CS:set>
+        <D:href>mailto:user03 at example.com</D:href>
+        <CS:summary>Your Shared Calendar</CS:summary>
+        <CS:read-write/>
+    </CS:set>
+    <CS:set>
+        <D:href>mailto:user04 at example.com</D:href>
+        <CS:summary>Your Shared Calendar</CS:summary>
+        <CS:read-write/>
+    </CS:set>
+</CS:share>
+""")
+
+        propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+        self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
+            customxml.InviteUser(
+                customxml.UID.fromString(""),
+                davxml.HRef.fromString("mailto:user02 at example.com"),
+                customxml.InviteAccess(customxml.ReadWriteAccess()),
+                customxml.InviteStatusNoResponse(),
+            ),
+            customxml.InviteUser(
+                customxml.UID.fromString(""),
+                davxml.HRef.fromString("mailto:user03 at example.com"),
+                customxml.InviteAccess(customxml.ReadWriteAccess()),
+                customxml.InviteStatusNoResponse(),
+            ),
+            customxml.InviteUser(
+                customxml.UID.fromString(""),
+                davxml.HRef.fromString("mailto:user04 at example.com"),
+                customxml.InviteAccess(customxml.ReadWriteAccess()),
+                customxml.InviteStatusNoResponse(),
+            ),
+        ))
+
+    @inlineCallbacks
+    def test_POSTaddRemoveInvitees(self):
+        
+        yield self.resource.upgradeToShare(SimpleRequest(self.site, "MKCOL", "/calendar/"))
+
+        yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+    <CS:set>
+        <D:href>mailto:user02 at example.com</D:href>
+        <CS:summary>My Shared Calendar</CS:summary>
+        <CS:read-write/>
+    </CS:set>
+    <CS:set>
+        <D:href>mailto:user03 at example.com</D:href>
+        <CS:summary>My Shared Calendar</CS:summary>
+        <CS:read-write/>
+    </CS:set>
+</CS:share>
+""")
+        yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+    <CS:remove>
+        <D:href>mailto:user03 at example.com</D:href>
+    </CS:remove>
+    <CS:set>
+        <D:href>mailto:user04 at example.com</D:href>
+        <CS:summary>Your Shared Calendar</CS:summary>
+        <CS:read-write/>
+    </CS:set>
+</CS:share>
+""")
+
+        propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+        self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
+            customxml.InviteUser(
+                customxml.UID.fromString(""),
+                davxml.HRef.fromString("mailto:user02 at example.com"),
+                customxml.InviteAccess(customxml.ReadWriteAccess()),
+                customxml.InviteStatusNoResponse(),
+            ),
+            customxml.InviteUser(
+                customxml.UID.fromString(""),
+                davxml.HRef.fromString("mailto:user04 at example.com"),
+                customxml.InviteAccess(customxml.ReadWriteAccess()),
+                customxml.InviteStatusNoResponse(),
+            ),
+        ))
+
+    @inlineCallbacks
+    def test_POSTaddRemoveSameInvitee(self):
+        
+        yield self.resource.upgradeToShare(SimpleRequest(self.site, "MKCOL", "/calendar/"))
+
+        yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+    <CS:set>
+        <D:href>mailto:user02 at example.com</D:href>
+        <CS:summary>My Shared Calendar</CS:summary>
+        <CS:read-write/>
+    </CS:set>
+    <CS:set>
+        <D:href>mailto:user03 at example.com</D:href>
+        <CS:summary>My Shared Calendar</CS:summary>
+        <CS:read-write/>
+    </CS:set>
+</CS:share>
+""")
+        yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+    <CS:remove>
+        <D:href>mailto:user03 at example.com</D:href>
+    </CS:remove>
+    <CS:set>
+        <D:href>mailto:user03 at example.com</D:href>
+        <CS:summary>Your Shared Calendar</CS:summary>
+        <CS:read/>
+    </CS:set>
+</CS:share>
+""")
+
+        propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+        self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
+            customxml.InviteUser(
+                customxml.UID.fromString(""),
+                davxml.HRef.fromString("mailto:user02 at example.com"),
+                customxml.InviteAccess(customxml.ReadWriteAccess()),
+                customxml.InviteStatusNoResponse(),
+            ),
+            customxml.InviteUser(
+                customxml.UID.fromString(""),
+                davxml.HRef.fromString("mailto:user03 at example.com"),
+                customxml.InviteAccess(customxml.ReadAccess()),
+                customxml.InviteStatusNoResponse(),
+            ),
+        ))
+
+    @inlineCallbacks
+    def test_POSTaddInvalidInvitee(self):
+        
+        yield self.resource.upgradeToShare(SimpleRequest(self.site, "MKCOL", "/calendar/"))
+
+        response = (yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+    <CS:set>
+        <D:href>mailto:bogus at example.net</D:href>
+        <CS:summary>My Shared Calendar</CS:summary>
+        <CS:read-write/>
+    </CS:set>
+</CS:share>
+""",
+            responsecode.MULTI_STATUS
+        ))
+        
+        self.assertEqual(
+            str(response.stream.read()).replace("\r\n", "\n"),
+            """<?xml version='1.0' encoding='UTF-8'?>
+<multistatus xmlns='DAV:'>
+  <response>
+    <href>mailto:bogus at example.net</href>
+    <status>HTTP/1.1 403 Forbidden</status>
+  </response>
+</multistatus>"""
+        )
+
+        propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+        self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite())
+
+    @inlineCallbacks
+    def test_POSTremoveInvalidInvitee(self):
+        
+        yield self.resource.upgradeToShare(SimpleRequest(self.site, "MKCOL", "/calendar/"))
+
+        yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+    <CS:set>
+        <D:href>mailto:user01 at example.com</D:href>
+        <CS:summary>My Shared Calendar</CS:summary>
+        <CS:read-write/>
+    </CS:set>
+</CS:share>
+""")
+
+        propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+        self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
+            customxml.InviteUser(
+                customxml.UID.fromString(""),
+                davxml.HRef.fromString("mailto:user01 at example.com"),
+                customxml.InviteAccess(customxml.ReadWriteAccess()),
+                customxml.InviteStatusNoResponse(),
+            )
+        ))
+
+        self.resource.validUserIDForShare = self._fakeInvalidUserID
+
+        propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+        self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite(
+            customxml.InviteUser(
+                customxml.UID.fromString(""),
+                davxml.HRef.fromString("mailto:user01 at example.com"),
+                customxml.InviteAccess(customxml.ReadWriteAccess()),
+                customxml.InviteStatusInvalid(),
+            )
+        ))
+        
+        yield self._doPOST("""<?xml version="1.0" encoding="utf-8" ?>
+<CS:share xmlns:D="DAV:" xmlns:CS="http://calendarserver.org/ns/">
+    <CS:remove>
+        <D:href>mailto:user01 at example.com</D:href>
+    </CS:remove>
+</CS:share>
+""")
+
+        propInvite = (yield self.resource.readProperty(customxml.Invite, None))
+        self.assertEquals(self._clearUIDElementValue(propInvite), customxml.Invite())

Modified: CalendarServer/trunk/twistedcaldav/test/test_xml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_xml.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/test/test_xml.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -18,8 +18,10 @@
 
 from twisted.trial.unittest import SkipTest
 from twistedcaldav.ical import Component
-from twistedcaldav.caldavxml import *
+from twistedcaldav.query import queryfilter
 import twistedcaldav.test.util
+from twistedcaldav.caldavxml import ComponentFilter, PropertyFilter, TextMatch,\
+    Filter, TimeRange
 
 class XML (twistedcaldav.test.util.TestCase):
     """
@@ -41,11 +43,13 @@
             if has: no = "no "
             else:   no = ""
 
-            if has != ComponentFilter(
+            if has != queryfilter.ComponentFilter(
                 ComponentFilter(
-                    name=component_name
-                ),
-                name="VCALENDAR"
+                    ComponentFilter(
+                        name=component_name
+                    ),
+                    name="VCALENDAR"
+                )
             ).match(self.calendar, None):
                 self.fail("Calendar has %s%s?" % (no, component_name))
 
@@ -60,14 +64,16 @@
             if has: no = "no "
             else:   no = ""
 
-            if has != ComponentFilter(
+            if has != queryfilter.ComponentFilter(
                 ComponentFilter(
-                    PropertyFilter(
-                        name=property_name
+                    ComponentFilter(
+                        PropertyFilter(
+                            name=property_name
+                        ),
+                        name="VEVENT"
                     ),
-                    name="VEVENT"
-                ),
-                name="VCALENDAR"
+                    name="VCALENDAR"
+                )
             ).match(self.calendar, None):
                 self.fail("Calendar has %sVEVENT with %s?" % (no, property_name))
 
@@ -90,15 +96,17 @@
             if has: no = "no "
             else:   no = ""
 
-            if has != ComponentFilter(
+            if has != queryfilter.ComponentFilter(
                 ComponentFilter(
-                    PropertyFilter(
-                        TextMatch.fromString(uid, caseless=caseless),
-                        name="UID"
+                    ComponentFilter(
+                        PropertyFilter(
+                            TextMatch.fromString(uid, caseless=caseless),
+                            name="UID"
+                        ),
+                        name="VEVENT"
                     ),
-                    name="VEVENT"
-                ),
-                name="VCALENDAR"
+                    name="VCALENDAR"
+                )
             ).match(self.calendar, None):
                 self.fail("Calendar has %sVEVENT with UID %s? (caseless=%s)" % (no, uid, caseless))
 
@@ -127,13 +135,17 @@
             if has: no = "no "
             else:   no = ""
 
-            if has != Filter(ComponentFilter(
-                ComponentFilter(
-                    TimeRange(start=start, end=end),
-                    name="VEVENT"
-                ),
-                name="VCALENDAR"
-            )).match(self.calendar):
+            if has != queryfilter.Filter(
+                Filter(
+                    ComponentFilter(
+                        ComponentFilter(
+                            TimeRange(start=start, end=end),
+                            name="VEVENT"
+                        ),
+                        name="VCALENDAR"
+                    )
+                )
+            ).match(self.calendar):
                 self.fail("Calendar has %sVEVENT with timerange %s?" % (no, (start, end)))
 
     test_TimeRange.todo = "recurrence expansion"

Modified: CalendarServer/trunk/twistedcaldav/test/util.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/util.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/test/util.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -219,24 +219,38 @@
         self._properties = {}
         self.resource = _FauxResource()
 
-    def get(self, qname):
-        data = self._properties.get(qname)
+    def get(self, qname, uid=None):
+        qnameuid = qname + (uid,)
+        data = self._properties.get(qnameuid)
         if data is None:
             raise HTTPError(StatusResponse(404, "No such property"))
         return data
 
-    def set(self, property):
-        self._properties[property.qname()] = property
+    def set(self, property, uid=None):
+        qnameuid = property.qname() + (uid,)
+        self._properties[qnameuid] = property
 
-    def delete(self, qname):
+    def delete(self, qname, uid=None):
         try:
-            del self._properties[qname]
+            qnameuid = qname + (uid,)
+            del self._properties[qnameuid]
         except KeyError:
             pass
 
+    def contains(self, qname, uid=None):
+        qnameuid = qname + (uid,)
+        return qnameuid in self._properties
 
-    def list(self):
-        return self._properties.iterkeys()
+    def list(self, uid=None, filterByUID=True):
+        results = self._properties.iterkeys()
+        if filterByUID:
+            return [ 
+                (namespace, name)
+                for namespace, name, propuid in results
+                if propuid == uid
+            ]
+        else:
+            return results
 
 
 class StubCacheChangeNotifier(object):

Modified: CalendarServer/trunk/twistedcaldav/timezoneservice.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/timezoneservice.py	2010-04-07 19:59:00 UTC (rev 5441)
+++ CalendarServer/trunk/twistedcaldav/timezoneservice.py	2010-04-07 20:00:47 UTC (rev 5442)
@@ -28,10 +28,11 @@
 from twext.web2.dav import davxml
 from twext.web2.http import HTTPError
 from twext.web2.http import Response
+from twext.web2.http import XMLResponse
 from twext.web2.http_headers import MimeType
 from twext.web2.stream import MemoryStream
 
-from twext.web2.http import XMLResponse
+from twisted.internet.defer import succeed
 
 from twistedcaldav import customxml
 from twistedcaldav.customxml import calendarserver_namespace
@@ -72,8 +73,8 @@
             ),
         )
 
-    def resourceType(self):
-        return davxml.ResourceType.timezones
+    def resourceType(self, request):
+        return succeed(davxml.ResourceType.timezones)
 
     def isCollection(self):
         return False
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100407/6d5bc383/attachment-0001.html>


More information about the calendarserver-changes mailing list