[CalendarServer-changes] [5979] CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore

source_changes at macosforge.org source_changes at macosforge.org
Wed Aug 4 13:17:36 PDT 2010


Revision: 5979
          http://trac.macosforge.org/projects/calendarserver/changeset/5979
Author:   glyph at apple.com
Date:     2010-08-04 13:17:36 -0700 (Wed, 04 Aug 2010)
Log Message:
-----------
more tests passing

Modified Paths:
--------------
    CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/file.py
    CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/postgres.py

Added Paths:
-----------
    CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/util.py

Modified: CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/file.py
===================================================================
--- CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/file.py	2010-08-04 20:17:07 UTC (rev 5978)
+++ CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/file.py	2010-08-04 20:17:36 UTC (rev 5979)
@@ -14,6 +14,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
+from txcaldav.calendarstore.util import validateCalendarComponent,\
+    dropboxIDFromCalendarObject
 
 """
 File calendar store.
@@ -52,10 +54,10 @@
 from txcaldav.icalendarstore import ICalendarHome
 
 
-from txdav.common.datastore.file import CommonDataStore, CommonStoreTransaction, \
-    CommonHome, CommonHomeChild, CommonObjectResource
-from txdav.common.icommondatastore import InvalidObjectResourceError, \
-    NoSuchObjectResourceError, InternalDataStoreError
+from txdav.common.datastore.file import (CommonDataStore, 
+    CommonStoreTransaction, CommonHome, CommonHomeChild, CommonObjectResource)
+from txdav.common.icommondatastore import (NoSuchObjectResourceError,
+    InternalDataStoreError)
 from txdav.datastore.file import writeOperation, hidden
 from txdav.propertystore.base import PropertyName
 
@@ -183,12 +185,6 @@
             ),
         )
 
-    def _doValidate(self, component):
-        # FIXME: should be separate class, not separate case!
-        if self.name() == 'inbox':
-            component.validateComponentsForCalDAV(True)
-        else:
-            component.validateForCalDAV()
 
 
 class CalendarObject(CommonObjectResource):
@@ -211,24 +207,8 @@
 
     @writeOperation
     def setComponent(self, component):
-        if not isinstance(component, VComponent):
-            raise TypeError(type(component))
+        validateCalendarComponent(self, self._calendar, component)
 
-        try:
-            if component.resourceUID() != self.uid():
-                raise InvalidObjectResourceError(
-                    "UID may not change (%s != %s)" % (
-                        component.resourceUID(), self.uid()
-                     )
-                )
-        except NoSuchObjectResourceError:
-            pass
-
-        try:
-            self._calendar._doValidate(component)
-        except InvalidICalendarDataError, e:
-            raise InvalidObjectResourceError(e)
-
         newRevision = self._calendar._updateSyncToken() # FIXME: test
         self._calendar.retrieveOldIndex().addResource(
             self.name(), component, newRevision
@@ -362,22 +342,7 @@
 
 
     def dropboxID(self):
-        # FIXME: direct tests
-        dropboxProperty = self.component().getFirstPropertyInAnyComponent("X-APPLE-DROPBOX")
-        if dropboxProperty is not None:
-            componentDropboxID = dropboxProperty.value().split("/")[-1]
-            return componentDropboxID
-        attachProperty = self.component().getFirstPropertyInAnyComponent("ATTACH")
-        if attachProperty is not None:
-            # Make sure the value type is URI
-            valueType = attachProperty.params().get("VALUE", ("TEXT",))
-            if valueType[0] == "URI": 
-                # FIXME: more aggressive checking to see if this URI is really the
-                # 'right' URI.  Maybe needs to happen in the front end.
-                attachPath = attachProperty.value().split("/")[-2]
-                return attachPath
-        
-        return self.uid() + ".dropbox"
+        return dropboxIDFromCalendarObject(self)
 
 
     def _dropboxPath(self):

Modified: CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/postgres.py
===================================================================
--- CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/postgres.py	2010-08-04 20:17:07 UTC (rev 5978)
+++ CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/postgres.py	2010-08-04 20:17:36 UTC (rev 5979)
@@ -1,4 +1,4 @@
-# -*- test-case-name: txcaldav.calendarstore.test.test_postgres -*-
+# -*- test-case-name: txcaldav.calendarstore.test.test_postgres.SQLStorageTests -*-
 ##
 # Copyright (c) 2010 Apple Inc. All rights reserved.
 #
@@ -26,15 +26,21 @@
     "CalendarObject",
 ]
 
+from inspect import getargspec
+
 from twisted.python.modules import getModule
 from twisted.application.service import Service
-from txdav.idav import IDataStore
+from txcaldav.calendarstore.util import validateCalendarComponent,\
+    dropboxIDFromCalendarObject
+from txdav.common.icommondatastore import ObjectResourceNameAlreadyExistsError,\
+    HomeChildNameAlreadyExistsError, NoSuchHomeChildError,\
+    NoSuchObjectResourceError
+from txdav.idav import IDataStore, AlreadyFinishedError
 from zope.interface.declarations import implements
 from txcaldav.icalendarstore import ICalendarTransaction, ICalendarHome, \
     ICalendar, ICalendarObject
 from txdav.propertystore.base import AbstractPropertyStore, PropertyName
 from twext.web2.dav.element.parser import WebDAVDocument
-from txdav.common.icommondatastore import ObjectResourceNameAlreadyExistsError
 from txdav.propertystore.none import PropertyStore
 
 from twext.python.vcomponent import VComponent
@@ -55,16 +61,80 @@
 _BIND_MODE_OWN = 0
 
 
-class PropertyStore(AbstractPropertyStore):
+
+def _getarg(argname, argspec, args, kw):
     """
-    
+    Get an argument from some arguments.
+
+    @param argname: The name of the argument to retrieve.
+
+    @param argspec: The result of L{inspect.getargspec}.
+
+    @param args: positional arguments passed to the function specified by
+        argspec.
+
+    @param kw: keyword arguments passed to the function specified by
+        argspec.
+
+    @return: The value of the argument named by 'argname'.
     """
+    argnames = argspec[0]
+    try:
+        argpos = argnames.index(argname)
+    except ValueError:
+        argpos = None
+    if argpos is not None:
+        if len(args) > argpos:
+            return args[argpos]
+    if argname in kw:
+        return kw[argname]
+    else:
+        raise TypeError("could not find key argument %r in %r/%r (%r)" %
+            (argname, args, kw, argpos)
+        )
 
-    def __init__(self, cursor, connection, resourceID):
+
+
+def memoized(keyArgument, memoAttribute):
+    """
+    Decorator which memoizes the result of a method on that method's instance.
+
+    @param keyArgument: The name of the 'key' argument.
+
+    @type keyArgument: C{str}
+
+    @param memoAttribute: The name of the attribute on the instance which
+        should be used for memoizing the result of this method; the attribute
+        itself must be a dictionary.
+
+    @type memoAttribute: C{str}
+    """
+    def decorate(thunk):
+        spec = getargspec(thunk)
+        def outer(*a, **kw):
+            self = a[0]
+            memo = getattr(self, memoAttribute)
+            key = _getarg(keyArgument, spec, a, kw)
+            if key in memo:
+                return memo[key]
+            result = thunk(*a, **kw)
+            if result is not None:
+                memo[key] = result
+            return result
+        return outer
+    return decorate
+
+
+
+class PropertyStore(AbstractPropertyStore):
+
+    def __init__(self, peruser, defaultuser, cursor, connection, resourceID):
+        super(PropertyStore, self).__init__(peruser, defaultuser)
         self._cursor = cursor
         self._connection = connection
         self._resourceID = resourceID
 
+
     def _getitem_uid(self, key, uid):
         self._cursor.execute(
             "select VALUE from RESOURCE_PROPERTY where "
@@ -109,14 +179,19 @@
         self._calendar = calendar
         self._name = name
         self._resourceID = resid
+        self._calendarText = None
 
 
     def uid(self):
         return self.component().resourceUID()
-    
-    
+
+
+    def organizer(self):
+        return self.component().getOrganizer()
+
+
     def dropboxID(self):
-        return self.uid() + ".dropbox"
+        return dropboxIDFromCalendarObject(self)
 
 
     def name(self):
@@ -124,10 +199,15 @@
 
 
     def iCalendarText(self):
-        c = self._calendar._cursor()
-        c.execute("select ICALENDAR_TEXT from CALENDAR_OBJECT where "
-                  "RESOURCE_ID = %s", [self._resourceID])
-        return c.fetchall()[0][0]
+        if self._calendarText is None:
+            c = self._calendar._cursor()
+            c.execute("select ICALENDAR_TEXT from CALENDAR_OBJECT where "
+                      "RESOURCE_ID = %s", [self._resourceID])
+            text = c.fetchall()[0][0]
+            self._calendarText = text
+            return text
+        else:
+            return self._calendarText
 
 
     def component(self):
@@ -139,16 +219,23 @@
 
 
     def properties(self):
-        return PropertyStore(self._calendar._cursor(),
+        return PropertyStore(
+            self.uid(),
+            self.uid(),
+            self._calendar._cursor(),
             self._calendar._home._txn._connection,
-            self._resourceID)
+            self._resourceID
+        )
 
 
     def setComponent(self, component):
+        validateCalendarComponent(self, self._calendar, component)
+        calendarText = str(component)
         self._calendar._cursor().execute(
             "update CALENDAR_OBJECT set ICALENDAR_TEXT = %s "
-            "where RESOURCE_ID = %s", [str(component), self._resourceID]
+            "where RESOURCE_ID = %s", [calendarText, self._resourceID]
         )
+        self._calendarText = calendarText
 
 
     def createAttachmentWithName(self, name, contentType):
@@ -169,9 +256,6 @@
 
 
 class PostgresCalendar(object):
-    """
-    
-    """
 
     implements(ICalendar)
 
@@ -180,6 +264,7 @@
         self._home = home
         self._name = name
         self._resourceID = resourceID
+        self._objects = {}
 
 
     def _cursor(self):
@@ -190,8 +275,20 @@
         return self._name
 
     def rename(self, name):
-        raise NotImplementedError()
+        oldName = self._name
+        c = self._cursor()
+        c.execute(
+            "update CALENDAR_BIND set CALENDAR_RESOURCE_NAME = %s "
+            "where CALENDAR_RESOURCE_ID = %s AND "
+            "CALENDAR_HOME_RESOURCE_ID = %s",
+            [name, self._resourceID, self._home._resourceID]
+        )
+        self._name = name
+        # update memos
+        del self._home._calendars[oldName]
+        self._home._calendars[name] = self
 
+
     def ownerCalendarHome(self):
         return self._home
 
@@ -208,6 +305,7 @@
             yield self.calendarObjectWithName(name)
 
 
+    @memoized('name', '_objects')
     def calendarObjectWithName(self, name):
         c = self._cursor()
         c.execute("select RESOURCE_ID from CALENDAR_OBJECT where "
@@ -233,35 +331,62 @@
 
 
     def createCalendarObjectWithName(self, name, component):
-        str(component)
         c = self._cursor()
         c.execute(
-"""
-insert into CALENDAR_OBJECT
-(CALENDAR_RESOURCE_ID, RESOURCE_NAME, ICALENDAR_TEXT, ICALENDAR_UID,
- ICALENDAR_TYPE, ATTACHMENTS_MODE)
- values
-(%s, %s, %s, %s, %s, %s)
-"""
-,
-# should really be filling out more fields: ORGANIZER, ORGANIZER_OBJECT,
-# a correct ATTACHMENTS_MODE based on X-APPLE-DROPBOX
-[self._resourceID, name, str(component), component.resourceUID(),
-component.resourceType(), _ATTACHMENTS_MODE_WRITE])
+            "select RESOURCE_NAME from CALENDAR_OBJECT where "
+            " RESOURCE_NAME = %s AND CALENDAR_RESOURCE_ID = %s",
+            [name, self._resourceID]
+        )
+        rows = c.fetchall()
+        if rows:
+            raise ObjectResourceNameAlreadyExistsError()
 
+        calendarObject = PostgresCalendarObject(self, name, None)
+        calendarObject.component = lambda : component
 
+        validateCalendarComponent(calendarObject, self, component)
+
+        componentText = str(component)
+        c.execute(
+            """
+            insert into CALENDAR_OBJECT
+            (CALENDAR_RESOURCE_ID, RESOURCE_NAME, ICALENDAR_TEXT, ICALENDAR_UID,
+             ICALENDAR_TYPE, ATTACHMENTS_MODE)
+             values
+            (%s, %s, %s, %s, %s, %s)
+            """,
+            # should really be filling out more fields: ORGANIZER, ORGANIZER_OBJECT,
+            # a correct ATTACHMENTS_MODE based on X-APPLE-DROPBOX
+            [self._resourceID, name, componentText, component.resourceUID(),
+            component.resourceType(), _ATTACHMENTS_MODE_WRITE]
+        )
+
+
     def removeCalendarObjectWithName(self, name):
         c = self._cursor()
-        c.execute("delete from CALENDAR_OBJECT where RESOURCE_NAME = %s and ",
+        c.execute("delete from CALENDAR_OBJECT where RESOURCE_NAME = %s and "
                   "CALENDAR_RESOURCE_ID = %s",
                   [name, self._resourceID])
+        if c.rowcount == 0:
+            raise NoSuchObjectResourceError()
+        self._objects.pop(name, None)
 
 
     def removeCalendarObjectWithUID(self, uid):
         c = self._cursor()
-        c.execute("delete from CALENDAR_OBJECT where ICALENDAR_UID = %s and ",
+        c.execute(
+            "select RESOURCE_NAME from CALENDAR_OBJECT where "
+            "ICALENDAR_UID = %s AND CALENDAR_RESOURCE_ID = %s",
+            [uid, self._resourceID]
+        )
+        rows = c.fetchall()
+        if not rows:
+            raise NoSuchObjectResourceError()
+        name = rows[0][0]
+        c.execute("delete from CALENDAR_OBJECT where ICALENDAR_UID = %s and "
                   "CALENDAR_RESOURCE_ID = %s",
                   [uid, self._resourceID])
+        self._objects.pop(name, None)
 
 
     def syncToken(self):
@@ -280,17 +405,25 @@
 
 
     def properties(self):
-        return PropertyStore(self._cursor(), self._home._txn._connection,
-                             self._resourceID)
+        ownerUID = self.ownerCalendarHome().uid()
+        return PropertyStore(
+            ownerUID,
+            ownerUID,
+            self._cursor(), self._home._txn._connection,
+            self._resourceID
+        )
 
 
 
 class PostgresCalendarHome(object):
+
     implements(ICalendarHome)
+
     def __init__(self, transaction, ownerUID, resourceID):
         self._txn = transaction
         self._ownerUID = ownerUID
         self._resourceID = resourceID
+        self._calendars = {}
 
 
     def uid(self):
@@ -316,11 +449,12 @@
             [self._resourceID,
             _BIND_STATUS_DECLINED, ]
         )
-        names = c.fetchall()
+        names = [row[0] for row in c.fetchall()]
         for name in names:
             yield self.calendarWithName(name)
 
 
+    @memoized('name', '_calendars')
     def calendarWithName(self, name):
         """
         Retrieve the calendar with the given C{name} contained in this
@@ -351,8 +485,18 @@
                     return calendarObject
 
 
+    @memoized('name', '_calendars')
     def createCalendarWithName(self, name):
         c = self._txn._cursor
+        c.execute(
+            "select CALENDAR_RESOURCE_NAME from CALENDAR_BIND where "
+            "CALENDAR_RESOURCE_NAME = %s AND "
+            "CALENDAR_HOME_RESOURCE_ID = %s",
+            [name, self._resourceID]
+        )
+        rows = c.fetchall()
+        if rows:
+            raise HomeChildNameAlreadyExistsError()
         c.execute("select nextval('RESOURCE_ID_SEQ')")
         resourceID = c.fetchall()[0][0]
         c.execute("insert into CALENDAR (SYNC_TOKEN, RESOURCE_ID) values "
@@ -360,14 +504,14 @@
                   ['uninitialized', resourceID])
 
         c.execute("""
-        insert into CALENDAR_BIND (
-            CALENDAR_HOME_RESOURCE_ID,
-            CALENDAR_RESOURCE_ID, CALENDAR_RESOURCE_NAME, CALENDAR_MODE,
-            SEEN_BY_OWNER, SEEN_BY_SHAREE, STATUS) values (
-        %s, %s, %s, %s, %s, %s, %s)
-        """,
-        [self._resourceID, resourceID, name, _BIND_MODE_OWN, True, True,
-        _BIND_STATUS_ACCEPTED])
+            insert into CALENDAR_BIND (
+                CALENDAR_HOME_RESOURCE_ID,
+                CALENDAR_RESOURCE_ID, CALENDAR_RESOURCE_NAME, CALENDAR_MODE,
+                SEEN_BY_OWNER, SEEN_BY_SHAREE, STATUS) values (
+            %s, %s, %s, %s, %s, %s, %s)
+            """,
+            [self._resourceID, resourceID, name, _BIND_MODE_OWN, True, True,
+             _BIND_STATUS_ACCEPTED])
 
 
     def removeCalendarWithName(self, name):
@@ -375,14 +519,23 @@
         c.execute(
             "delete from CALENDAR_BIND where CALENDAR_RESOURCE_NAME = %s and "
             "CALENDAR_HOME_RESOURCE_ID = %s",
-            [name, self._resourceID])
-        # FIXME: the schema should probably cascade the delete when the last
-        # bind is deleted.
+            [name, self._resourceID]
+        )
+        self._calendars.pop(name, None)
+        if c.rowcount == 0:
+            raise NoSuchHomeChildError()
+        # FIXME: the schema should probably cascade the calendar delete when
+        # the last bind is deleted.
 
 
     def properties(self):
-        return PropertyStore(self._txn._cursor, self._txn._connection,
-                             self._resourceID)
+        return PropertyStore(
+            self.uid(),
+            self.uid(),
+            self._txn._cursor,
+            self._txn._connection,
+            self._resourceID
+        )
 
 
 
@@ -395,8 +548,17 @@
     def __init__(self, connection):
         self._connection = connection
         self._cursor = connection.cursor()
+        self._completed = False
+        self._homes = {}
 
 
+    def __del__(self):
+        if not self._completed:
+            self._connection.rollback()
+            self._connection.close()
+
+
+    @memoized('uid', '_homes')
     def calendarHomeWithUID(self, uid, create=False):
         self._cursor.execute(
             "select RESOURCE_ID from CALENDAR_HOME where OWNER_UID = %s",
@@ -415,16 +577,32 @@
         return PostgresCalendarHome(self, uid, resid)
 
 
+    def notificationsWithUID(self, uid):
+        """
+        Implement notificationsWithUID.
+        """
+        raise NotImplementedError("no notifications collection yet")
+
+
     def abort(self):
-        self._connection.rollback()
-        self._connection.close()
+        if not self._completed:
+            self._completed = True
+            self._connection.rollback()
+            self._connection.close()
+        else:
+            raise AlreadyFinishedError()
 
 
     def commit(self):
-        self._connection.commit()
-        self._connection.close()
+        if not self._completed:
+            self._completed = True
+            self._connection.commit()
+            self._connection.close()
+        else:
+            raise AlreadyFinishedError()
 
 
+
 class PostgresStore(Service, object):
 
     implements(IDataStore)

Added: CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/util.py
===================================================================
--- CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/util.py	                        (rev 0)
+++ CalendarServer/branches/users/glyph/sql-store/txcaldav/calendarstore/util.py	2010-08-04 20:17:36 UTC (rev 5979)
@@ -0,0 +1,86 @@
+##
+# 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 txdav.common.icommondatastore import InvalidObjectResourceError,\
+    NoSuchObjectResourceError
+
+"""
+Utility logic common to multiple backend implementations.
+"""
+
+from twext.python.vcomponent import InvalidICalendarDataError
+from twext.python.vcomponent import VComponent
+
+def validateCalendarComponent(calendarObject, calendar, component):
+    """
+    Validate a calendar component for a particular calendar.
+
+    @param calendarObject: The calendar object whose component will be replaced.
+    @type calendarObject: L{ICalendarObject}
+
+    @param calendar: The calendar which the L{ICalendarObject} is present in.
+    @type calendar: L{ICalendar}
+
+    @param component: The VComponent to be validated.
+    @type component: L{VComponent}
+    """
+
+    if not isinstance(component, VComponent):
+        raise TypeError(type(component))
+
+    try:
+        if component.resourceUID() != calendarObject.uid():
+            raise InvalidObjectResourceError(
+                "UID may not change (%s != %s)" % (
+                    component.resourceUID(), calendarObject.uid()
+                 )
+            )
+    except NoSuchObjectResourceError:
+        pass
+
+    try:
+        # FIXME: This is a bad way to do this test, there should be a
+        # Calendar-level API for it.
+        if calendar.name() == 'inbox':
+            component.validateComponentsForCalDAV(True)
+        else:
+            component.validateForCalDAV()
+    except InvalidICalendarDataError, e:
+        raise InvalidObjectResourceError(e)
+
+
+def dropboxIDFromCalendarObject(calendarObject):
+    """
+    Helper to implement L{ICalendarObject.dropboxID}.
+
+    @param calendarObject: The calendar object to retrieve a dropbox ID for.
+    @type calendarObject: L{ICalendarObject}
+    """
+    dropboxProperty = calendarObject.component(
+        ).getFirstPropertyInAnyComponent("X-APPLE-DROPBOX")
+    if dropboxProperty is not None:
+        componentDropboxID = dropboxProperty.value().split("/")[-1]
+        return componentDropboxID
+    attachProperty = calendarObject.component().getFirstPropertyInAnyComponent("ATTACH")
+    if attachProperty is not None:
+        # Make sure the value type is URI
+        valueType = attachProperty.params().get("VALUE", ("TEXT",))
+        if valueType[0] == "URI": 
+            # FIXME: more aggressive checking to see if this URI is really the
+            # 'right' URI.  Maybe needs to happen in the front end.
+            attachPath = attachProperty.value().split("/")[-2]
+            return attachPath
+    
+    return calendarObject.uid() + ".dropbox"
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100804/bf987a38/attachment-0001.html>


More information about the calendarserver-changes mailing list