[CalendarServer-changes] [6184] CalendarServer/branches/generic-sqlstore
source_changes at macosforge.org
source_changes at macosforge.org
Wed Aug 25 17:52:31 PDT 2010
Revision: 6184
http://trac.macosforge.org/projects/calendarserver/changeset/6184
Author: cdaboo at apple.com
Date: 2010-08-25 17:52:30 -0700 (Wed, 25 Aug 2010)
Log Message:
-----------
Major re-factor of classes out of postgres.py into common and separate modules.
Modified Paths:
--------------
CalendarServer/branches/generic-sqlstore/calendarserver/tap/caldav.py
CalendarServer/branches/generic-sqlstore/calendarserver/tap/util.py
CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/file.py
CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/util.py
CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/file.py
CalendarServer/branches/generic-sqlstore/txdav/common/datastore/file.py
CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql.py
CalendarServer/branches/generic-sqlstore/txdav/common/inotifications.py
CalendarServer/branches/generic-sqlstore/txdav/datastore/file.py
Added Paths:
-----------
CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/sql.py
CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/test/test_sql.py
CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/sql.py
CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/test/test_sql.py
CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/util.py
CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_legacy.py
CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_schema_v1.sql
CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_tables.py
CalendarServer/branches/generic-sqlstore/txdav/common/datastore/test/
CalendarServer/branches/generic-sqlstore/txdav/common/datastore/test/__init__.py
CalendarServer/branches/generic-sqlstore/txdav/common/datastore/test/util.py
CalendarServer/branches/generic-sqlstore/txdav/datastore/util.py
Removed Paths:
-------------
CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/postgres.py
CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/postgres_schema_v1.sql
CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/test/test_postgres.py
Modified: CalendarServer/branches/generic-sqlstore/calendarserver/tap/caldav.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/calendarserver/tap/caldav.py 2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/calendarserver/tap/caldav.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -88,7 +88,7 @@
from calendarserver.tap.util import getRootResource, computeProcessCount
from calendarserver.tools.util import checkDirectory
-from txcaldav.calendarstore.postgres import v1_schema
+from txdav.common.datastore.sql import v1_schema
from txdav.datastore.subpostgres import PostgresService
from twext.python.filepath import CachingFilePath
Modified: CalendarServer/branches/generic-sqlstore/calendarserver/tap/util.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/calendarserver/tap/util.py 2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/calendarserver/tap/util.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -69,7 +69,7 @@
from txdav.common.datastore.sql import CommonDataStore as CommonSQLDataStore
from txdav.common.datastore.file import CommonDataStore as CommonFileDataStore
-from txcaldav.calendarstore.postgres import v1_schema
+from txdav.common.datastore.sql import v1_schema
from txdav.datastore.subpostgres import PostgresService
from twext.python.filepath import CachingFilePath
Modified: CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/file.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/file.py 2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/file.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -227,8 +227,8 @@
@writeOperation
- def setComponent(self, component):
- validateCalendarComponent(self, self._calendar, component)
+ def setComponent(self, component, inserting=False):
+ validateCalendarComponent(self, self._calendar, component, inserting)
self._calendar.retrieveOldIndex().addResource(
self.name(), component
Deleted: CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/postgres.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/postgres.py 2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/postgres.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -1,3013 +0,0 @@
-# -*- test-case-name: txcaldav.calendarstore.test.test_postgres -*-
-##
-# 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.
-##
-
-"""
-PostgreSQL data store.
-"""
-
-__all__ = [
- "PostgresCalendarHome",
- "PostgresCalendar",
- "PostgresCalendarObject",
- "PostgresAddressBookHome",
- "PostgresAddressBook",
- "PostgresAddressBookObject",
-]
-
-import datetime
-import StringIO
-
-from twistedcaldav.sharing import SharedCollectionRecord #@UnusedImport
-
-from zope.interface.declarations import implements
-
-from twisted.internet.error import ConnectionLost
-from twisted.internet.interfaces import ITransport
-from twisted.python import hashlib
-from twisted.python.failure import Failure
-from twisted.internet.defer import succeed
-from twisted.python.modules import getModule
-
-from twext.web2.dav.element.rfc2518 import ResourceType
-
-from txdav.common.inotifications import (INotificationCollection,
- INotificationObject)
-
-from txdav.common.icommondatastore import (
- ObjectResourceNameAlreadyExistsError, HomeChildNameAlreadyExistsError,
- NoSuchHomeChildError, NoSuchObjectResourceError)
-from txcaldav.calendarstore.util import (validateCalendarComponent,
- validateAddressBookComponent, dropboxIDFromCalendarObject, CalendarSyncTokenHelper,
- AddressbookSyncTokenHelper)
-from txdav.datastore.file import cached
-from txdav.datastore.sql import memoized
-
-from txcaldav.icalendarstore import (ICalendarHome, ICalendar, ICalendarObject, IAttachment)
-from txcarddav.iaddressbookstore import (IAddressBookHome, IAddressBook, IAddressBookObject)
-from txdav.propertystore.base import PropertyName
-from txdav.propertystore.sql import PropertyStore
-
-from twext.web2.http_headers import MimeType, generateContentType
-
-from twext.python.log import Logger, LoggingMixIn
-from twext.python.vcomponent import VComponent
-
-from twistedcaldav import carddavxml
-from twistedcaldav.config import config
-from twistedcaldav.caldavxml import ScheduleCalendarTransp, Opaque
-from twistedcaldav.customxml import NotificationType
-from twistedcaldav.dateops import normalizeForIndex
-from twistedcaldav.index import IndexedSearchException, ReservationError,\
- SyncTokenValidException
-from twistedcaldav.instance import InvalidOverriddenInstanceError
-from twistedcaldav.memcachepool import CachePoolUserMixIn
-from twistedcaldav.notifications import NotificationRecord
-from twistedcaldav.query import calendarqueryfilter, calendarquery, \
- addressbookquery
-from twistedcaldav.query.sqlgenerator import sqlgenerator
-from twistedcaldav.sharing import Invite
-from twistedcaldav.vcard import Component as VCard
-
-from vobject.icalendar import utc
-
-v1_schema = getModule(__name__).filePath.sibling(
- "postgres_schema_v1.sql").getContent()
-
-log = Logger()
-
-# FIXME: these constants are in the schema, and should probably be discovered
-# from there somehow.
-
-_BIND_STATUS_INVITED = 0
-_BIND_STATUS_ACCEPTED = 1
-_BIND_STATUS_DECLINED = 2
-_BIND_STATUS_INVALID = 3
-
-_ATTACHMENTS_MODE_WRITE = 1
-
-_BIND_MODE_OWN = 0
-_BIND_MODE_READ = 1
-_BIND_MODE_WRITE = 2
-
-
-#
-# Duration into the future through which recurrences are expanded in the index
-# by default. This is a caching parameter which affects the size of the index;
-# it does not affect search results beyond this period, but it may affect
-# performance of such a search.
-#
-default_future_expansion_duration = datetime.timedelta(days=365 * 1)
-
-#
-# Maximum duration into the future through which recurrences are expanded in the
-# index. This is a caching parameter which affects the size of the index; it
-# does not affect search results beyond this period, but it may affect
-# performance of such a search.
-#
-# When a search is performed on a time span that goes beyond that which is
-# expanded in the index, we have to open each resource which may have data in
-# that time period. In order to avoid doing that multiple times, we want to
-# cache those results. However, we don't necessarily want to cache all
-# occurrences into some obscenely far-in-the-future date, so we cap the caching
-# period. Searches beyond this period will always be relatively expensive for
-# resources with occurrences beyond this period.
-#
-maximum_future_expansion_duration = datetime.timedelta(days=365 * 5)
-
-icalfbtype_to_indexfbtype = {
- "UNKNOWN" : 0,
- "FREE" : 1,
- "BUSY" : 2,
- "BUSY-UNAVAILABLE": 3,
- "BUSY-TENTATIVE" : 4,
-}
-
-indexfbtype_to_icalfbtype = {
- 0: '?',
- 1: 'F',
- 2: 'B',
- 3: 'U',
- 4: 'T',
-}
-
-
-class PostgresCalendarObject(object):
- implements(ICalendarObject)
-
- def __init__(self, calendar, name, resid):
- self._calendar = calendar
- self._name = name
- self._resourceID = resid
- self._calendarText = None
-
-
- @property
- def _txn(self):
- return self._calendar._txn
-
-
- def uid(self):
- return self.component().resourceUID()
-
-
- def organizer(self):
- return self.component().getOrganizer()
-
-
- def dropboxID(self):
- return dropboxIDFromCalendarObject(self)
-
-
- def name(self):
- return self._name
-
-
- def calendar(self):
- return self._calendar
-
-
- def iCalendarText(self):
- if self._calendarText is None:
- text = self._txn.execSQL(
- "select ICALENDAR_TEXT from CALENDAR_OBJECT where "
- "RESOURCE_ID = %s", [self._resourceID]
- )[0][0]
- self._calendarText = text
- return text
- else:
- return self._calendarText
-
-
- def component(self):
- return VComponent.fromString(self.iCalendarText())
-
-
- def componentType(self):
- return self.component().mainType()
-
-
- @cached
- def properties(self):
- return PropertyStore(
- self.uid(),
- self._txn,
- self._resourceID
- )
-
-
- def setComponent(self, component):
- validateCalendarComponent(self, self._calendar, component)
-
- self.updateDatabase(component)
- self._calendar._updateRevision(self._name)
-
- if self._calendar._notifier:
- self._calendar._home._txn.postCommit(self._calendar._notifier.notify)
-
- def updateDatabase(self, component, expand_until=None, reCreate=False, inserting=False):
- """
- Update the database tables for the new data being written.
-
- @param component: calendar data to store
- @type component: L{Component}
- """
-
- # Decide how far to expand based on the component
- master = component.masterComponent()
- if master is None or not component.isRecurring() and not component.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 = component.expandTimeRanges(expand, ignoreInvalidInstances=reCreate)
- except InvalidOverriddenInstanceError, e:
- log.err("Invalid instance %s when indexing %s in %s" % (e.rid, self._name, self.resource,))
- raise
-
- componentText = str(component)
- self._calendarText = componentText
- organizer = component.getOrganizer()
- if not organizer:
- organizer = ""
-
- # CALENDAR_OBJECT table update
- if inserting:
- self._resourceID = self._txn.execSQL(
- """
- insert into CALENDAR_OBJECT
- (CALENDAR_RESOURCE_ID, RESOURCE_NAME, ICALENDAR_TEXT, ICALENDAR_UID, ICALENDAR_TYPE, ATTACHMENTS_MODE, ORGANIZER, RECURRANCE_MAX)
- values
- (%s, %s, %s, %s, %s, %s, %s, %s)
- returning RESOURCE_ID
- """,
- # FIXME: correct ATTACHMENTS_MODE based on X-APPLE-
- # DROPBOX
- [
- self._calendar._resourceID,
- self._name,
- componentText,
- component.resourceUID(),
- component.resourceType(),
- _ATTACHMENTS_MODE_WRITE,
- organizer,
- normalizeForIndex(instances.limit) if instances.limit else None,
- ]
- )[0][0]
- else:
- self._txn.execSQL(
- """
- update CALENDAR_OBJECT set
- (ICALENDAR_TEXT, ICALENDAR_UID, ICALENDAR_TYPE, ATTACHMENTS_MODE, ORGANIZER, RECURRANCE_MAX, MODIFIED)
- =
- (%s, %s, %s, %s, %s, %s, timezone('UTC', CURRENT_TIMESTAMP))
- where RESOURCE_ID = %s
- """,
- # should really be filling out more fields: ORGANIZER,
- # ORGANIZER_OBJECT, a correct ATTACHMENTS_MODE based on X-APPLE-
- # DROPBOX
- [
- componentText,
- component.resourceUID(),
- component.resourceType(),
- _ATTACHMENTS_MODE_WRITE,
- organizer,
- normalizeForIndex(instances.limit) if instances.limit else None,
- self._resourceID
- ]
- )
-
- # Need to wipe the existing time-range for this and rebuild
- self._txn.execSQL(
- """
- delete from TIME_RANGE where CALENDAR_OBJECT_RESOURCE_ID = %s
- """,
- [
- self._resourceID,
- ],
- )
-
-
- # CALENDAR_OBJECT table update
- for key in instances:
- instance = instances[key]
- start = instance.start.replace(tzinfo=utc)
- end = instance.end.replace(tzinfo=utc)
- float = instance.start.tzinfo is None
- transp = instance.component.propertyValue("TRANSP") == "TRANSPARENT"
- instanceid = self._txn.execSQL(
- """
- insert into TIME_RANGE
- (CALENDAR_RESOURCE_ID, CALENDAR_OBJECT_RESOURCE_ID, FLOATING, START_DATE, END_DATE, FBTYPE, TRANSPARENT)
- values
- (%s, %s, %s, %s, %s, %s, %s)
- returning
- INSTANCE_ID
- """,
- [
- self._calendar._resourceID,
- self._resourceID,
- float,
- start,
- end,
- icalfbtype_to_indexfbtype.get(instance.component.getFBType(), icalfbtype_to_indexfbtype["FREE"]),
- transp,
- ],
- )[0][0]
- peruserdata = component.perUserTransparency(instance.rid)
- for useruid, transp in peruserdata:
- self._txn.execSQL(
- """
- insert into TRANSPARENCY
- (TIME_RANGE_INSTANCE_ID, USER_ID, TRANSPARENT)
- values
- (%s, %s, %s)
- """,
- [
- instanceid,
- useruid,
- transp,
- ],
- )
-
- # Special - for unbounded recurrence we insert a value for "infinity"
- # that will allow an open-ended time-range to always match it.
- if component.isRecurringUnbounded():
- start = datetime.datetime(2100, 1, 1, 0, 0, 0, tzinfo=utc)
- end = datetime.datetime(2100, 1, 1, 1, 0, 0, tzinfo=utc)
- float = False
- instanceid = self._txn.execSQL(
- """
- insert into TIME_RANGE
- (CALENDAR_RESOURCE_ID, CALENDAR_OBJECT_RESOURCE_ID, FLOATING, START_DATE, END_DATE, FBTYPE, TRANSPARENT)
- values
- (%s, %s, %s, %s, %s, %s, %s)
- returning
- INSTANCE_ID
- """,
- [
- self._calendar._resourceID,
- self._resourceID,
- float,
- start,
- end,
- icalfbtype_to_indexfbtype["UNKNOWN"],
- True,
- ],
- )[0][0]
- peruserdata = component.perUserTransparency(None)
- for useruid, transp in peruserdata:
- self._txn.execSQL(
- """
- insert into TRANSPARENCY
- (TIME_RANGE_INSTANCE_ID, USER_ID, TRANSPARENT)
- values
- (%s, %s, %s)
- """,
- [
- instanceid,
- useruid,
- transp,
- ],
- )
-
- def _attachmentPath(self, name):
- attachmentRoot = self._calendar._home._txn._store.attachmentsPath
- try:
- attachmentRoot.createDirectory()
- except:
- pass
- return attachmentRoot.child(
- "%s-%s-%s-%s.attachment" % (
- self._calendar._home.uid(), self._calendar.name(),
- self.name(), name
- )
- )
-
-
- def createAttachmentWithName(self, name, contentType):
- path = self._attachmentPath(name)
- attachment = PostgresAttachment(self, path)
- self._txn.execSQL("""
- insert into ATTACHMENT (CALENDAR_OBJECT_RESOURCE_ID, CONTENT_TYPE,
- SIZE, MD5, PATH)
- values (%s, %s, %s, %s, %s)
- """,
- [
- self._resourceID, generateContentType(contentType), 0, "",
- attachment._pathValue()
- ]
- )
- return attachment.store(contentType)
-
-
- def attachments(self):
- rows = self._txn.execSQL("""
- select PATH from ATTACHMENT where CALENDAR_OBJECT_RESOURCE_ID = %s
- """, [self._resourceID])
- for row in rows:
- demangledName = _pathToName(row[0])
- yield self.attachmentWithName(demangledName)
-
-
- def attachmentWithName(self, name):
- attachment = PostgresAttachment(self, self._attachmentPath(name))
- if attachment._populate():
- return attachment
- else:
- return None
-
-
- def removeAttachmentWithName(self, name):
- attachment = PostgresAttachment(self, self._attachmentPath(name))
- self._calendar._home._txn.postCommit(attachment._path.remove)
- self._txn.execSQL("""
- delete from ATTACHMENT where CALENDAR_OBJECT_RESOURCE_ID = %s AND
- PATH = %s
- """, [self._resourceID, attachment._pathValue()])
-
-
- # IDataStoreResource
- def contentType(self):
- """
- The content type of Calendar objects is text/calendar.
- """
- return MimeType.fromString("text/calendar; charset=utf-8")
-
-
- def md5(self):
- return None
-
-
- def size(self):
- size = self._txn.execSQL(
- "select character_length(ICALENDAR_TEXT) from CALENDAR_OBJECT where "
- "RESOURCE_ID = %s", [self._resourceID]
- )[0][0]
- return size
-
-
- def created(self):
- created = self._txn.execSQL(
- "select extract(EPOCH from CREATED) from CALENDAR_OBJECT where "
- "RESOURCE_ID = %s", [self._resourceID]
- )[0][0]
- return int(created)
-
- def modified(self):
- modified = self._txn.execSQL(
- "select extract(EPOCH from MODIFIED) from CALENDAR_OBJECT where "
- "RESOURCE_ID = %s", [self._resourceID]
- )[0][0]
- return int(modified)
-
-
- def attendeesCanManageAttachments(self):
- return self.component().hasPropertyInAnyComponent("X-APPLE-DROPBOX")
-
-
-
-def _pathToName(path):
- return path.rsplit(".", 1)[0].split("-", 3)[-1]
-
-
-
-class PostgresAttachment(object):
-
- implements(IAttachment)
-
- def __init__(self, calendarObject, path):
- self._calendarObject = calendarObject
- self._path = path
-
-
- @property
- def _txn(self):
- return self._calendarObject._txn
-
-
- def _populate(self):
- """
- Execute necessary SQL queries to retrieve attributes.
-
- @return: C{True} if this attachment exists, C{False} otherwise.
- """
- rows = self._txn.execSQL(
- """
- select CONTENT_TYPE, SIZE, MD5, extract(EPOCH from CREATED), extract(EPOCH from MODIFIED) from ATTACHMENT where PATH = %s
- """, [self._pathValue()])
- if not rows:
- return False
- self._contentType = MimeType.fromString(rows[0][0])
- self._size = rows[0][1]
- self._md5 = rows[0][2]
- self._created = int(rows[0][3])
- self._modified = int(rows[0][4])
- return True
-
-
- def store(self, contentType):
- return PostgresAttachmentStorageTransport(self, contentType)
-
-
- def retrieve(self, protocol):
- protocol.dataReceived(self._path.getContent())
- protocol.connectionLost(Failure(ConnectionLost()))
-
-
- def properties(self):
- pass # stub
-
-
- # IDataStoreResource
- def contentType(self):
- return self._contentType
-
-
- def md5(self):
- return self._md5
-
-
- def size(self):
- return self._size
-
-
- def created(self):
- return self._created
-
- def modified(self):
- return self._modified
-
-
- def name(self):
- return _pathToName(self._pathValue())
-
-
- def _pathValue(self):
- """
- Compute the value which should go into the 'path' column for this
- attachment.
- """
- root = self._calendarObject._calendar._home._txn._store.attachmentsPath
- return '/'.join(self._path.segmentsFrom(root))
-
-
-
-class PostgresAttachmentStorageTransport(object):
-
- implements(ITransport)
-
- def __init__(self, attachment, contentType):
- self.attachment = attachment
- self.contentType = contentType
- self.buf = ''
- self.hash = hashlib.md5()
-
-
- @property
- def _txn(self):
- return self.attachment._txn
-
-
- def write(self, data):
- self.buf += data
- self.hash.update(data)
-
-
- def loseConnection(self):
- self.attachment._path.setContent(self.buf)
- pathValue = self.attachment._pathValue()
- contentTypeString = generateContentType(self.contentType)
- self._txn.execSQL(
- "update ATTACHMENT set CONTENT_TYPE = %s, SIZE = %s, MD5 = %s, MODIFIED = timezone('UTC', CURRENT_TIMESTAMP) "
- "WHERE PATH = %s",
- [contentTypeString, len(self.buf), self.hash.hexdigest(), pathValue]
- )
-
-
-
-class PostgresLegacyInvitesEmulator(object):
- """
- Emulator for the implicit interface specified by
- L{twistedcaldav.sharing.InvitesDatabase}.
- """
-
-
- def __init__(self, calendar):
- self._calendar = calendar
-
-
- @property
- def _txn(self):
- return self._calendar._txn
-
-
- def create(self):
- "No-op, because the index implicitly always exists in the database."
-
-
- def remove(self):
- "No-op, because the index implicitly always exists in the database."
-
-
- def allRecords(self):
- for row in self._txn.execSQL(
- """
- select
- INVITE.INVITE_UID, INVITE.NAME, INVITE.RECIPIENT_ADDRESS,
- CALENDAR_HOME.OWNER_UID, CALENDAR_BIND.BIND_MODE,
- CALENDAR_BIND.BIND_STATUS, CALENDAR_BIND.MESSAGE
- from
- INVITE, CALENDAR_HOME, CALENDAR_BIND
- where
- INVITE.RESOURCE_ID = %s and
- INVITE.HOME_RESOURCE_ID =
- CALENDAR_HOME.RESOURCE_ID and
- CALENDAR_BIND.CALENDAR_RESOURCE_ID =
- INVITE.RESOURCE_ID and
- CALENDAR_BIND.CALENDAR_HOME_RESOURCE_ID =
- INVITE.HOME_RESOURCE_ID
- order by
- INVITE.NAME asc
- """, [self._calendar._resourceID]):
- [inviteuid, common_name, userid, ownerUID,
- bindMode, bindStatus, summary] = row
- # FIXME: this is really the responsibility of the protocol layer.
- state = {
- _BIND_STATUS_INVITED: "NEEDS-ACTION",
- _BIND_STATUS_ACCEPTED: "ACCEPTED",
- _BIND_STATUS_DECLINED: "DECLINED",
- _BIND_STATUS_INVALID: "INVALID",
- }[bindStatus]
- access = {
- _BIND_MODE_READ: "read-only",
- _BIND_MODE_WRITE: "read-write"
- }[bindMode]
- principalURL = "/principals/__uids__/%s/" % (ownerUID,)
- yield Invite(
- inviteuid, userid, principalURL, common_name,
- access, state, summary
- )
-
-
- def recordForUserID(self, userid):
- for record in self.allRecords():
- if record.userid == userid:
- return record
-
-
- def recordForPrincipalURL(self, principalURL):
- for record in self.allRecords():
- if record.principalURL == principalURL:
- return record
-
-
- def recordForInviteUID(self, inviteUID):
- for record in self.allRecords():
- if record.inviteuid == inviteUID:
- return record
-
-
- def addOrUpdateRecord(self, record):
- bindMode = {'read-only': _BIND_MODE_READ,
- 'read-write': _BIND_MODE_WRITE}[record.access]
- bindStatus = {
- "NEEDS-ACTION": _BIND_STATUS_INVITED,
- "ACCEPTED": _BIND_STATUS_ACCEPTED,
- "DECLINED": _BIND_STATUS_DECLINED,
- "INVALID": _BIND_STATUS_INVALID,
- }[record.state]
- # principalURL is derived from a directory record's principalURL() so
- # it will always contain the UID. The form is '/principals/__uids__/x'
- # (and may contain a trailing slash).
- principalUID = record.principalURL.split("/")[3]
- shareeHome = self._txn.calendarHomeWithUID(principalUID, create=True)
- rows = self._txn.execSQL(
- "select RESOURCE_ID, HOME_RESOURCE_ID from INVITE where RECIPIENT_ADDRESS = %s",
- [record.userid]
- )
- if rows:
- [[resourceID, homeResourceID]] = rows
- # Invite(inviteuid, userid, principalURL, common_name, access, state, summary)
- self._txn.execSQL("""
- update CALENDAR_BIND set BIND_MODE = %s,
- BIND_STATUS = %s, MESSAGE = %s
- where
- CALENDAR_RESOURCE_ID = %s and
- CALENDAR_HOME_RESOURCE_ID = %s
- """, [bindMode, bindStatus, record.summary,
- resourceID, homeResourceID])
- self._txn.execSQL("""
- update INVITE set NAME = %s, INVITE_UID = %s
- where RECIPIENT_ADDRESS = %s
- """,
- [record.name, record.inviteuid, record.userid]
- )
- else:
- self._txn.execSQL(
- """
- insert into INVITE (
- INVITE_UID, NAME,
- HOME_RESOURCE_ID, RESOURCE_ID,
- RECIPIENT_ADDRESS
- )
- values (%s, %s, %s, %s, %s)
- """,
- [
- record.inviteuid, record.name,
- shareeHome._resourceID, self._calendar._resourceID,
- record.userid
- ])
- self._txn.execSQL(
- """
- insert into CALENDAR_BIND
- (
- CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID,
- CALENDAR_RESOURCE_NAME, BIND_MODE, BIND_STATUS,
- SEEN_BY_OWNER, SEEN_BY_SHAREE, MESSAGE
- )
- values (%s, %s, %s, %s, %s, %s, %s, %s)
- """,
- [
- shareeHome._resourceID,
- self._calendar._resourceID,
- None, # this is NULL because it is not bound yet, let's be
- # explicit about that.
- bindMode,
- bindStatus,
- False,
- False,
- record.summary
- ])
-
-
- def removeRecordForUserID(self, userid):
- rec = self.recordForUserID(userid)
- self.removeRecordForInviteUID(rec.inviteuid)
-
-
- def removeRecordForPrincipalURL(self, principalURL):
- raise NotImplementedError("removeRecordForPrincipalURL")
-
-
- def removeRecordForInviteUID(self, inviteUID):
- rows = self._txn.execSQL("""
- select HOME_RESOURCE_ID, RESOURCE_ID from INVITE where
- INVITE_UID = %s
- """, [inviteUID])
- if rows:
- [[homeID, resourceID]] = rows
- self._txn.execSQL(
- "delete from CALENDAR_BIND where "
- "CALENDAR_HOME_RESOURCE_ID = %s and CALENDAR_RESOURCE_ID = %s",
- [homeID, resourceID])
- self._txn.execSQL("delete from INVITE where INVITE_UID = %s",
- [inviteUID])
-
-
-
-class PostgresLegacySharesEmulator(object):
-
- def __init__(self, home):
- self._home = home
-
-
- @property
- def _txn(self):
- return self._home._txn
-
-
- def create(self):
- pass
-
-
- def remove(self):
- pass
-
-
- def allRecords(self):
- # This should have been a smart join that got all these columns at
- # once, but let's not bother to fix it, since the actual query we
- # _want_ to do (just look for calendar binds in a particular homes) is
- # much simpler anyway; we should just do that.
- shareRows = self._txn.execSQL(
- """
- select CALENDAR_RESOURCE_ID, CALENDAR_RESOURCE_NAME, MESSAGE
- from CALENDAR_BIND
- where CALENDAR_HOME_RESOURCE_ID = %s and
- BIND_MODE != %s and
- CALENDAR_RESOURCE_NAME is not null
- """, [self._home._resourceID, _BIND_MODE_OWN])
- for resourceID, resourceName, summary in shareRows:
- [[shareuid]] = self._txn.execSQL(
- """
- select INVITE_UID
- from INVITE
- where RESOURCE_ID = %s and HOME_RESOURCE_ID = %s
- """, [resourceID, self._home._resourceID])
- sharetype = 'I'
- [[ownerHomeID, ownerResourceName]] = self._txn.execSQL(
- """
- select CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_NAME
- from CALENDAR_BIND
- where CALENDAR_RESOURCE_ID = %s and
- BIND_MODE = %s
- """, [resourceID, _BIND_MODE_OWN]
- )
- [[ownerUID]] = self._txn.execSQL(
- "select OWNER_UID from CALENDAR_HOME where RESOURCE_ID = %s",
- [ownerHomeID])
- hosturl = '/calendars/__uids__/%s/%s' % (
- ownerUID, ownerResourceName
- )
- localname = resourceName
- record = SharedCollectionRecord(
- shareuid, sharetype, hosturl, localname, summary
- )
- yield record
-
-
- def _search(self, **kw):
- [[key, value]] = kw.items()
- for record in self.allRecords():
- if getattr(record, key) == value:
- return record
-
- def recordForLocalName(self, localname):
- return self._search(localname=localname)
-
- def recordForShareUID(self, shareUID):
- return self._search(shareuid=shareUID)
-
-
- def addOrUpdateRecord(self, record):
-# print '*** SHARING***: Adding or updating this record:'
-# import pprint
-# pprint.pprint(record.__dict__)
- # record.hosturl -> /calendars/__uids__/<uid>/<calendarname>
- splithost = record.hosturl.split('/')
- ownerUID = splithost[3]
- ownerCalendarName = splithost[4]
- ownerHome = self._txn.calendarHomeWithUID(ownerUID)
- ownerCalendar = ownerHome.calendarWithName(ownerCalendarName)
- calendarResourceID = ownerCalendar._resourceID
-
- # There needs to be a bind already, one that corresponds to the
- # invitation. The invitation's UID is the same as the share UID. I
- # just need to update its 'localname', i.e.
- # CALENDAR_BIND.CALENDAR_RESOURCE_NAME.
-
- self._txn.execSQL(
- """
- update CALENDAR_BIND set CALENDAR_RESOURCE_NAME = %s
- where CALENDAR_HOME_RESOURCE_ID = %s and CALENDAR_RESOURCE_ID = %s
- """,
- [record.localname, self._home._resourceID, calendarResourceID]
- )
-
-
- def removeRecordForLocalName(self, localname):
- self._txn.execSQL(
- "delete from CALENDAR_BIND where CALENDAR_RESOURCE_NAME = %s "
- "and CALENDAR_HOME_RESOURCE_ID = %s",
- [localname, self._home._resourceID]
- )
-
-
- def removeRecordForShareUID(self, shareUID):
- pass
-# c = self._home._cursor()
-# c.execute(
-# "delete from CALENDAR_BIND where CALENDAR_RESOURCE_NAME = %s "
-# "and CALENDAR_HOME_RESOURCE_ID = %s",
-# [self._home._resourceID]
-# )
-
-
-
-class postgresqlgenerator(sqlgenerator):
- """
- Query generator for postgreSQL indexed searches. (Currently unused: work
- in progress.)
- """
-
- ISOP = " = "
- CONTAINSOP = " LIKE "
- NOTCONTAINSOP = " NOT LIKE "
- FIELDS = {
- "TYPE": "CALENDAR_OBJECT.ICALENDAR_TYPE",
- "UID": "CALENDAR_OBJECT.ICALENDAR_UID",
- }
-
- def __init__(self, expr, calendarid, userid):
- self.RESOURCEDB = "CALENDAR_OBJECT"
- self.TIMESPANDB = "TIME_RANGE"
- self.TIMESPANTEST = "((TIME_RANGE.FLOATING = FALSE AND TIME_RANGE.START_DATE < %s AND TIME_RANGE.END_DATE > %s) OR (TIME_RANGE.FLOATING = TRUE AND TIME_RANGE.START_DATE < %s AND TIME_RANGE.END_DATE > %s))"
- self.TIMESPANTEST_NOEND = "((TIME_RANGE.FLOATING = FALSE AND TIME_RANGE.END_DATE > %s) OR (TIME_RANGE.FLOATING = TRUE AND TIME_RANGE.END_DATE > %s))"
- self.TIMESPANTEST_NOSTART = "((TIME_RANGE.FLOATING = FALSE AND TIME_RANGE.START_DATE < %s) OR (TIME_RANGE.FLOATING = TRUE AND TIME_RANGE.START_DATE < %s))"
- self.TIMESPANTEST_TAIL_PIECE = " AND TIME_RANGE.CALENDAR_OBJECT_RESOURCE_ID = CALENDAR_OBJECT.RESOURCE_ID AND CALENDAR_OBJECT.CALENDAR_RESOURCE_ID = %s"
- self.TIMESPANTEST_JOIN_ON_PIECE = "TIME_RANGE.INSTANCE_ID = TRANSPARENCY.TIME_RANGE_INSTANCE_ID AND TRANSPARENCY.USER_ID = %s"
-
- super(postgresqlgenerator, self).__init__(expr, calendarid, userid)
-
-
- def generate(self):
- """
- Generate the actual SQL 'where ...' expression from the passed in
- expression tree.
-
- @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the
- partial SQL statement, and the C{list} is the list of argument
- substitutions to use with the SQL API execute method.
- """
-
- # Init state
- self.sout = StringIO.StringIO()
- self.arguments = []
- self.substitutions = []
- self.usedtimespan = False
-
- # Generate ' where ...' partial statement
- self.sout.write(self.WHERE)
- self.generateExpression(self.expression)
-
- # Prefix with ' from ...' partial statement
- select = self.FROM + self.RESOURCEDB
- if self.usedtimespan:
- self.frontArgument(self.userid)
- select += ", %s LEFT OUTER JOIN %s ON (%s)" % (
- self.TIMESPANDB,
- self.TRANSPARENCYDB,
- self.TIMESPANTEST_JOIN_ON_PIECE
- )
- select += self.sout.getvalue()
-
- select = select % tuple(self.substitutions)
-
- return select, self.arguments
-
-
- def addArgument(self, arg):
- self.arguments.append(arg)
- self.substitutions.append("%s")
- self.sout.write("%s")
-
- def setArgument(self, arg):
- self.arguments.append(arg)
- self.substitutions.append("%s")
-
- def frontArgument(self, arg):
- self.arguments.insert(0, arg)
- self.substitutions.insert(0, "%s")
-
- def containsArgument(self, arg):
- return "%%%s%%" % (arg,)
-
-
-class MemcachedUIDReserver(CachePoolUserMixIn, LoggingMixIn):
- def __init__(self, index, cachePool=None):
- self.index = index
- self._cachePool = cachePool
-
- def _key(self, uid):
- return 'reservation:%s' % (
- hashlib.md5('%s:%s' % (uid,
- self.index.resource._resourceID)).hexdigest())
-
- def reserveUID(self, uid):
- uid = uid.encode('utf-8')
- self.log_debug("Reserving UID %r @ %r" % (
- uid,
- self.index.resource))
-
- def _handleFalse(result):
- if result is False:
- raise ReservationError(
- "UID %s already reserved for calendar collection %s."
- % (uid, self.index.resource._name)
- )
-
- d = self.getCachePool().add(self._key(uid),
- 'reserved',
- expireTime=config.UIDReservationTimeOut)
- d.addCallback(_handleFalse)
- return d
-
-
- def unreserveUID(self, uid):
- uid = uid.encode('utf-8')
- self.log_debug("Unreserving UID %r @ %r" % (
- uid,
- self.index.resource))
-
- def _handleFalse(result):
- if result is False:
- raise ReservationError(
- "UID %s is not reserved for calendar collection %s."
- % (uid, self.index.resource._resourceID)
- )
-
- d = self.getCachePool().delete(self._key(uid))
- d.addCallback(_handleFalse)
- return d
-
-
- def isReservedUID(self, uid):
- uid = uid.encode('utf-8')
- self.log_debug("Is reserved UID %r @ %r" % (
- uid,
- self.index.resource))
-
- def _checkValue((flags, value)):
- if value is None:
- return False
- else:
- return True
-
- d = self.getCachePool().get(self._key(uid))
- d.addCallback(_checkValue)
- return d
-
-class DummyUIDReserver(LoggingMixIn):
-
- def __init__(self, index):
- self.index = index
- self.reservations = set()
-
- def _key(self, uid):
- return 'reservation:%s' % (
- hashlib.md5('%s:%s' % (uid,
- self.index.resource._resourceID)).hexdigest())
-
- def reserveUID(self, uid):
- uid = uid.encode('utf-8')
- self.log_debug("Reserving UID %r @ %r" % (
- uid,
- self.index.resource))
-
- key = self._key(uid)
- if key in self.reservations:
- raise ReservationError(
- "UID %s already reserved for calendar collection %s."
- % (uid, self.index.resource._name)
- )
- self.reservations.add(key)
- return succeed(None)
-
-
- def unreserveUID(self, uid):
- uid = uid.encode('utf-8')
- self.log_debug("Unreserving UID %r @ %r" % (
- uid,
- self.index.resource))
-
- key = self._key(uid)
- if key in self.reservations:
- self.reservations.remove(key)
- return succeed(None)
-
-
- def isReservedUID(self, uid):
- uid = uid.encode('utf-8')
- self.log_debug("Is reserved UID %r @ %r" % (
- uid,
- self.index.resource))
- key = self._key(uid)
- return succeed(key in self.reservations)
-
-class PostgresLegacyIndexEmulator(LoggingMixIn):
- """
- Emulator for L{twistedcaldv.index.Index} and
- L{twistedcaldv.index.IndexSchedule}.
- """
-
- def __init__(self, calendar):
- self.resource = self.calendar = calendar
- if (
- hasattr(config, "Memcached") and
- config.Memcached.Pools.Default.ClientEnabled
- ):
- self.reserver = MemcachedUIDReserver(self)
- else:
- # This is only used with unit tests
- self.reserver = DummyUIDReserver(self)
-
- @property
- def _txn(self):
- return self.calendar._txn
-
-
- def reserveUID(self, uid):
- if self.calendar._name == "inbox":
- return succeed(None)
- else:
- return self.reserver.reserveUID(uid)
-
-
- def unreserveUID(self, uid):
- if self.calendar._name == "inbox":
- return succeed(None)
- else:
- return self.reserver.unreserveUID(uid)
-
-
- def isReservedUID(self, uid):
- if self.calendar._name == "inbox":
- return succeed(False)
- else:
- return self.reserver.isReservedUID(uid)
-
-
- def isAllowedUID(self, uid, *names):
- """
- Checks to see whether to allow an operation which would add the
- specified UID to the index. Specifically, the operation may not
- violate the constraint that UIDs must be unique.
- @param uid: the UID to check
- @param names: the names of resources being replaced or deleted by the
- operation; UIDs associated with these resources are not checked.
- @return: True if the UID is not in the index and is not reserved,
- False otherwise.
- """
- if self.calendar._name == "inbox":
- return True
- else:
- rname = self.resourceNameForUID(uid)
- return (rname is None or rname in names)
-
- def resourceUIDForName(self, name):
- obj = self.calendar.calendarObjectWithName(name)
- if obj is None:
- return None
- return obj.uid()
-
-
- def resourceNameForUID(self, uid):
- obj = self.calendar.calendarObjectWithUID(uid)
- if obj is None:
- return None
- return obj.name()
-
-
- def notExpandedBeyond(self, minDate):
- """
- Gives all resources which have not been expanded beyond a given date
- in the database. (Unused; see above L{postgresqlgenerator}.
- """
- return [row[0] for row in self._txn.execSQL(
- "select RESOURCE_NAME from CALENDAR_OBJECT "
- "where RECURRANCE_MAX < %s and CALENDAR_RESOURCE_ID = %s",
- [normalizeForIndex(minDate), self.calendar._resourceID]
- )]
-
-
- def reExpandResource(self, name, expand_until):
- """
- Given a resource name, remove it from the database and re-add it
- with a longer expansion.
- """
- obj = self.calendar.calendarObjectWithName(name)
- obj.updateDatabase(obj.component(), expand_until=expand_until, reCreate=True)
-
- def testAndUpdateIndex(self, minDate):
- # Find out if the index is expanded far enough
- names = self.notExpandedBeyond(minDate)
-
- # Actually expand recurrence max
- for name in names:
- self.log_info("Search falls outside range of index for %s %s" % (name, minDate))
- self.reExpandResource(name, minDate)
-
- def whatchanged(self, revision):
-
- results = [
- (name.encode("utf-8"), deleted)
- for name, deleted in
- self._txn.execSQL(
- """select RESOURCE_NAME, DELETED from CALENDAR_OBJECT_REVISIONS
- where REVISION > %s and CALENDAR_RESOURCE_ID = %s""",
- [revision, self.calendar._resourceID],
- )
- ]
- results.sort(key=lambda x:x[1])
-
- changed = []
- deleted = []
- for name, wasdeleted in results:
- if name:
- if wasdeleted:
- if revision:
- deleted.append(name)
- else:
- changed.append(name)
- else:
- raise SyncTokenValidException
-
- return changed, deleted,
-
- def indexedSearch(self, filter, useruid='', fbtype=False):
- """
- Finds resources matching the given qualifiers.
- @param filter: the L{Filter} for the calendar-query to execute.
- @return: an iterable of tuples for each resource matching the
- given C{qualifiers}. The tuples are C{(name, uid, type)}, where
- C{name} is the resource name, C{uid} is the resource UID, and
- C{type} is the resource iCalendar component type.x
- """
-
- # Make sure we have a proper Filter element and get the partial SQL
- # statement to use.
- if isinstance(filter, calendarqueryfilter.Filter):
- qualifiers = calendarquery.sqlcalendarquery(filter, self.calendar._resourceID, useruid, generator=postgresqlgenerator)
- if qualifiers is not None:
- # Determine how far we need to extend the current expansion of
- # events. If we have an open-ended time-range we will expand one
- # year past the start. That should catch bounded recurrences - unbounded
- # will have been indexed with an "infinite" value always included.
- maxDate, isStartDate = filter.getmaxtimerange()
- if maxDate:
- maxDate = maxDate.date()
- if isStartDate:
- maxDate += datetime.timedelta(days=365)
- self.testAndUpdateIndex(maxDate)
- else:
- # We cannot handler this filter in an indexed search
- raise IndexedSearchException()
-
- else:
- qualifiers = None
-
- # Perform the search
- if qualifiers is None:
- rowiter = self._txn.execSQL(
- "select RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = %s",
- [self.calendar._resourceID, ],
- )
- else:
- if fbtype:
- # For a free-busy time-range query we return all instances
- rowiter = self._txn.execSQL(
- """select DISTINCT
- CALENDAR_OBJECT.RESOURCE_NAME, CALENDAR_OBJECT.ICALENDAR_UID, CALENDAR_OBJECT.ICALENDAR_TYPE, CALENDAR_OBJECT.ORGANIZER,
- TIME_RANGE.FLOATING, TIME_RANGE.START_DATE, TIME_RANGE.END_DATE, TIME_RANGE.FBTYPE, TIME_RANGE.TRANSPARENT, TRANSPARENCY.TRANSPARENT""" +
- qualifiers[0],
- qualifiers[1]
- )
- else:
- rowiter = self._txn.execSQL(
- "select DISTINCT CALENDAR_OBJECT.RESOURCE_NAME, CALENDAR_OBJECT.ICALENDAR_UID, CALENDAR_OBJECT.ICALENDAR_TYPE" +
- qualifiers[0],
- qualifiers[1]
- )
-
- # Check result for missing resources
-
- for row in rowiter:
- if fbtype:
- row = list(row)
- row[4] = 'Y' if row[4] else 'N'
- row[7] = indexfbtype_to_icalfbtype[row[7]]
- row[8] = 'T' if row[9] else 'F'
- del row[9]
- yield row
-
-
- def bruteForceSearch(self):
- return self._txn.execSQL(
- "select RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from "
- "CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = %s",
- [self.calendar._resourceID]
- )
-
-
- def resourcesExist(self, names):
- return list(set(names).intersection(
- set(self.calendar.listCalendarObjects())))
-
-
- def resourceExists(self, name):
- return bool(
- self._txn.execSQL(
- "select RESOURCE_NAME from CALENDAR_OBJECT where "
- "RESOURCE_NAME = %s and CALENDAR_RESOURCE_ID = %s",
- [name, self.calendar._resourceID]
- )
- )
-
-
-
-class PostgresCalendar(CalendarSyncTokenHelper):
-
- implements(ICalendar)
-
- def __init__(self, home, name, resourceID, notifier):
- self._home = home
- self._name = name
- self._resourceID = resourceID
- self._objects = {}
- self._notifier = notifier
-
-
- @property
- def _txn(self):
- return self._home._txn
-
-
- def setSharingUID(self, uid):
- self.properties()._setPerUserUID(uid)
-
-
- def retrieveOldInvites(self):
- return PostgresLegacyInvitesEmulator(self)
-
- def retrieveOldIndex(self):
- return PostgresLegacyIndexEmulator(self)
-
-
- def notifierID(self, label="default"):
- if self._notifier:
- return self._notifier.getID(label)
- else:
- return None
-
-
- def name(self):
- return self._name
-
-
- def rename(self, name):
- oldName = self._name
- self._txn.execSQL(
- "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
- self._updateSyncToken()
-
-
- def ownerCalendarHome(self):
- return self._home
-
-
- def listCalendarObjects(self):
- # FIXME: see listChildren
- rows = self._txn.execSQL(
- "select RESOURCE_NAME from "
- "CALENDAR_OBJECT where "
- "CALENDAR_RESOURCE_ID = %s",
- [self._resourceID])
- return [row[0] for row in rows]
-
-
- def calendarObjects(self):
- for name in self.listCalendarObjects():
- yield self.calendarObjectWithName(name)
-
-
- @memoized('name', '_objects')
- def calendarObjectWithName(self, name):
- rows = self._txn.execSQL(
- "select RESOURCE_ID from CALENDAR_OBJECT where "
- "RESOURCE_NAME = %s and CALENDAR_RESOURCE_ID = %s",
- [name, self._resourceID]
- )
- if not rows:
- return None
- resid = rows[0][0]
- return PostgresCalendarObject(self, name, resid)
-
-
- @memoized('uid', '_objects')
- def calendarObjectWithUID(self, uid):
- rows = self._txn.execSQL(
- "select RESOURCE_ID, RESOURCE_NAME from CALENDAR_OBJECT where "
- "ICALENDAR_UID = %s and CALENDAR_RESOURCE_ID = %s",
- [uid, self._resourceID]
- )
- if not rows:
- return None
- resid = rows[0][0]
- name = rows[0][1]
- return PostgresCalendarObject(self, name, resid)
-
-
- def createCalendarObjectWithName(self, name, component):
- rows = self._txn.execSQL(
- "select RESOURCE_NAME from CALENDAR_OBJECT where "
- " RESOURCE_NAME = %s AND CALENDAR_RESOURCE_ID = %s",
- [name, self._resourceID]
- )
- if rows:
- raise ObjectResourceNameAlreadyExistsError()
-
- calendarObject = PostgresCalendarObject(self, name, None)
- calendarObject.component = lambda : component
-
- validateCalendarComponent(calendarObject, self, component)
-
- calendarObject.updateDatabase(component, inserting=True)
- self._insertRevision(name)
-
- if self._notifier:
- self._home._txn.postCommit(self._notifier.notify)
-
-
- def removeCalendarObjectWithName(self, name):
- self._txn.execSQL(
- "delete from CALENDAR_OBJECT where RESOURCE_NAME = %s and "
- "CALENDAR_RESOURCE_ID = %s",
- [name, self._resourceID]
- )
- if self._txn._cursor.rowcount == 0:
- raise NoSuchObjectResourceError()
- self._objects.pop(name, None)
- self._deleteRevision(name)
-
- if self._notifier:
- self._txn.postCommit(self._notifier.notify)
-
-
- def removeCalendarObjectWithUID(self, uid):
- rows = self._txn.execSQL(
- "select RESOURCE_NAME from CALENDAR_OBJECT where "
- "ICALENDAR_UID = %s AND CALENDAR_RESOURCE_ID = %s",
- [uid, self._resourceID]
- )
- if not rows:
- raise NoSuchObjectResourceError()
- name = rows[0][0]
- self._txn.execSQL(
- "delete from CALENDAR_OBJECT where ICALENDAR_UID = %s and "
- "CALENDAR_RESOURCE_ID = %s",
- [uid, self._resourceID]
- )
- self._objects.pop(name, None)
- self._objects.pop(uid, None)
- self._deleteRevision(name)
-
- if self._notifier:
- self._home._txn.postCommit(self._notifier.notify)
-
-
- def calendarObjectsInTimeRange(self, start, end, timeZone):
- raise NotImplementedError()
-
-
- def calendarObjectsSinceToken(self, token):
- raise NotImplementedError()
-
-
- @cached
- def properties(self):
- return PropertyStore(
- self.ownerCalendarHome().uid(),
- self._txn,
- self._resourceID
- )
-
-
- # IDataStoreResource
- def contentType(self):
- """
- The content type of Calendar objects is text/calendar.
- """
- return MimeType.fromString("text/calendar; charset=utf-8")
-
-
- def md5(self):
- return None
-
-
- def size(self):
- return 0
-
-
- def created(self):
- created = self._txn.execSQL(
- "select extract(EPOCH from CREATED) from CALENDAR where "
- "RESOURCE_ID = %s", [self._resourceID]
- )[0][0]
- return int(created)
-
- def modified(self):
- modified = self._txn.execSQL(
- "select extract(EPOCH from MODIFIED) from CALENDAR where "
- "RESOURCE_ID = %s", [self._resourceID]
- )[0][0]
- return int(modified)
-
-
-class PostgresCalendarHome(object):
-
- implements(ICalendarHome)
-
- def __init__(self, transaction, ownerUID, resourceID, notifier):
- self._txn = transaction
- self._ownerUID = ownerUID
- self._resourceID = resourceID
- self._calendars = {}
- self._notifier = notifier
-
-
- def retrieveOldShares(self):
- return PostgresLegacySharesEmulator(self)
-
-
- def uid(self):
- """
- Retrieve the unique identifier for this calendar home.
-
- @return: a string.
- """
- return self._ownerUID
-
-
- def name(self):
- """
- Implement L{IDataStoreResource.name} to return the uid.
- """
- return self.uid()
-
-
- def transaction(self):
- return self._txn
-
-
- def listChildren(self):
- """
- Retrieve the names of the children in this calendar home.
-
- @return: an iterable of C{str}s.
- """
- # FIXME: not specified on the interface or exercised by the tests, but
- # required by clients of the implementation!
- rows = self._txn.execSQL(
- "select CALENDAR_RESOURCE_NAME from CALENDAR_BIND where "
- "CALENDAR_HOME_RESOURCE_ID = %s "
- "and BIND_MODE = %s ",
- # Right now, we only show owned calendars.
- [self._resourceID, _BIND_MODE_OWN]
- )
- names = [row[0] for row in rows]
- return names
-
-
- def createdHome(self):
- self.createCalendarWithName("calendar")
- defaultCal = self.calendarWithName("calendar")
- props = defaultCal.properties()
- props[PropertyName(*ScheduleCalendarTransp.qname())] = ScheduleCalendarTransp(
- Opaque())
- self.createCalendarWithName("inbox")
-
- def calendars(self):
- """
- Retrieve calendars contained in this calendar home.
-
- @return: an iterable of L{ICalendar}s.
- """
- names = self.listChildren()
- 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
- calendar home.
-
- @param name: a string.
- @return: an L{ICalendar} or C{None} if no such calendar
- exists.
- """
- data = self._txn.execSQL(
- "select CALENDAR_RESOURCE_ID from CALENDAR_BIND where "
- "CALENDAR_RESOURCE_NAME = %s and CALENDAR_HOME_RESOURCE_ID = %s "
- "and BIND_MODE = %s",
- [name, self._resourceID, _BIND_MODE_OWN]
- )
- if not data:
- return None
- resourceID = data[0][0]
- if self._notifier:
- childID = "%s/%s" % (self.uid(), name)
- notifier = self._notifier.clone(label="collection", id=childID)
- else:
- notifier = None
- return PostgresCalendar(self, name, resourceID, notifier)
-
-
- def calendarObjectWithDropboxID(self, dropboxID):
- """
- Implement lookup with brute-force scanning.
- """
- for calendar in self.calendars():
- for calendarObject in calendar.calendarObjects():
- if dropboxID == calendarObject.dropboxID():
- return calendarObject
-
-
- def createCalendarWithName(self, name):
- rows = self._txn.execSQL(
- "select CALENDAR_RESOURCE_NAME from CALENDAR_BIND where "
- "CALENDAR_RESOURCE_NAME = %s AND "
- "CALENDAR_HOME_RESOURCE_ID = %s",
- [name, self._resourceID]
- )
- if rows:
- raise HomeChildNameAlreadyExistsError()
- rows = self._txn.execSQL("select nextval('RESOURCE_ID_SEQ')")
- resourceID = rows[0][0]
- self._txn.execSQL(
- "insert into CALENDAR (RESOURCE_ID) values "
- "(%s)",
- [resourceID])
-
- self._txn.execSQL("""
- insert into CALENDAR_BIND (
- CALENDAR_HOME_RESOURCE_ID,
- CALENDAR_RESOURCE_ID, CALENDAR_RESOURCE_NAME, BIND_MODE,
- SEEN_BY_OWNER, SEEN_BY_SHAREE, BIND_STATUS) values (
- %s, %s, %s, %s, %s, %s, %s)
- """,
- [self._resourceID, resourceID, name, _BIND_MODE_OWN, True, True,
- _BIND_STATUS_ACCEPTED]
- )
-
- calendarType = ResourceType.calendar #@UndefinedVariable
- newCalendar = self.calendarWithName(name)
- newCalendar.properties()[
- PropertyName.fromElement(ResourceType)] = calendarType
- newCalendar._updateSyncToken()
-
- if self._notifier:
- self._txn.postCommit(self._notifier.notify)
-
-
- def removeCalendarWithName(self, name):
- self._txn.execSQL(
- "delete from CALENDAR_BIND where CALENDAR_RESOURCE_NAME = %s and "
- "CALENDAR_HOME_RESOURCE_ID = %s",
- [name, self._resourceID]
- )
- self._calendars.pop(name, None)
- if self._txn._cursor.rowcount == 0:
- raise NoSuchHomeChildError()
- # FIXME: the schema should probably cascade the calendar delete when
- # the last bind is deleted.
- if self._notifier:
- self._txn.postCommit(self._notifier.notify)
-
-
- @cached
- def properties(self):
- return PropertyStore(
- self.uid(),
- self._txn,
- self._resourceID
- )
-
-
- # IDataStoreResource
- def contentType(self):
- """
- The content type of Calendar objects is text/calendar.
- """
- return None
-
-
- def md5(self):
- return None
-
-
- def size(self):
- return 0
-
-
- def created(self):
- return None
-
-
- def modified(self):
- return None
-
-
- def notifierID(self, label="default"):
- if self._notifier:
- return self._notifier.getID(label)
- else:
- return None
-
-
-
-class PostgresNotificationObject(object):
- implements(INotificationObject)
-
- def __init__(self, home, resourceID):
- self._home = home
- self._resourceID = resourceID
-
-
- def notificationCollection(self):
- return self._home
-
-
- def name(self):
- return self.uid() + ".xml"
-
-
- def contentType(self):
- """
- The content type of NotificationObjects is text/xml.
- """
- return MimeType.fromString("text/xml")
-
-
- @property
- def _txn(self):
- return self._home._txn
-
-
- def setData(self, uid, xmltype, xmldata):
- self.properties()[PropertyName.fromElement(NotificationType)] = NotificationType(xmltype)
- return self._txn.execSQL(
- """
- update NOTIFICATION set NOTIFICATION_UID = %s, XML_TYPE = %s,
- XML_DATA = %s where RESOURCE_ID = %s
- """,
- [uid, xmltype, xmldata, self._resourceID]
- )
-
-
- def _fieldQuery(self, field):
- [[data]] = self._txn.execSQL(
- "select " + field + " from NOTIFICATION where "
- "RESOURCE_ID = %s",
- [self._resourceID])
- return data
-
-
- def xmldata(self):
- return self._fieldQuery("XML_DATA")
-
-
- def uid(self):
- return self._fieldQuery("NOTIFICATION_UID")
-
-
- @cached
- def properties(self):
- return PropertyStore(
- self._home.uid(),
- self._txn,
- self._resourceID
- )
-
-
- def md5(self):
- return hashlib.md5(self.xmldata()).hexdigest()
-
-
- def modified(self):
- modified = self._txn.execSQL(
- "select extract(EPOCH from MODIFIED) from NOTIFICATION where "
- "RESOURCE_ID = %s", [self._resourceID]
- )[0][0]
- return int(modified)
-
-
- def created(self):
- return None
-
-
- def size(self):
- return len(self.xmldata())
-
-
-
-class PostgresLegacyNotificationsEmulator(object):
- def __init__(self, notificationsCollection):
- self._collection = notificationsCollection
-
-
- def _recordForObject(self, notificationObject):
- return NotificationRecord(
- notificationObject.uid(),
- notificationObject.name(),
- notificationObject._fieldQuery("XML_TYPE"))
-
-
- def recordForName(self, name):
- return self._recordForObject(
- self._collection.notificationObjectWithName(name)
- )
-
-
- def recordForUID(self, uid):
- return self._recordForObject(
- self._collection.notificationObjectWithUID(uid)
- )
-
-
- def removeRecordForUID(self, uid):
- self._collection.removeNotificationObjectWithUID(uid)
-
-
- def removeRecordForName(self, name):
- self._collection.removeNotificationObjectWithName(name)
-
-
-
-class PostgresNotificationCollection(object):
-
- implements(INotificationCollection)
-
- def __init__(self, txn, uid, resourceID):
- self._txn = txn
- self._uid = uid
- self._resourceID = resourceID
- self._notifications = {}
-
-
- def retrieveOldIndex(self):
- return PostgresLegacyNotificationsEmulator(self)
-
-
- def name(self):
- return 'notification'
-
-
- def uid(self):
- return self._uid
-
-
- def notificationObjects(self):
- for [uid] in self._txn.execSQL(
- "select (NOTIFICATION_UID) "
- "from NOTIFICATION "
- "where NOTIFICATION_HOME_RESOURCE_ID = %s",
- [self._resourceID]):
- yield self.notificationObjectWithUID(uid)
-
-
- def _nameToUID(self, name):
- """
- Based on the file-backed implementation, the 'name' is just uid +
- ".xml".
- """
- return name.rsplit(".", 1)[0]
-
-
- def notificationObjectWithName(self, name):
- return self.notificationObjectWithUID(self._nameToUID(name))
-
-
- @memoized('uid', '_notifications')
- def notificationObjectWithUID(self, uid):
- rows = self._txn.execSQL(
- "select RESOURCE_ID from NOTIFICATION where NOTIFICATION_UID = %s"
- " and NOTIFICATION_HOME_RESOURCE_ID = %s",
- [uid, self._resourceID])
- if rows:
- [[resourceID]] = rows
- return PostgresNotificationObject(self, resourceID)
- else:
- return None
-
-
- def writeNotificationObject(self, uid, xmltype, xmldata):
- xmltypeString = xmltype.toxml()
- existing = self._txn.execSQL("select NOTIFICATION_UID from NOTIFICATION where NOTIFICATION_HOME_RESOURCE_ID = %s and NOTIFICATION_UID = %s",
- [self._resourceID, uid])
- if existing:
- self._txn.execSQL(
- "update NOTIFICATION set XML_TYPE = %s, XML_DATA = %s where NOTIFICATION_HOME_RESOURCE_ID = %s and NOTIFICATION_UID = %s",
- [xmltypeString, xmldata, self._resourceID, uid])
- else:
- self._txn.execSQL(
- "insert into NOTIFICATION (NOTIFICATION_HOME_RESOURCE_ID, NOTIFICATION_UID, XML_TYPE, XML_DATA) "
- "values (%s, %s, %s, %s)", [self._resourceID, uid, xmltypeString, xmldata])
- notificationObject = self.notificationObjectWithUID(uid)
- notificationObject.properties()[PropertyName.fromElement(NotificationType)] = NotificationType(xmltype)
-
-
- def removeNotificationObjectWithName(self, name):
- self.removeNotificationObjectWithUID(self._nameToUID(name))
-
-
- def removeNotificationObjectWithUID(self, uid):
- self._txn.execSQL(
- "delete from NOTIFICATION where NOTIFICATION_UID = %s and "
- "NOTIFICATION_HOME_RESOURCE_ID = %s",
- [uid, self._resourceID])
-
-
- def syncToken(self):
- return 'dummy-sync-token'
-
-
- def notificationObjectsSinceToken(self, token):
- changed = []
- removed = []
- token = self.syncToken()
- return (changed, removed, token)
-
-
- @cached
- def properties(self):
- return PropertyStore(
- self._uid,
- self._txn,
- self._resourceID
- )
-
-
-
-# CARDDAV
-
-class PostgresAddressBookObject(object):
-
- implements(IAddressBookObject)
-
- def __init__(self, addressbook, name, resid):
- self._addressbook = addressbook
- self._name = name
- self._resourceID = resid
- self._vCardText = None
-
-
- @property
- def _txn(self):
- return self._addressbook._txn
-
-
- def uid(self):
- return self.component().resourceUID()
-
-
- def name(self):
- return self._name
-
-
- def addressbook(self):
- return self._addressbook
-
-
- def vCardText(self):
- if self._vCardText is None:
- text = self._txn.execSQL(
- "select VCARD_TEXT from ADDRESSBOOK_OBJECT where "
- "RESOURCE_ID = %s", [self._resourceID]
- )[0][0]
- self._vCardText = text
- return text
- else:
- return self._vCardText
-
-
- def component(self):
- return VCard.fromString(self.vCardText())
-
-
- def componentType(self):
- return self.component().mainType()
-
-
- @cached
- def properties(self):
- return PropertyStore(
- self.uid(),
- self._txn,
- self._resourceID
- )
-
-
- def setComponent(self, component):
- validateAddressBookComponent(self, self._addressbook, component)
-
- self.updateDatabase(component)
- self._addressbook._updateRevision(self._name)
-
- if self._addressbook._notifier:
- self._addressbook._home._txn.postCommit(self._addressbook._notifier.notify)
-
- def updateDatabase(self, component, expand_until=None, reCreate=False, inserting=False):
- """
- Update the database tables for the new data being written.
-
- @param component: calendar data to store
- @type component: L{Component}
- """
-
- componentText = str(component)
- self._vCardText = componentText
-
- # CALENDAR_OBJECT table update
- if inserting:
- self._resourceID = self._txn.execSQL(
- """
- insert into ADDRESSBOOK_OBJECT
- (ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME, VCARD_TEXT, VCARD_UID)
- values
- (%s, %s, %s, %s)
- returning RESOURCE_ID
- """,
- [
- self._addressbook._resourceID,
- self._name,
- componentText,
- component.resourceUID(),
- ]
- )[0][0]
- else:
- self._txn.execSQL(
- """
- update ADDRESSBOOK_OBJECT set
- (VCARD_TEXT, VCARD_UID, MODIFIED)
- =
- (%s, %s, timezone('UTC', CURRENT_TIMESTAMP))
- where RESOURCE_ID = %s
- """,
- [
- componentText,
- component.resourceUID(),
- self._resourceID
- ]
- )
-
- # IDataStoreResource
- def contentType(self):
- """
- The content type of Addressbook objects is text/x-vcard.
- """
- return MimeType.fromString("text/vcard; charset=utf-8")
-
-
- def md5(self):
- return None
-
-
- def size(self):
- size = self._txn.execSQL(
- "select character_length(VCARD_TEXT) from ADDRESSBOOK_OBJECT where "
- "RESOURCE_ID = %s", [self._resourceID]
- )[0][0]
- return size
-
-
- def created(self):
- created = self._txn.execSQL(
- "select extract(EPOCH from CREATED) from ADDRESSBOOK_OBJECT where "
- "RESOURCE_ID = %s", [self._resourceID]
- )[0][0]
- return int(created)
-
-
- def modified(self):
- modified = self._txn.execSQL(
- "select extract(EPOCH from MODIFIED) from ADDRESSBOOK_OBJECT where "
- "RESOURCE_ID = %s", [self._resourceID]
- )[0][0]
- return int(modified)
-
-
-
-class postgresqladbkgenerator(sqlgenerator):
- """
- Query generator for postgreSQL indexed searches. (Currently unused: work
- in progress.)
- """
-
- ISOP = " = "
- CONTAINSOP = " LIKE "
- NOTCONTAINSOP = " NOT LIKE "
- FIELDS = {
- "UID": "ADDRESSBOOK_OBJECT.VCARD_UID",
- }
-
- def __init__(self, expr, addressbookid):
- self.RESOURCEDB = "ADDRESSBOOK_OBJECT"
-
- super(postgresqladbkgenerator, self).__init__(expr, addressbookid)
-
-
- def generate(self):
- """
- Generate the actual SQL 'where ...' expression from the passed in
- expression tree.
-
- @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the
- partial SQL statement, and the C{list} is the list of argument
- substitutions to use with the SQL API execute method.
- """
-
- # Init state
- self.sout = StringIO.StringIO()
- self.arguments = []
- self.substitutions = []
-
- # Generate ' where ...' partial statement
- self.sout.write(self.WHERE)
- self.generateExpression(self.expression)
-
- # Prefix with ' from ...' partial statement
- select = self.FROM + self.RESOURCEDB
- select += self.sout.getvalue()
-
- select = select % tuple(self.substitutions)
-
- return select, self.arguments
-
-
- def addArgument(self, arg):
- self.arguments.append(arg)
- self.substitutions.append("%s")
- self.sout.write("%s")
-
- def setArgument(self, arg):
- self.arguments.append(arg)
- self.substitutions.append("%s")
-
- def frontArgument(self, arg):
- self.arguments.insert(0, arg)
- self.substitutions.insert(0, "%s")
-
- def containsArgument(self, arg):
- return "%%%s%%" % (arg,)
-
-
-class PostgresLegacyABIndexEmulator(object):
- """
- Emulator for L{twistedcaldv.index.Index} and
- L{twistedcaldv.index.IndexSchedule}.
- """
-
- def __init__(self, addressbook):
- self.resource = self.addressbook = addressbook
- if (
- hasattr(config, "Memcached") and
- config.Memcached.Pools.Default.ClientEnabled
- ):
- self.reserver = MemcachedUIDReserver(self)
- else:
- # This is only used with unit tests
- self.reserver = DummyUIDReserver(self)
-
-
- @property
- def _txn(self):
- return self.addressbook._txn
-
-
- def reserveUID(self, uid):
- return self.reserver.reserveUID(uid)
-
-
- def unreserveUID(self, uid):
- return self.reserver.unreserveUID(uid)
-
-
- def isReservedUID(self, uid):
- return self.reserver.isReservedUID(uid)
-
-
- def isAllowedUID(self, uid, *names):
- """
- Checks to see whether to allow an operation which would add the
- specified UID to the index. Specifically, the operation may not
- violate the constraint that UIDs must be unique.
- @param uid: the UID to check
- @param names: the names of resources being replaced or deleted by the
- operation; UIDs associated with these resources are not checked.
- @return: True if the UID is not in the index and is not reserved,
- False otherwise.
- """
- rname = self.resourceNameForUID(uid)
- return (rname is None or rname in names)
-
-
- def resourceUIDForName(self, name):
- obj = self.addressbook.addressbookObjectWithName(name)
- if obj is None:
- return None
- return obj.uid()
-
-
- def resourceNameForUID(self, uid):
- obj = self.addressbook.addressbookObjectWithUID(uid)
- if obj is None:
- return None
- return obj.name()
-
-
- def whatchanged(self, revision):
-
- results = [
- (name.encode("utf-8"), deleted)
- for name, deleted in
- self._txn.execSQL(
- """select RESOURCE_NAME, DELETED from ADDRESSBOOK_OBJECT_REVISIONS
- where REVISION > %s and ADDRESSBOOK_RESOURCE_ID = %s""",
- [revision, self.addressbook._resourceID],
- )
- ]
- results.sort(key=lambda x:x[1])
-
- changed = []
- deleted = []
- for name, wasdeleted in results:
- if name:
- if wasdeleted:
- if revision:
- deleted.append(name)
- else:
- changed.append(name)
- else:
- raise SyncTokenValidException
-
- return changed, deleted,
-
- def searchValid(self, filter):
- if isinstance(filter, carddavxml.Filter):
- qualifiers = addressbookquery.sqladdressbookquery(filter)
- else:
- qualifiers = None
-
- return qualifiers is not None
-
- def search(self, filter):
- """
- Finds resources matching the given qualifiers.
- @param filter: the L{Filter} for the addressbook-query to execute.
- @return: an iterable of tuples for each resource matching the
- given C{qualifiers}. The tuples are C{(name, uid, type)}, where
- C{name} is the resource name, C{uid} is the resource UID, and
- C{type} is the resource iCalendar component type.x
- """
-
- # Make sure we have a proper Filter element and get the partial SQL statement to use.
- if isinstance(filter, carddavxml.Filter):
- qualifiers = addressbookquery.sqladdressbookquery(filter, self.addressbook._resourceID, generator=postgresqladbkgenerator)
- else:
- qualifiers = None
- if qualifiers is not None:
- rowiter = self._txn.execSQL(
- "select DISTINCT ADDRESSBOOK_OBJECT.RESOURCE_NAME, ADDRESSBOOK_OBJECT.VCARD_UID" +
- qualifiers[0],
- qualifiers[1]
- )
- else:
- rowiter = self._txn.execSQL(
- "select RESOURCE_NAME, VCARD_UID from ADDRESSBOOK_OBJECT where ADDRESSBOOK_RESOURCE_ID = %s",
- [self.addressbook._resourceID, ],
- )
-
- for row in rowiter:
- yield row
-
- def indexedSearch(self, filter, useruid='', fbtype=False):
- """
- Always raise L{IndexedSearchException}, since these indexes are not
- fully implemented yet.
- """
- raise IndexedSearchException()
-
-
- def bruteForceSearch(self):
- return self._txn.execSQL(
- "select RESOURCE_NAME, VCARD_UID from "
- "ADDRESSBOOK_OBJECT where ADDRESSBOOK_RESOURCE_ID = %s",
- [self.addressbook._resourceID]
- )
-
-
- def resourcesExist(self, names):
- return list(set(names).intersection(
- set(self.addressbook.listAddressbookObjects())))
-
-
- def resourceExists(self, name):
- return bool(
- self._txn.execSQL(
- "select RESOURCE_NAME from ADDRESSBOOK_OBJECT where "
- "RESOURCE_NAME = %s and ADDRESSBOOK_RESOURCE_ID = %s",
- [name, self.addressbook._resourceID]
- )
- )
-
-
-class PostgresLegacyABInvitesEmulator(object):
- """
- Emulator for the implicit interface specified by
- L{twistedcaldav.sharing.InvitesDatabase}.
- """
-
-
- def __init__(self, addressbook):
- self._addressbook = addressbook
-
-
- @property
- def _txn(self):
- return self._addressbook._txn
-
-
- def create(self):
- "No-op, because the index implicitly always exists in the database."
-
-
- def remove(self):
- "No-op, because the index implicitly always exists in the database."
-
-
- def allRecords(self):
- for row in self._txn.execSQL(
- """
- select
- INVITE.INVITE_UID, INVITE.NAME, INVITE.RECIPIENT_ADDRESS,
- ADDRESSBOOK_HOME.OWNER_UID, ADDRESSBOOK_BIND.BIND_MODE,
- ADDRESSBOOK_BIND.BIND_STATUS, ADDRESSBOOK_BIND.MESSAGE
- from
- INVITE, ADDRESSBOOK_HOME, ADDRESSBOOK_BIND
- where
- INVITE.RESOURCE_ID = %s and
- INVITE.HOME_RESOURCE_ID =
- ADDRESSBOOK_HOME.RESOURCE_ID and
- ADDRESSBOOK_BIND.ADDRESSBOOK_RESOURCE_ID =
- INVITE.RESOURCE_ID and
- ADDRESSBOOK_BIND.ADDRESSBOOK_HOME_RESOURCE_ID =
- INVITE.HOME_RESOURCE_ID
- order by
- INVITE.NAME asc
- """, [self._addressbook._resourceID]):
- [inviteuid, common_name, userid, ownerUID,
- bindMode, bindStatus, summary] = row
- # FIXME: this is really the responsibility of the protocol layer.
- state = {
- _BIND_STATUS_INVITED: "NEEDS-ACTION",
- _BIND_STATUS_ACCEPTED: "ACCEPTED",
- _BIND_STATUS_DECLINED: "DECLINED",
- _BIND_STATUS_INVALID: "INVALID",
- }[bindStatus]
- access = {
- _BIND_MODE_READ: "read-only",
- _BIND_MODE_WRITE: "read-write"
- }[bindMode]
- principalURL = "/principals/__uids__/%s/" % (ownerUID,)
- yield Invite(
- inviteuid, userid, principalURL, common_name,
- access, state, summary
- )
-
-
- def recordForUserID(self, userid):
- for record in self.allRecords():
- if record.userid == userid:
- return record
-
-
- def recordForPrincipalURL(self, principalURL):
- for record in self.allRecords():
- if record.principalURL == principalURL:
- return record
-
-
- def recordForInviteUID(self, inviteUID):
- for record in self.allRecords():
- if record.inviteuid == inviteUID:
- return record
-
-
- def addOrUpdateRecord(self, record):
- bindMode = {'read-only': _BIND_MODE_READ,
- 'read-write': _BIND_MODE_WRITE}[record.access]
- bindStatus = {
- "NEEDS-ACTION": _BIND_STATUS_INVITED,
- "ACCEPTED": _BIND_STATUS_ACCEPTED,
- "DECLINED": _BIND_STATUS_DECLINED,
- "INVALID": _BIND_STATUS_INVALID,
- }[record.state]
- # principalURL is derived from a directory record's principalURL() so
- # it will always contain the UID. The form is '/principals/__uids__/x'
- # (and may contain a trailing slash).
- principalUID = record.principalURL.split("/")[3]
- shareeHome = self._txn.addressbookHomeWithUID(principalUID, create=True)
- rows = self._txn.execSQL(
- "select RESOURCE_ID, HOME_RESOURCE_ID from INVITE where RECIPIENT_ADDRESS = %s",
- [record.userid]
- )
- if rows:
- [[resourceID, homeResourceID]] = rows
- # Invite(inviteuid, userid, principalURL, common_name, access, state, summary)
- self._txn.execSQL("""
- update ADDRESSBOOK_BIND set BIND_MODE = %s,
- BIND_STATUS = %s, MESSAGE = %s
- where
- ADDRESSBOOK_RESOURCE_ID = %s and
- ADDRESSBOOK_HOME_RESOURCE_ID = %s
- """, [bindMode, bindStatus, record.summary,
- resourceID, homeResourceID])
- self._txn.execSQL("""
- update INVITE set NAME = %s, INVITE_UID = %s
- where RECIPIENT_ADDRESS = %s
- """,
- [record.name, record.inviteuid, record.userid]
- )
- else:
- self._txn.execSQL(
- """
- insert into INVITE (
- INVITE_UID, NAME,
- HOME_RESOURCE_ID, RESOURCE_ID,
- RECIPIENT_ADDRESS
- )
- values (%s, %s, %s, %s, %s)
- """,
- [
- record.inviteuid, record.name,
- shareeHome._resourceID, self._addressbook._resourceID,
- record.userid
- ])
- self._txn.execSQL(
- """
- insert into ADDRESSBOOK_BIND
- (
- ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_ID,
- ADDRESSBOOK_RESOURCE_NAME, BIND_MODE, BIND_STATUS,
- SEEN_BY_OWNER, SEEN_BY_SHAREE, MESSAGE
- )
- values (%s, %s, %s, %s, %s, %s, %s, %s)
- """,
- [
- shareeHome._resourceID,
- self._addressbook._resourceID,
- None, # this is NULL because it is not bound yet, let's be
- # explicit about that.
- bindMode,
- bindStatus,
- False,
- False,
- record.summary
- ])
-
-
- def removeRecordForUserID(self, userid):
- rec = self.recordForUserID(userid)
- self.removeRecordForInviteUID(rec.inviteuid)
-
-
- def removeRecordForPrincipalURL(self, principalURL):
- raise NotImplementedError("removeRecordForPrincipalURL")
-
-
- def removeRecordForInviteUID(self, inviteUID):
- rows = self._txn.execSQL("""
- select HOME_RESOURCE_ID, RESOURCE_ID from INVITE where
- INVITE_UID = %s
- """, [inviteUID])
- if rows:
- [[homeID, resourceID]] = rows
- self._txn.execSQL(
- "delete from ADDRESSBOOK_BIND where "
- "ADDRESSBOOK_HOME_RESOURCE_ID = %s and ADDRESSBOOK_RESOURCE_ID = %s",
- [homeID, resourceID])
- self._txn.execSQL("delete from INVITE where INVITE_UID = %s",
- [inviteUID])
-
-
-
-class PostgresLegacyABSharesEmulator(object):
-
- def __init__(self, home):
- self._home = home
-
-
- @property
- def _txn(self):
- return self._home._txn
-
-
- def create(self):
- pass
-
-
- def remove(self):
- pass
-
-
- def allRecords(self):
- # This should have been a smart join that got all these columns at
- # once, but let's not bother to fix it, since the actual query we
- # _want_ to do (just look for addressbook binds in a particular homes) is
- # much simpler anyway; we should just do that.
- shareRows = self._txn.execSQL(
- """
- select ADDRESSBOOK_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME, MESSAGE
- from ADDRESSBOOK_BIND
- where ADDRESSBOOK_HOME_RESOURCE_ID = %s and
- BIND_MODE != %s and
- ADDRESSBOOK_RESOURCE_NAME is not null
- """, [self._home._resourceID, _BIND_MODE_OWN])
- for resourceID, resourceName, summary in shareRows:
- [[shareuid]] = self._txn.execSQL(
- """
- select INVITE_UID
- from INVITE
- where RESOURCE_ID = %s and HOME_RESOURCE_ID = %s
- """, [resourceID, self._home._resourceID])
- sharetype = 'I'
- [[ownerHomeID, ownerResourceName]] = self._txn.execSQL(
- """
- select ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME
- from ADDRESSBOOK_BIND
- where ADDRESSBOOK_RESOURCE_ID = %s and
- BIND_MODE = %s
- """, [resourceID, _BIND_MODE_OWN]
- )
- [[ownerUID]] = self._txn.execSQL(
- "select OWNER_UID from ADDRESSBOOK_HOME where RESOURCE_ID = %s",
- [ownerHomeID])
- hosturl = '/addressbooks/__uids__/%s/%s' % (
- ownerUID, ownerResourceName
- )
- localname = resourceName
- record = SharedCollectionRecord(
- shareuid, sharetype, hosturl, localname, summary
- )
- yield record
-
-
- def _search(self, **kw):
- [[key, value]] = kw.items()
- for record in self.allRecords():
- if getattr(record, key) == value:
- return record
-
- def recordForLocalName(self, localname):
- return self._search(localname=localname)
-
- def recordForShareUID(self, shareUID):
- return self._search(shareuid=shareUID)
-
-
- def addOrUpdateRecord(self, record):
-# print '*** SHARING***: Adding or updating this record:'
-# import pprint
-# pprint.pprint(record.__dict__)
- # record.hosturl -> /addressbooks/__uids__/<uid>/<addressbookname>
- splithost = record.hosturl.split('/')
- ownerUID = splithost[3]
- ownerAddressBookName = splithost[4]
- ownerHome = self._txn.addressbookHomeWithUID(ownerUID)
- ownerAddressBook = ownerHome.addressbookWithName(ownerAddressBookName)
- addressbookResourceID = ownerAddressBook._resourceID
-
- # There needs to be a bind already, one that corresponds to the
- # invitation. The invitation's UID is the same as the share UID. I
- # just need to update its 'localname', i.e.
- # ADDRESSBOOK_BIND.ADDRESSBOOK_RESOURCE_NAME.
-
- self._txn.execSQL(
- """
- update ADDRESSBOOK_BIND set ADDRESSBOOK_RESOURCE_NAME = %s
- where ADDRESSBOOK_HOME_RESOURCE_ID = %s and ADDRESSBOOK_RESOURCE_ID = %s
- """,
- [record.localname, self._home._resourceID, addressbookResourceID]
- )
-
-
- def removeRecordForLocalName(self, localname):
- self._txn.execSQL(
- "delete from ADDRESSBOOK_BIND where ADDRESSBOOK_RESOURCE_NAME = %s "
- "and ADDRESSBOOK_HOME_RESOURCE_ID = %s",
- [localname, self._home._resourceID]
- )
-
-
- def removeRecordForShareUID(self, shareUID):
- pass
-# c = self._home._cursor()
-# c.execute(
-# "delete from ADDRESSBOOK_BIND where ADDRESSBOOK_RESOURCE_NAME = %s "
-# "and ADDRESSBOOK_HOME_RESOURCE_ID = %s",
-# [self._home._resourceID]
-# )
-
-
-
-
-class PostgresAddressBook(AddressbookSyncTokenHelper):
-
- implements(IAddressBook)
-
- def __init__(self, home, name, resourceID, notifier):
- self._home = home
- self._name = name
- self._resourceID = resourceID
- self._objects = {}
- self._notifier = notifier
-
-
- @property
- def _txn(self):
- return self._home._txn
-
-
- def setSharingUID(self, uid):
- self.properties()._setPerUserUID(uid)
-
-
- def retrieveOldInvites(self):
- return PostgresLegacyABInvitesEmulator(self)
-
- def retrieveOldIndex(self):
- return PostgresLegacyABIndexEmulator(self)
-
-
- def notifierID(self, label="default"):
- if self._notifier:
- return self._notifier.getID(label)
- else:
- return None
-
-
- def name(self):
- return self._name
-
-
- def rename(self, name):
- oldName = self._name
- self._txn.execSQL(
- "update ADDRESSBOOK_BIND set ADDRESSBOOK_RESOURCE_NAME = %s "
- "where ADDRESSBOOK_RESOURCE_ID = %s AND "
- "ADDRESSBOOK_HOME_RESOURCE_ID = %s",
- [name, self._resourceID, self._home._resourceID]
- )
- self._name = name
- # update memos
- del self._home._addressbooks[oldName]
- self._home._addressbooks[name] = self
- self._updateSyncToken()
-
-
- def ownerAddressBookHome(self):
- return self._home
-
-
- def listAddressbookObjects(self):
- # FIXME: see listChildren
- rows = self._txn.execSQL(
- "select RESOURCE_NAME from "
- "ADDRESSBOOK_OBJECT where "
- "ADDRESSBOOK_RESOURCE_ID = %s",
- [self._resourceID])
- return [row[0] for row in rows]
-
-
- def addressbookObjects(self):
- for name in self.listAddressbookObjects():
- yield self.addressbookObjectWithName(name)
-
-
- @memoized('name', '_objects')
- def addressbookObjectWithName(self, name):
- rows = self._txn.execSQL(
- "select RESOURCE_ID from ADDRESSBOOK_OBJECT where "
- "RESOURCE_NAME = %s and ADDRESSBOOK_RESOURCE_ID = %s",
- [name, self._resourceID]
- )
- if not rows:
- return None
- resid = rows[0][0]
- return PostgresAddressBookObject(self, name, resid)
-
-
- @memoized('uid', '_objects')
- def addressbookObjectWithUID(self, uid):
- rows = self._txn.execSQL(
- "select RESOURCE_ID, RESOURCE_NAME from ADDRESSBOOK_OBJECT where "
- "VCARD_UID = %s and ADDRESSBOOK_RESOURCE_ID = %s",
- [uid, self._resourceID]
- )
- if not rows:
- return None
- resid = rows[0][0]
- name = rows[0][1]
- return PostgresAddressBookObject(self, name, resid)
-
-
- def createAddressBookObjectWithName(self, name, component):
- rows = self._txn.execSQL(
- "select RESOURCE_NAME from ADDRESSBOOK_OBJECT where "
- " RESOURCE_NAME = %s AND ADDRESSBOOK_RESOURCE_ID = %s",
- [name, self._resourceID]
- )
- if rows:
- raise ObjectResourceNameAlreadyExistsError()
-
- addressbookObject = PostgresAddressBookObject(self, name, None)
- addressbookObject.component = lambda : component
-
- validateAddressBookComponent(addressbookObject, self, component)
-
- addressbookObject.updateDatabase(component, inserting=True)
- self._insertRevision(name)
-
- if self._notifier:
- self._home._txn.postCommit(self._notifier.notify)
-
-
- def removeAddressBookObjectWithName(self, name):
- self._txn.execSQL(
- "delete from ADDRESSBOOK_OBJECT where RESOURCE_NAME = %s and "
- "ADDRESSBOOK_RESOURCE_ID = %s",
- [name, self._resourceID]
- )
- if self._txn._cursor.rowcount == 0:
- raise NoSuchObjectResourceError()
- self._objects.pop(name, None)
- self._deleteRevision(name)
-
- if self._notifier:
- self._txn.postCommit(self._notifier.notify)
-
-
- def removeAddressBookObjectWithUID(self, uid):
- rows = self._txn.execSQL(
- "select RESOURCE_NAME from ADDRESSBOOK_OBJECT where "
- "VCARD_UID = %s AND ADDRESSBOOK_RESOURCE_ID = %s",
- [uid, self._resourceID]
- )
- if not rows:
- raise NoSuchObjectResourceError()
- name = rows[0][0]
- self._txn.execSQL(
- "delete from ADDRESSBOOK_OBJECT where VCARD_UID = %s and "
- "ADDRESSBOOK_RESOURCE_ID = %s",
- [uid, self._resourceID]
- )
- self._objects.pop(name, None)
- self._objects.pop(uid, None)
- self._deleteRevision(name)
-
- if self._notifier:
- self._home._txn.postCommit(self._notifier.notify)
-
-
- def addressbookObjectsSinceToken(self, token):
- raise NotImplementedError()
-
-
- @cached
- def properties(self):
- return PropertyStore(
- self.ownerAddressBookHome().uid(),
- self._txn,
- self._resourceID
- )
-
-
- # IDataStoreResource
- def contentType(self):
- """
- The content type of Addressbook objects is ???
- """
- return None # FIXME: verify
-
-
- def md5(self):
- return None
-
-
- def size(self):
- return 0
-
-
- def created(self):
- created = self._txn.execSQL(
- "select extract(EPOCH from CREATED) from ADDRESSBOOK where "
- "RESOURCE_ID = %s", [self._resourceID]
- )[0][0]
- return int(created)
-
-
- def modified(self):
- modified = self._txn.execSQL(
- "select extract(EPOCH from MODIFIED) from ADDRESSBOOK where "
- "RESOURCE_ID = %s", [self._resourceID]
- )[0][0]
- return int(modified)
-
-
-
-
-class PostgresAddressBookHome(object):
-
- implements(IAddressBookHome)
-
- def __init__(self, transaction, ownerUID, resourceID, notifier):
- self._txn = transaction
- self._ownerUID = ownerUID
- self._resourceID = resourceID
- self._addressbooks = {}
- self._notifier = notifier
-
-
- def retrieveOldShares(self):
- return PostgresLegacyABSharesEmulator(self)
-
-
- def uid(self):
- """
- Retrieve the unique identifier for this calendar home.
-
- @return: a string.
- """
- return self._ownerUID
-
-
- def name(self):
- """
- Implement L{IDataStoreResource.name} to return the uid.
- """
- return self.uid()
-
-
- def listChildren(self):
- """
- Retrieve the names of the children in this addressbook home.
-
- @return: an iterable of C{str}s.
- """
- # FIXME: not specified on the interface or exercised by the tests, but
- # required by clients of the implementation!
- rows = self._txn.execSQL(
- "select ADDRESSBOOK_RESOURCE_NAME from ADDRESSBOOK_BIND where "
- "ADDRESSBOOK_HOME_RESOURCE_ID = %s "
- "AND BIND_STATUS != %s",
- [self._resourceID, _BIND_STATUS_DECLINED]
- )
- names = [row[0] for row in rows]
- return names
-
-
- def createdHome(self):
- self.createAddressBookWithName("addressbook")
-
- def addressbooks(self):
- """
- Retrieve addressbooks contained in this addressbook home.
-
- @return: an iterable of L{IAddressBook}s.
- """
- names = self.listChildren()
- for name in names:
- yield self.addressbookWithName(name)
-
-
- @memoized('name', '_addressbooks')
- def addressbookWithName(self, name):
- """
- Retrieve the addressbook with the given C{name} contained in this
- addressbook home.
-
- @param name: a string.
- @return: an L{IAddressBook} or C{None} if no such addressbook
- exists.
- """
- data = self._txn.execSQL(
- "select ADDRESSBOOK_RESOURCE_ID from ADDRESSBOOK_BIND where "
- "ADDRESSBOOK_RESOURCE_NAME = %s and "
- "ADDRESSBOOK_HOME_RESOURCE_ID = %s",
- [name, self._resourceID]
- )
- if not data:
- return None
- resourceID = data[0][0]
- if self._notifier:
- childID = "%s/%s" % (self.uid(), name)
- notifier = self._notifier.clone(label="collection", id=childID)
- else:
- notifier = None
- return PostgresAddressBook(self, name, resourceID, notifier)
-
-
- def createAddressBookWithName(self, name):
- rows = self._txn.execSQL(
- "select ADDRESSBOOK_RESOURCE_NAME from ADDRESSBOOK_BIND where "
- "ADDRESSBOOK_RESOURCE_NAME = %s AND "
- "ADDRESSBOOK_HOME_RESOURCE_ID = %s",
- [name, self._resourceID]
- )
- if rows:
- raise HomeChildNameAlreadyExistsError()
- rows = self._txn.execSQL("select nextval('RESOURCE_ID_SEQ')")
- resourceID = rows[0][0]
- self._txn.execSQL(
- "insert into ADDRESSBOOK (RESOURCE_ID) values "
- "(%s)",
- [resourceID])
-
- self._txn.execSQL("""
- insert into ADDRESSBOOK_BIND (
- ADDRESSBOOK_HOME_RESOURCE_ID,
- ADDRESSBOOK_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME, BIND_MODE,
- SEEN_BY_OWNER, SEEN_BY_SHAREE, BIND_STATUS) values (
- %s, %s, %s, %s, %s, %s, %s)
- """,
- [self._resourceID, resourceID, name, _BIND_MODE_OWN, True, True,
- _BIND_STATUS_ACCEPTED]
- )
-
- addressbookType = ResourceType.addressbook #@UndefinedVariable
- newAddressbook = self.addressbookWithName(name)
- newAddressbook.properties()[
- PropertyName.fromElement(ResourceType)] = addressbookType
- newAddressbook._updateSyncToken()
-
- if self._notifier:
- self._txn.postCommit(self._notifier.notify)
-
-
- def removeAddressBookWithName(self, name):
- self._txn.execSQL(
- "delete from ADDRESSBOOK_BIND where ADDRESSBOOK_RESOURCE_NAME = %s and "
- "ADDRESSBOOK_HOME_RESOURCE_ID = %s",
- [name, self._resourceID]
- )
- self._addressbooks.pop(name, None)
- if self._txn._cursor.rowcount == 0:
- raise NoSuchHomeChildError()
- # FIXME: the schema should probably cascade the addressbook delete when
- # the last bind is deleted.
- if self._notifier:
- self._txn.postCommit(self._notifier.notify)
-
-
- @cached
- def properties(self):
- return PropertyStore(
- self.uid(),
- self._txn,
- self._resourceID
- )
-
-
- # IDataStoreResource
- def contentType(self):
- """
- The content type of Addressbook home objects is ???
- """
- return None # FIXME: verify
-
-
- def md5(self):
- return None
-
-
- def size(self):
- return 0
-
-
- def created(self):
- return None
-
-
- def modified(self):
- return None
-
-
- def notifierID(self, label="default"):
- if self._notifier:
- return self._notifier.getID(label)
- else:
- return None
Deleted: CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/postgres_schema_v1.sql
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/postgres_schema_v1.sql 2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/postgres_schema_v1.sql 2010-08-26 00:52:30 UTC (rev 6184)
@@ -1,342 +0,0 @@
------------------
--- Resource ID --
------------------
-
-create sequence RESOURCE_ID_SEQ;
-
-
--------------------
--- Calendar Home --
--------------------
-
-create table CALENDAR_HOME (
- RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'),
- OWNER_UID varchar(255) not null unique
-);
-
-
---------------
--- Calendar --
---------------
-
-create table CALENDAR (
- RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'),
- REVISION integer default 0,
- CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
- MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP)
-);
-
-
-------------------------
--- Sharing Invitation --
-------------------------
-
-create table INVITE (
- INVITE_UID varchar(255) not null,
- NAME varchar(255) not null,
- RECIPIENT_ADDRESS varchar(255) not null,
- HOME_RESOURCE_ID integer not null,
- RESOURCE_ID integer not null
-);
-
-
----------------------------
--- Sharing Notifications --
----------------------------
-
-create table NOTIFICATION_HOME (
- RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'),
- OWNER_UID varchar(255) not null unique
-);
-
-
-create table NOTIFICATION (
- RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'),
- NOTIFICATION_HOME_RESOURCE_ID integer not null references NOTIFICATION_HOME,
- NOTIFICATION_UID varchar(255) not null,
- XML_TYPE varchar not null,
- XML_DATA varchar not null,
- CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
- MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
-
- unique(NOTIFICATION_UID, NOTIFICATION_HOME_RESOURCE_ID)
-);
-
-
--------------------
--- Calendar Bind --
--------------------
-
--- Joins CALENDAR_HOME and CALENDAR
-
-create table CALENDAR_BIND (
- CALENDAR_HOME_RESOURCE_ID integer not null references CALENDAR_HOME,
- CALENDAR_RESOURCE_ID integer not null references CALENDAR,
-
- -- An invitation which hasn't been accepted yet will not yet have a resource
- -- name, so this field may be null.
-
- CALENDAR_RESOURCE_NAME varchar(255),
- BIND_MODE integer not null, -- enum CALENDAR_BIND_MODE
- BIND_STATUS integer not null, -- enum CALENDAR_BIND_STATUS
- SEEN_BY_OWNER boolean not null,
- SEEN_BY_SHAREE boolean not null,
- MESSAGE text,
-
- primary key(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID),
- unique(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_NAME)
-);
-
--- Enumeration of calendar bind modes
-
-create table CALENDAR_BIND_MODE (
- ID integer primary key,
- DESCRIPTION varchar(16) not null unique
-);
-
-insert into CALENDAR_BIND_MODE values (0, 'own' );
-insert into CALENDAR_BIND_MODE values (1, 'read' );
-insert into CALENDAR_BIND_MODE values (2, 'write');
-
--- Enumeration of statuses
-
-create table CALENDAR_BIND_STATUS (
- ID integer primary key,
- DESCRIPTION varchar(16) not null unique
-);
-
-insert into CALENDAR_BIND_STATUS values (0, 'invited' );
-insert into CALENDAR_BIND_STATUS values (1, 'accepted');
-insert into CALENDAR_BIND_STATUS values (2, 'declined');
-insert into CALENDAR_BIND_STATUS values (3, 'invalid');
-
-
----------------------
--- Calendar Object --
----------------------
-
-create table CALENDAR_OBJECT (
- RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'),
- CALENDAR_RESOURCE_ID integer not null references CALENDAR,
- RESOURCE_NAME varchar(255) not null,
- ICALENDAR_TEXT text not null,
- ICALENDAR_UID varchar(255) not null,
- ICALENDAR_TYPE varchar(255) not null,
- ATTACHMENTS_MODE integer not null, -- enum CALENDAR_OBJECT_ATTACHMENTS_MODE
- ORGANIZER varchar(255),
- ORGANIZER_OBJECT integer references CALENDAR_OBJECT,
- RECURRANCE_MAX date, -- maximum date that recurrences have been expanded to.
- CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
- MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
-
- unique(CALENDAR_RESOURCE_ID, RESOURCE_NAME)
-
- -- since the 'inbox' is a 'calendar resource' for the purpose of storing
- -- calendar objects, this constraint has to be selectively enforced by the
- -- application layer.
-
- -- unique(CALENDAR_RESOURCE_ID, ICALENDAR_UID)
-);
-
--- Enumeration of attachment modes
-
-create table CALENDAR_OBJECT_ATTACHMENTS_MODE (
- ID integer primary key,
- DESCRIPTION varchar(16) not null unique
-);
-
-insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (0, 'read' );
-insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (1, 'write');
-
-
------------------
--- Instance ID --
------------------
-
-create sequence INSTANCE_ID_SEQ;
-
-
-----------------
--- Time Range --
-----------------
-
-create table TIME_RANGE (
- INSTANCE_ID integer primary key default nextval('INSTANCE_ID_SEQ'),
- CALENDAR_RESOURCE_ID integer not null references CALENDAR,
- CALENDAR_OBJECT_RESOURCE_ID integer not null references CALENDAR_OBJECT on delete cascade,
- FLOATING boolean not null,
- START_DATE timestamp not null,
- END_DATE timestamp not null,
- FBTYPE integer not null,
- TRANSPARENT boolean not null
-);
-
--- Enumeration of free/busy types
-
-create table FREE_BUSY_TYPE (
- ID integer primary key,
- DESCRIPTION varchar(16) not null unique
-);
-
-insert into FREE_BUSY_TYPE values (0, 'unknown' );
-insert into FREE_BUSY_TYPE values (1, 'free' );
-insert into FREE_BUSY_TYPE values (2, 'busy' );
-insert into FREE_BUSY_TYPE values (3, 'busy-unavailable');
-insert into FREE_BUSY_TYPE values (4, 'busy-tentative' );
-
-
-------------------
--- Transparency --
-------------------
-
-create table TRANSPARENCY (
- TIME_RANGE_INSTANCE_ID integer not null references TIME_RANGE on delete cascade,
- USER_ID varchar(255) not null,
- TRANSPARENT boolean not null
-);
-
-
-----------------
--- Attachment --
-----------------
-
-create table ATTACHMENT (
- CALENDAR_OBJECT_RESOURCE_ID integer not null references CALENDAR_OBJECT on delete cascade,
- CONTENT_TYPE varchar(255) not null,
- SIZE integer not null,
- MD5 char(32) not null,
- CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
- MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
- PATH varchar(1024) not null unique
-);
-
-
-------------------------------
--- Calendar Object Revision --
-------------------------------
-
-create sequence CALENDAR_OBJECT_REVISION_SEQ;
-
-
--------------------------------
--- Calendar Object Revisions --
--------------------------------
-
-create table CALENDAR_OBJECT_REVISIONS (
- CALENDAR_RESOURCE_ID integer not null references CALENDAR on delete cascade,
- RESOURCE_NAME varchar(255) not null,
- REVISION integer not null,
- DELETED boolean not null,
-
- unique(CALENDAR_RESOURCE_ID, RESOURCE_NAME)
-);
-
-
-------------------
--- iTIP Message --
-------------------
-
-create table ITIP_MESSAGE (
- CALENDAR_RESOURCE_ID integer not null references CALENDAR,
- ICALENDAR_TEXT text not null,
- ICALENDAR_UID varchar(255) not null,
- MD5 char(32) not null,
- CHANGES text not null
-);
-
-
------------------------
--- Resource Property --
------------------------
-
-create table RESOURCE_PROPERTY (
- RESOURCE_ID integer not null, -- foreign key: *.RESOURCE_ID
- NAME varchar(255) not null,
- VALUE text not null, -- FIXME: xml?
- VIEWER_UID varchar(255),
-
- primary key(RESOURCE_ID, NAME, VIEWER_UID)
-);
-
-
-----------------------
--- AddressBook Home --
-----------------------
-
-create table ADDRESSBOOK_HOME (
- RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'),
- OWNER_UID varchar(255) not null unique
-);
-
-
------------------
--- AddressBook --
------------------
-
-create table ADDRESSBOOK (
- RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'),
- REVISION integer default 0,
- CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
- MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP)
-);
-
-
-----------------------
--- AddressBook Bind --
-----------------------
-
--- Joins ADDRESSBOOK_HOME and ADDRESSBOOK
-
-create table ADDRESSBOOK_BIND (
- ADDRESSBOOK_HOME_RESOURCE_ID integer not null references ADDRESSBOOK_HOME,
- ADDRESSBOOK_RESOURCE_ID integer not null references ADDRESSBOOK,
-
- -- An invitation which hasn't been accepted yet will not yet have a resource
- -- name, so this field may be null.
-
- ADDRESSBOOK_RESOURCE_NAME varchar(255),
- BIND_MODE integer not null, -- enum CALENDAR_BIND_MODE
- BIND_STATUS integer not null, -- enum CALENDAR_BIND_STATUS
- SEEN_BY_OWNER boolean not null,
- SEEN_BY_SHAREE boolean not null,
- MESSAGE text, -- FIXME: xml?
-
- primary key(ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_ID),
- unique(ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME)
-);
-
-
-create table ADDRESSBOOK_OBJECT (
- RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'),
- ADDRESSBOOK_RESOURCE_ID integer not null references ADDRESSBOOK,
- RESOURCE_NAME varchar(255) not null,
- VCARD_TEXT text not null,
- VCARD_UID varchar(255) not null,
- CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
- MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
-
- unique(ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME),
- unique(ADDRESSBOOK_RESOURCE_ID, VCARD_UID)
-);
-
-------------------------------
--- AddressBook Object Revision --
-------------------------------
-
-create sequence ADDRESSBOOK_OBJECT_REVISION_SEQ;
-
-
--------------------------------
--- AddressBook Object Revisions --
--------------------------------
-
-create table ADDRESSBOOK_OBJECT_REVISIONS (
- ADDRESSBOOK_RESOURCE_ID integer not null references ADDRESSBOOK on delete cascade,
- RESOURCE_NAME varchar(255) not null,
- REVISION integer not null,
- DELETED boolean not null,
-
- unique(ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME)
-);
-
-
Added: CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/sql.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/sql.py (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/sql.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,634 @@
+# -*- test-case-name: txcaldav.calendarstore.test.test_sql -*-
+##
+# 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__ = [
+ "CalendarHome",
+ "Calendar",
+ "CalendarObject",
+]
+
+from twext.python.vcomponent import VComponent
+from twext.web2.dav.element.rfc2518 import ResourceType
+from twext.web2.http_headers import MimeType, generateContentType
+
+from twisted.internet.error import ConnectionLost
+from twisted.internet.interfaces import ITransport
+from twisted.python import hashlib
+from twisted.python.failure import Failure
+
+from twistedcaldav import caldavxml, customxml
+from twistedcaldav.caldavxml import ScheduleCalendarTransp, Opaque
+from twistedcaldav.dateops import normalizeForIndex
+from twistedcaldav.index import IndexedSearchException
+from twistedcaldav.instance import InvalidOverriddenInstanceError
+
+from txcaldav.calendarstore.util import validateCalendarComponent,\
+ dropboxIDFromCalendarObject
+from txcaldav.icalendarstore import ICalendarHome, ICalendar, ICalendarObject,\
+ IAttachment
+
+from txdav.common.datastore.sql import CommonHome, CommonHomeChild,\
+ CommonObjectResource
+from txdav.common.datastore.sql_legacy import \
+ PostgresLegacyIndexEmulator, PostgresLegacyInvitesEmulator,\
+ PostgresLegacySharesEmulator
+from txdav.common.datastore.sql_tables import CALENDAR_TABLE,\
+ CALENDAR_BIND_TABLE, CALENDAR_OBJECT_REVISIONS_TABLE, CALENDAR_OBJECT_TABLE,\
+ _ATTACHMENTS_MODE_WRITE
+from txdav.propertystore.base import PropertyName
+
+from vobject.icalendar import utc
+
+import datetime
+
+from zope.interface.declarations import implements
+
+class CalendarHome(CommonHome):
+
+ implements(ICalendarHome)
+
+ def __init__(self, transaction, ownerUID, resourceID, notifier):
+ super(CalendarHome, self).__init__(transaction, ownerUID, resourceID, notifier)
+
+ self._shares = PostgresLegacySharesEmulator(self)
+ self._childClass = Calendar
+ self._childTable = CALENDAR_TABLE
+ self._bindTable = CALENDAR_BIND_TABLE
+
+ createCalendarWithName = CommonHome.createChildWithName
+ removeCalendarWithName = CommonHome.removeChildWithName
+ calendarWithName = CommonHome.childWithName
+ calendars = CommonHome.children
+ listCalendars = CommonHome.listChildren
+
+ def calendarObjectWithDropboxID(self, dropboxID):
+ """
+ Implement lookup with brute-force scanning.
+ """
+ for calendar in self.calendars():
+ for calendarObject in calendar.calendarObjects():
+ if dropboxID == calendarObject.dropboxID():
+ return calendarObject
+
+
+ def createdHome(self):
+ self.createCalendarWithName("calendar")
+ defaultCal = self.calendarWithName("calendar")
+ props = defaultCal.properties()
+ props[PropertyName(*ScheduleCalendarTransp.qname())] = ScheduleCalendarTransp(
+ Opaque())
+ self.createCalendarWithName("inbox")
+
+class Calendar(CommonHomeChild):
+ """
+ File-based implementation of L{ICalendar}.
+ """
+ implements(ICalendar)
+
+ def __init__(self, home, name, resourceID, notifier):
+ """
+ Initialize a calendar pointing at a path on disk.
+
+ @param name: the subdirectory of calendarHome where this calendar
+ resides.
+ @type name: C{str}
+
+ @param calendarHome: the home containing this calendar.
+ @type calendarHome: L{CalendarHome}
+
+ @param realName: If this calendar was just created, the name which it
+ will eventually have on disk.
+ @type realName: C{str}
+ """
+ super(Calendar, self).__init__(home, name, resourceID, notifier)
+
+ self._index = PostgresLegacyIndexEmulator(self)
+ self._invites = PostgresLegacyInvitesEmulator(self)
+ self._objectResourceClass = CalendarObject
+ self._bindTable = CALENDAR_BIND_TABLE
+ self._homeChildTable = CALENDAR_TABLE
+ self._revisionsTable = CALENDAR_OBJECT_REVISIONS_TABLE
+ self._objectTable = CALENDAR_OBJECT_TABLE
+
+
+ @property
+ def _calendarHome(self):
+ return self._home
+
+
+ def resourceType(self):
+ return ResourceType.calendar #@UndefinedVariable
+
+
+ ownerCalendarHome = CommonHomeChild.ownerHome
+ calendarObjects = CommonHomeChild.objectResources
+ listCalendarObjects = CommonHomeChild.listObjectResources
+ calendarObjectWithName = CommonHomeChild.objectResourceWithName
+ calendarObjectWithUID = CommonHomeChild.objectResourceWithUID
+ createCalendarObjectWithName = CommonHomeChild.createObjectResourceWithName
+ removeCalendarObjectWithName = CommonHomeChild.removeObjectResourceWithName
+ removeCalendarObjectWithUID = CommonHomeChild.removeObjectResourceWithUID
+ calendarObjectsSinceToken = CommonHomeChild.objectResourcesSinceToken
+
+
+ def calendarObjectsInTimeRange(self, start, end, timeZone):
+ raise NotImplementedError()
+
+
+ def initPropertyStore(self, props):
+ # Setup peruser special properties
+ props.setSpecialProperties(
+ (
+ PropertyName.fromElement(caldavxml.CalendarDescription),
+ PropertyName.fromElement(caldavxml.CalendarTimeZone),
+ ),
+ (
+ PropertyName.fromElement(customxml.GETCTag),
+ PropertyName.fromElement(caldavxml.SupportedCalendarComponentSet),
+ PropertyName.fromElement(caldavxml.ScheduleCalendarTransp),
+ ),
+ )
+
+ def contentType(self):
+ """
+ The content type of Calendar objects is text/calendar.
+ """
+ return MimeType.fromString("text/calendar; charset=utf-8")
+
+#
+# Duration into the future through which recurrences are expanded in the index
+# by default. This is a caching parameter which affects the size of the index;
+# it does not affect search results beyond this period, but it may affect
+# performance of such a search.
+#
+default_future_expansion_duration = datetime.timedelta(days=365 * 1)
+
+#
+# Maximum duration into the future through which recurrences are expanded in the
+# index. This is a caching parameter which affects the size of the index; it
+# does not affect search results beyond this period, but it may affect
+# performance of such a search.
+#
+# When a search is performed on a time span that goes beyond that which is
+# expanded in the index, we have to open each resource which may have data in
+# that time period. In order to avoid doing that multiple times, we want to
+# cache those results. However, we don't necessarily want to cache all
+# occurrences into some obscenely far-in-the-future date, so we cap the caching
+# period. Searches beyond this period will always be relatively expensive for
+# resources with occurrences beyond this period.
+#
+maximum_future_expansion_duration = datetime.timedelta(days=365 * 5)
+
+icalfbtype_to_indexfbtype = {
+ "UNKNOWN" : 0,
+ "FREE" : 1,
+ "BUSY" : 2,
+ "BUSY-UNAVAILABLE": 3,
+ "BUSY-TENTATIVE" : 4,
+}
+
+indexfbtype_to_icalfbtype = {
+ 0: '?',
+ 1: 'F',
+ 2: 'B',
+ 3: 'U',
+ 4: 'T',
+}
+
+def _pathToName(path):
+ return path.rsplit(".", 1)[0].split("-", 3)[-1]
+
+class CalendarObject(CommonObjectResource):
+ implements(ICalendarObject)
+
+ def __init__(self, name, calendar, resid):
+ super(CalendarObject, self).__init__(name, calendar, resid)
+
+ self._objectTable = CALENDAR_OBJECT_TABLE
+
+ @property
+ def _calendar(self):
+ return self._parentCollection
+
+ def calendar(self):
+ return self._calendar
+
+ def setComponent(self, component, inserting=False):
+ validateCalendarComponent(self, self._calendar, component, inserting)
+
+ self.updateDatabase(component, inserting=inserting)
+ if inserting:
+ self._calendar._insertRevision(self._name)
+ else:
+ self._calendar._updateRevision(self._name)
+
+ if self._calendar._notifier:
+ self._txn.postCommit(self._calendar._notifier.notify)
+
+ def updateDatabase(self, component, expand_until=None, reCreate=False, inserting=False):
+ """
+ Update the database tables for the new data being written.
+
+ @param component: calendar data to store
+ @type component: L{Component}
+ """
+
+ # Decide how far to expand based on the component
+ master = component.masterComponent()
+ if master is None or not component.isRecurring() and not component.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 = component.expandTimeRanges(expand, ignoreInvalidInstances=reCreate)
+ except InvalidOverriddenInstanceError, e:
+ self.log_err("Invalid instance %s when indexing %s in %s" % (e.rid, self._name, self.resource,))
+ raise
+
+ componentText = str(component)
+ self._objectText = componentText
+ organizer = component.getOrganizer()
+ if not organizer:
+ organizer = ""
+
+ # CALENDAR_OBJECT table update
+ if inserting:
+ self._resourceID = self._txn.execSQL(
+ """
+ insert into CALENDAR_OBJECT
+ (CALENDAR_RESOURCE_ID, RESOURCE_NAME, ICALENDAR_TEXT, ICALENDAR_UID, ICALENDAR_TYPE, ATTACHMENTS_MODE, ORGANIZER, RECURRANCE_MAX)
+ values
+ (%s, %s, %s, %s, %s, %s, %s, %s)
+ returning RESOURCE_ID
+ """,
+ # FIXME: correct ATTACHMENTS_MODE based on X-APPLE-
+ # DROPBOX
+ [
+ self._calendar._resourceID,
+ self._name,
+ componentText,
+ component.resourceUID(),
+ component.resourceType(),
+ _ATTACHMENTS_MODE_WRITE,
+ organizer,
+ normalizeForIndex(instances.limit) if instances.limit else None,
+ ]
+ )[0][0]
+ else:
+ self._txn.execSQL(
+ """
+ update CALENDAR_OBJECT set
+ (ICALENDAR_TEXT, ICALENDAR_UID, ICALENDAR_TYPE, ATTACHMENTS_MODE, ORGANIZER, RECURRANCE_MAX, MODIFIED)
+ =
+ (%s, %s, %s, %s, %s, %s, timezone('UTC', CURRENT_TIMESTAMP))
+ where RESOURCE_ID = %s
+ """,
+ # should really be filling out more fields: ORGANIZER,
+ # ORGANIZER_OBJECT, a correct ATTACHMENTS_MODE based on X-APPLE-
+ # DROPBOX
+ [
+ componentText,
+ component.resourceUID(),
+ component.resourceType(),
+ _ATTACHMENTS_MODE_WRITE,
+ organizer,
+ normalizeForIndex(instances.limit) if instances.limit else None,
+ self._resourceID
+ ]
+ )
+
+ # Need to wipe the existing time-range for this and rebuild
+ self._txn.execSQL(
+ """
+ delete from TIME_RANGE where CALENDAR_OBJECT_RESOURCE_ID = %s
+ """,
+ [
+ self._resourceID,
+ ],
+ )
+
+
+ # CALENDAR_OBJECT table update
+ for key in instances:
+ instance = instances[key]
+ start = instance.start.replace(tzinfo=utc)
+ end = instance.end.replace(tzinfo=utc)
+ float = instance.start.tzinfo is None
+ transp = instance.component.propertyValue("TRANSP") == "TRANSPARENT"
+ instanceid = self._txn.execSQL(
+ """
+ insert into TIME_RANGE
+ (CALENDAR_RESOURCE_ID, CALENDAR_OBJECT_RESOURCE_ID, FLOATING, START_DATE, END_DATE, FBTYPE, TRANSPARENT)
+ values
+ (%s, %s, %s, %s, %s, %s, %s)
+ returning
+ INSTANCE_ID
+ """,
+ [
+ self._calendar._resourceID,
+ self._resourceID,
+ float,
+ start,
+ end,
+ icalfbtype_to_indexfbtype.get(instance.component.getFBType(), icalfbtype_to_indexfbtype["FREE"]),
+ transp,
+ ],
+ )[0][0]
+ peruserdata = component.perUserTransparency(instance.rid)
+ for useruid, transp in peruserdata:
+ self._txn.execSQL(
+ """
+ insert into TRANSPARENCY
+ (TIME_RANGE_INSTANCE_ID, USER_ID, TRANSPARENT)
+ values
+ (%s, %s, %s)
+ """,
+ [
+ instanceid,
+ useruid,
+ transp,
+ ],
+ )
+
+ # Special - for unbounded recurrence we insert a value for "infinity"
+ # that will allow an open-ended time-range to always match it.
+ if component.isRecurringUnbounded():
+ start = datetime.datetime(2100, 1, 1, 0, 0, 0, tzinfo=utc)
+ end = datetime.datetime(2100, 1, 1, 1, 0, 0, tzinfo=utc)
+ float = False
+ instanceid = self._txn.execSQL(
+ """
+ insert into TIME_RANGE
+ (CALENDAR_RESOURCE_ID, CALENDAR_OBJECT_RESOURCE_ID, FLOATING, START_DATE, END_DATE, FBTYPE, TRANSPARENT)
+ values
+ (%s, %s, %s, %s, %s, %s, %s)
+ returning
+ INSTANCE_ID
+ """,
+ [
+ self._calendar._resourceID,
+ self._resourceID,
+ float,
+ start,
+ end,
+ icalfbtype_to_indexfbtype["UNKNOWN"],
+ True,
+ ],
+ )[0][0]
+ peruserdata = component.perUserTransparency(None)
+ for useruid, transp in peruserdata:
+ self._txn.execSQL(
+ """
+ insert into TRANSPARENCY
+ (TIME_RANGE_INSTANCE_ID, USER_ID, TRANSPARENT)
+ values
+ (%s, %s, %s)
+ """,
+ [
+ instanceid,
+ useruid,
+ transp,
+ ],
+ )
+
+ def component(self):
+ return VComponent.fromString(self.iCalendarText())
+
+ def text(self):
+ if self._objectText is None:
+ text = self._txn.execSQL(
+ "select ICALENDAR_TEXT from CALENDAR_OBJECT where "
+ "RESOURCE_ID = %s", [self._resourceID]
+ )[0][0]
+ self._objectText = text
+ return text
+ else:
+ return self._objectText
+
+ iCalendarText = text
+
+ def uid(self):
+ return self.component().resourceUID()
+
+ def name(self):
+ return self._name
+
+ def componentType(self):
+ return self.component().mainType()
+
+ def organizer(self):
+ return self.component().getOrganizer()
+
+ def createAttachmentWithName(self, name, contentType):
+ path = self._attachmentPath(name)
+ attachment = Attachment(self, path)
+ self._txn.execSQL("""
+ insert into ATTACHMENT (CALENDAR_OBJECT_RESOURCE_ID, CONTENT_TYPE,
+ SIZE, MD5, PATH)
+ values (%s, %s, %s, %s, %s)
+ """,
+ [
+ self._resourceID, generateContentType(contentType), 0, "",
+ attachment._pathValue()
+ ]
+ )
+ return attachment.store(contentType)
+
+ def removeAttachmentWithName(self, name):
+ attachment = Attachment(self, self._attachmentPath(name))
+ self._txn.postCommit(attachment._path.remove)
+ self._txn.execSQL("""
+ delete from ATTACHMENT where CALENDAR_OBJECT_RESOURCE_ID = %s AND
+ PATH = %s
+ """, [self._resourceID, attachment._pathValue()])
+
+ def attachmentWithName(self, name):
+ attachment = Attachment(self, self._attachmentPath(name))
+ if attachment._populate():
+ return attachment
+ else:
+ return None
+
+ def attendeesCanManageAttachments(self):
+ return self.component().hasPropertyInAnyComponent("X-APPLE-DROPBOX")
+
+ def dropboxID(self):
+ return dropboxIDFromCalendarObject(self)
+
+ def _attachmentPath(self, name):
+ attachmentRoot = self._txn._store.attachmentsPath
+ try:
+ attachmentRoot.createDirectory()
+ except:
+ pass
+ return attachmentRoot.child(
+ "%s-%s-%s-%s.attachment" % (
+ self._calendar._home.uid(), self._calendar.name(),
+ self.name(), name
+ )
+ )
+
+ def attachments(self):
+ rows = self._txn.execSQL("""
+ select PATH from ATTACHMENT where CALENDAR_OBJECT_RESOURCE_ID = %s
+ """, [self._resourceID])
+ for row in rows:
+ demangledName = _pathToName(row[0])
+ yield self.attachmentWithName(demangledName)
+
+ def initPropertyStore(self, props):
+ # Setup peruser special properties
+ props.setSpecialProperties(
+ (
+ ),
+ (
+ PropertyName.fromElement(customxml.TwistedCalendarAccessProperty),
+ PropertyName.fromElement(customxml.TwistedSchedulingObjectResource),
+ PropertyName.fromElement(caldavxml.ScheduleTag),
+ PropertyName.fromElement(customxml.TwistedScheduleMatchETags),
+ PropertyName.fromElement(customxml.TwistedCalendarHasPrivateCommentsProperty),
+ PropertyName.fromElement(caldavxml.Originator),
+ PropertyName.fromElement(caldavxml.Recipient),
+ PropertyName.fromElement(customxml.ScheduleChanges),
+ ),
+ )
+
+ # IDataStoreResource
+ def contentType(self):
+ """
+ The content type of Calendar objects is text/calendar.
+ """
+ return MimeType.fromString("text/calendar; charset=utf-8")
+
+class AttachmentStorageTransport(object):
+
+ implements(ITransport)
+
+ def __init__(self, attachment, contentType):
+ self.attachment = attachment
+ self.contentType = contentType
+ self.buf = ''
+ self.hash = hashlib.md5()
+
+
+ @property
+ def _txn(self):
+ return self.attachment._txn
+
+
+ def write(self, data):
+ self.buf += data
+ self.hash.update(data)
+
+
+ def loseConnection(self):
+ self.attachment._path.setContent(self.buf)
+ pathValue = self.attachment._pathValue()
+ contentTypeString = generateContentType(self.contentType)
+ self._txn.execSQL(
+ "update ATTACHMENT set CONTENT_TYPE = %s, SIZE = %s, MD5 = %s, MODIFIED = timezone('UTC', CURRENT_TIMESTAMP) "
+ "WHERE PATH = %s",
+ [contentTypeString, len(self.buf), self.hash.hexdigest(), pathValue]
+ )
+
+class Attachment(object):
+
+ implements(IAttachment)
+
+ def __init__(self, calendarObject, path):
+ self._calendarObject = calendarObject
+ self._path = path
+
+
+ @property
+ def _txn(self):
+ return self._calendarObject._txn
+
+
+ def _populate(self):
+ """
+ Execute necessary SQL queries to retrieve attributes.
+
+ @return: C{True} if this attachment exists, C{False} otherwise.
+ """
+ rows = self._txn.execSQL(
+ """
+ select CONTENT_TYPE, SIZE, MD5, extract(EPOCH from CREATED), extract(EPOCH from MODIFIED) from ATTACHMENT where PATH = %s
+ """, [self._pathValue()])
+ if not rows:
+ return False
+ self._contentType = MimeType.fromString(rows[0][0])
+ self._size = rows[0][1]
+ self._md5 = rows[0][2]
+ self._created = int(rows[0][3])
+ self._modified = int(rows[0][4])
+ return True
+
+
+ def name(self):
+ return _pathToName(self._pathValue())
+
+
+ def _pathValue(self):
+ """
+ Compute the value which should go into the 'path' column for this
+ attachment.
+ """
+ root = self._txn._store.attachmentsPath
+ return '/'.join(self._path.segmentsFrom(root))
+
+ def properties(self):
+ pass # stub
+
+
+ def store(self, contentType):
+ return AttachmentStorageTransport(self, contentType)
+
+
+ def retrieve(self, protocol):
+ protocol.dataReceived(self._path.getContent())
+ protocol.connectionLost(Failure(ConnectionLost()))
+
+
+ # IDataStoreResource
+ def contentType(self):
+ return self._contentType
+
+
+ def md5(self):
+ return self._md5
+
+
+ def size(self):
+ return self._size
+
+
+ def created(self):
+ return self._created
+
+ def modified(self):
+ return self._modified
Deleted: CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/test/test_postgres.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/test/test_postgres.py 2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/test/test_postgres.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -1,245 +0,0 @@
-##
-# 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.
-##
-
-"""
-Tests for txcaldav.calendarstore.postgres, mostly based on
-L{txcaldav.calendarstore.test.common}.
-"""
-
-import gc
-
-from txcaldav.calendarstore.test.common import CommonTests as CalendarCommonTests
-from txcarddav.addressbookstore.test.common import CommonTests as AddressBookCommonTests
-from txdav.common.icommondatastore import NoSuchHomeChildError
-from txdav.common.datastore.sql import CommonDataStore
-
-from twisted.trial import unittest
-from txdav.datastore.subpostgres import (PostgresService,
- DiagnosticConnectionWrapper)
-from txcaldav.calendarstore.postgres import v1_schema
-from twisted.internet.defer import Deferred, inlineCallbacks, succeed
-from twisted.internet import reactor
-from twext.python.filepath import CachingFilePath
-from twext.python.vcomponent import VComponent
-from twistedcaldav.vcard import Component as VCard
-from twisted.internet.task import deferLater
-from twisted.python import log
-
-
-
-def allInstancesOf(cls):
- for o in gc.get_referrers(cls):
- if isinstance(o, cls):
- yield o
-
-
-
-def dumpConnectionStatus():
- print '+++ ALL CONNECTIONS +++'
- for connection in allInstancesOf(DiagnosticConnectionWrapper):
- print connection.label, connection.state
- print '--- CONNECTIONS END ---'
-
-
-
-class StoreBuilder(object):
- """
- Test-fixture-builder which can construct a PostgresStore.
- """
- sharedService = None
- currentTestID = None
-
- SHARED_DB_PATH = "../_test_postgres_db"
-
- def buildStore(self, testCase, notifierFactory):
- """
- Do the necessary work to build a store for a particular test case.
-
- @return: a L{Deferred} which fires with an L{IDataStore}.
- """
- currentTestID = testCase.id()
- dbRoot = CachingFilePath(self.SHARED_DB_PATH)
- if self.sharedService is None:
- ready = Deferred()
- def getReady(connectionFactory):
- attachmentRoot = dbRoot.child("attachments")
- try:
- attachmentRoot.createDirectory()
- except OSError:
- pass
- try:
- self.store = CommonDataStore(
- lambda label=None: connectionFactory(
- label or currentTestID
- ),
- notifierFactory,
- attachmentRoot
- )
- except:
- ready.errback()
- raise
- else:
- self.cleanDatabase(testCase)
- ready.callback(self.store)
- return self.store
- self.sharedService = PostgresService(
- dbRoot, getReady, v1_schema, "caldav", resetSchema=True,
- testMode=True
- )
- self.sharedService.startService()
- def startStopping():
- log.msg("Starting stopping.")
- self.sharedService.unpauseMonitor()
- return self.sharedService.stopService()
- reactor.addSystemEventTrigger(#@UndefinedVariable
- "before", "shutdown", startStopping)
- result = ready
- else:
- self.store.notifierFactory = notifierFactory
- self.cleanDatabase(testCase)
- result = succeed(self.store)
-
- def cleanUp():
- # FIXME: clean up any leaked connections and report them with an
- # immediate test failure.
- def stopit():
- self.sharedService.pauseMonitor()
- return deferLater(reactor, 0.1, stopit)
- testCase.addCleanup(cleanUp)
- return result
-
-
- def cleanDatabase(self, testCase):
- cleanupConn = self.store.connectionFactory(
- "%s schema-cleanup" % (testCase.id(),)
- )
- cursor = cleanupConn.cursor()
- tables = ['INVITE',
- 'RESOURCE_PROPERTY',
- 'ATTACHMENT',
- 'ADDRESSBOOK_OBJECT',
- 'CALENDAR_OBJECT',
- 'CALENDAR_BIND',
- 'ADDRESSBOOK_BIND',
- 'CALENDAR',
- 'ADDRESSBOOK',
- 'CALENDAR_HOME',
- 'ADDRESSBOOK_HOME',
- 'NOTIFICATION',
- 'NOTIFICATION_HOME']
- for table in tables:
- try:
- cursor.execute("delete from "+table)
- except:
- log.err()
- cleanupConn.commit()
- cleanupConn.close()
-
-
-
-theStoreBuilder = StoreBuilder()
-buildStore = theStoreBuilder.buildStore
-
-
-
-class CalendarSQLStorageTests(CalendarCommonTests, unittest.TestCase):
- """
- Calendar SQL storage tests.
- """
-
- @inlineCallbacks
- def setUp(self):
- super(CalendarSQLStorageTests, self).setUp()
- self.calendarStore = yield buildStore(self, self.notifierFactory)
- self.populate()
-
-
- def populate(self):
- populateTxn = self.calendarStore.newTransaction()
- for homeUID in self.requirements:
- calendars = self.requirements[homeUID]
- if calendars is not None:
- home = populateTxn.calendarHomeWithUID(homeUID, True)
- # We don't want the default calendar or inbox to appear unless it's
- # explicitly listed.
- try:
- home.removeCalendarWithName("calendar")
- home.removeCalendarWithName("inbox")
- except NoSuchHomeChildError:
- pass
- for calendarName in calendars:
- calendarObjNames = calendars[calendarName]
- if calendarObjNames is not None:
- home.createCalendarWithName(calendarName)
- calendar = home.calendarWithName(calendarName)
- for objectName in calendarObjNames:
- objData = calendarObjNames[objectName]
- calendar.createCalendarObjectWithName(
- objectName, VComponent.fromString(objData)
- )
- populateTxn.commit()
- self.notifierFactory.reset()
-
-
- def storeUnderTest(self):
- """
- Create and return a L{CalendarStore} for testing.
- """
- return self.calendarStore
-
-
-class AddressBookSQLStorageTests(AddressBookCommonTests, unittest.TestCase):
- """
- AddressBook SQL storage tests.
- """
-
- @inlineCallbacks
- def setUp(self):
- super(AddressBookSQLStorageTests, self).setUp()
- self.addressbookStore = yield buildStore(self, self.notifierFactory)
- self.populate()
-
- def populate(self):
- populateTxn = self.addressbookStore.newTransaction()
- for homeUID in self.requirements:
- addressbooks = self.requirements[homeUID]
- if addressbooks is not None:
- home = populateTxn.addressbookHomeWithUID(homeUID, True)
- # We don't want the default addressbook to appear unless it's
- # explicitly listed.
- home.removeAddressBookWithName("addressbook")
- for addressbookName in addressbooks:
- addressbookObjNames = addressbooks[addressbookName]
- if addressbookObjNames is not None:
- home.createAddressBookWithName(addressbookName)
- addressbook = home.addressbookWithName(addressbookName)
- for objectName in addressbookObjNames:
- objData = addressbookObjNames[objectName]
- addressbook.createAddressBookObjectWithName(
- objectName, VCard.fromString(objData)
- )
-
- populateTxn.commit()
- self.notifierFactory.reset()
-
-
-
- def storeUnderTest(self):
- """
- Create and return a L{AddressBookStore} for testing.
- """
- return self.addressbookStore
-
Copied: CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/test/test_sql.py (from rev 6168, CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/test/test_postgres.py)
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/test/test_sql.py (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/test/test_sql.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,78 @@
+##
+# 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.
+##
+
+"""
+Tests for txcaldav.calendarstore.postgres, mostly based on
+L{txcaldav.calendarstore.test.common}.
+"""
+
+from txcaldav.calendarstore.test.common import CommonTests as CalendarCommonTests
+
+from txdav.common.datastore.test.util import SQLStoreBuilder
+from txdav.common.icommondatastore import NoSuchHomeChildError
+
+from twisted.trial import unittest
+from twisted.internet.defer import inlineCallbacks
+from twext.python.vcomponent import VComponent
+
+
+theStoreBuilder = SQLStoreBuilder()
+buildStore = theStoreBuilder.buildStore
+
+class CalendarSQLStorageTests(CalendarCommonTests, unittest.TestCase):
+ """
+ Calendar SQL storage tests.
+ """
+
+ @inlineCallbacks
+ def setUp(self):
+ super(CalendarSQLStorageTests, self).setUp()
+ self.calendarStore = yield buildStore(self, self.notifierFactory)
+ self.populate()
+
+
+ def populate(self):
+ populateTxn = self.calendarStore.newTransaction()
+ for homeUID in self.requirements:
+ calendars = self.requirements[homeUID]
+ if calendars is not None:
+ home = populateTxn.calendarHomeWithUID(homeUID, True)
+ # We don't want the default calendar or inbox to appear unless it's
+ # explicitly listed.
+ try:
+ home.removeCalendarWithName("calendar")
+ home.removeCalendarWithName("inbox")
+ except NoSuchHomeChildError:
+ pass
+ for calendarName in calendars:
+ calendarObjNames = calendars[calendarName]
+ if calendarObjNames is not None:
+ home.createCalendarWithName(calendarName)
+ calendar = home.calendarWithName(calendarName)
+ for objectName in calendarObjNames:
+ objData = calendarObjNames[objectName]
+ calendar.createCalendarObjectWithName(
+ objectName, VComponent.fromString(objData)
+ )
+ populateTxn.commit()
+ self.notifierFactory.reset()
+
+
+ def storeUnderTest(self):
+ """
+ Create and return a L{CalendarStore} for testing.
+ """
+ return self.calendarStore
Modified: CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/util.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/util.py 2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txcaldav/calendarstore/util.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -19,14 +19,12 @@
from twext.python.vcomponent import InvalidICalendarDataError
from twext.python.vcomponent import VComponent
-from twistedcaldav.vcard import Component as VCard
-from twistedcaldav.vcard import InvalidVCardDataError
from txdav.common.icommondatastore import InvalidObjectResourceError,\
NoSuchObjectResourceError
-def validateCalendarComponent(calendarObject, calendar, component):
+def validateCalendarComponent(calendarObject, calendar, component, inserting):
"""
Validate a calendar component for a particular calendar.
@@ -44,7 +42,7 @@
raise TypeError(type(component))
try:
- if component.resourceUID() != calendarObject.uid():
+ if not inserting and component.resourceUID() != calendarObject.uid():
raise InvalidObjectResourceError(
"UID may not change (%s != %s)" % (
component.resourceUID(), calendarObject.uid()
@@ -87,219 +85,4 @@
return attachPath
return calendarObject.uid() + ".dropbox"
-
-
-def validateAddressBookComponent(addressbookObject, vcard, component):
- """
- Validate an addressbook component for a particular addressbook.
-
- @param addressbookObject: The addressbook object whose component will be replaced.
- @type addressbookObject: L{IAddressBookObject}
-
- @param addressbook: The addressbook which the L{IAddressBookObject} is present in.
- @type addressbook: L{IAddressBook}
-
- @param component: The VComponent to be validated.
- @type component: L{VComponent}
- """
-
- if not isinstance(component, VCard):
- raise TypeError(type(component))
-
- try:
- if component.resourceUID() != addressbookObject.uid():
- raise InvalidObjectResourceError(
- "UID may not change (%s != %s)" % (
- component.resourceUID(), addressbookObject.uid()
- )
- )
- except NoSuchObjectResourceError:
- pass
-
- try:
- component.validForCardDAV()
- except InvalidVCardDataError, e:
- raise InvalidObjectResourceError(e)
-
-
-
-class CalendarSyncTokenHelper(object):
- """
- This is a mixin for use by data store implementations.
- """
-
- def syncToken(self):
- revision = self._txn.execSQL(
- "select REVISION from CALENDAR where RESOURCE_ID = %s",
- [self._resourceID])[0][0]
- return "%s#%s" % (self._resourceID, revision,)
-
- def _updateSyncToken(self):
-
- self._txn.execSQL("""
- update CALENDAR
- set (REVISION)
- = (nextval('CALENDAR_OBJECT_REVISION_SEQ'))
- where RESOURCE_ID = %s
- """,
- [self._resourceID]
- )
-
- def _insertRevision(self, name):
- self._changeRevision("insert", name)
-
- def _updateRevision(self, name):
- self._changeRevision("update", name)
-
- def _deleteRevision(self, name):
- self._changeRevision("delete", name)
-
- def _changeRevision(self, action, name):
-
- nextrevision = self._txn.execSQL("""
- select nextval('CALENDAR_OBJECT_REVISION_SEQ')
- """
- )
-
- if action == "delete":
- self._txn.execSQL("""
- update CALENDAR_OBJECT_REVISIONS
- set (REVISION, DELETED) = (%s, TRUE)
- where CALENDAR_RESOURCE_ID = %s and RESOURCE_NAME = %s
- """,
- [nextrevision, self._resourceID, name]
- )
- self._txn.execSQL("""
- update CALENDAR
- set (REVISION) = (%s)
- where RESOURCE_ID = %s
- """,
- [nextrevision, self._resourceID]
- )
- elif action == "update":
- self._txn.execSQL("""
- update CALENDAR_OBJECT_REVISIONS
- set (REVISION) = (%s)
- where CALENDAR_RESOURCE_ID = %s and RESOURCE_NAME = %s
- """,
- [nextrevision, self._resourceID, name]
- )
- self._txn.execSQL("""
- update CALENDAR
- set (REVISION) = (%s)
- where RESOURCE_ID = %s
- """,
- [nextrevision, self._resourceID]
- )
- elif action == "insert":
- self._txn.execSQL("""
- delete from CALENDAR_OBJECT_REVISIONS
- where CALENDAR_RESOURCE_ID = %s and RESOURCE_NAME = %s
- """,
- [self._resourceID, name,]
- )
- self._txn.execSQL("""
- insert into CALENDAR_OBJECT_REVISIONS
- (CALENDAR_RESOURCE_ID, RESOURCE_NAME, REVISION, DELETED)
- values (%s, %s, %s, FALSE)
- """,
- [self._resourceID, name, nextrevision]
- )
- self._txn.execSQL("""
- update CALENDAR
- set (REVISION) = (%s)
- where RESOURCE_ID = %s
- """,
- [nextrevision, self._resourceID]
- )
-
-class AddressbookSyncTokenHelper(object):
- """
- This is a mixin for use by data store implementations.
- """
-
- def syncToken(self):
- revision = self._txn.execSQL(
- "select REVISION from ADDRESSBOOK where RESOURCE_ID = %s",
- [self._resourceID])[0][0]
- return "%s#%s" % (self._resourceID, revision,)
-
- def _updateSyncToken(self):
-
- self._txn.execSQL("""
- update ADDRESSBOOK
- set (REVISION)
- = (nextval('ADDRESSBOOK_OBJECT_REVISION_SEQ'))
- where RESOURCE_ID = %s
- """,
- [self._resourceID]
- )
-
- def _insertRevision(self, name):
- self._changeRevision("insert", name)
-
- def _updateRevision(self, name):
- self._changeRevision("update", name)
-
- def _deleteRevision(self, name):
- self._changeRevision("delete", name)
-
- def _changeRevision(self, action, name):
-
- nextrevision = self._txn.execSQL("""
- select nextval('ADDRESSBOOK_OBJECT_REVISION_SEQ')
- """
- )
-
- if action == "delete":
- self._txn.execSQL("""
- update ADDRESSBOOK_OBJECT_REVISIONS
- set (REVISION, DELETED) = (%s, TRUE)
- where ADDRESSBOOK_RESOURCE_ID = %s and RESOURCE_NAME = %s
- """,
- [nextrevision, self._resourceID, name]
- )
- self._txn.execSQL("""
- update ADDRESSBOOK
- set (REVISION) = (%s)
- where RESOURCE_ID = %s
- """,
- [nextrevision, self._resourceID]
- )
- elif action == "update":
- self._txn.execSQL("""
- update ADDRESSBOOK_OBJECT_REVISIONS
- set (REVISION) = (%s)
- where ADDRESSBOOK_RESOURCE_ID = %s and RESOURCE_NAME = %s
- """,
- [nextrevision, self._resourceID, name]
- )
- self._txn.execSQL("""
- update ADDRESSBOOK
- set (REVISION) = (%s)
- where RESOURCE_ID = %s
- """,
- [nextrevision, self._resourceID]
- )
- elif action == "insert":
- self._txn.execSQL("""
- delete from ADDRESSBOOK_OBJECT_REVISIONS
- where ADDRESSBOOK_RESOURCE_ID = %s and RESOURCE_NAME = %s
- """,
- [self._resourceID, name,]
- )
- self._txn.execSQL("""
- insert into ADDRESSBOOK_OBJECT_REVISIONS
- (ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME, REVISION, DELETED)
- values (%s, %s, %s, FALSE)
- """,
- [self._resourceID, name, nextrevision]
- )
- self._txn.execSQL("""
- update ADDRESSBOOK
- set (REVISION) = (%s)
- where RESOURCE_ID = %s
- """,
- [nextrevision, self._resourceID]
- )
\ No newline at end of file
Modified: CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/file.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/file.py 2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/file.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -35,14 +35,14 @@
from twistedcaldav.vcard import Component as VComponent, InvalidVCardDataError
from twistedcaldav.vcardindex import AddressBookIndex as OldIndex
+from txcarddav.addressbookstore.util import validateAddressBookComponent
from txcarddav.iaddressbookstore import IAddressBook, IAddressBookObject
from txcarddav.iaddressbookstore import IAddressBookHome
from txdav.common.datastore.file import CommonDataStore, CommonHome,\
CommonStoreTransaction, CommonHomeChild, CommonObjectResource,\
CommonStubResource
-from txdav.common.icommondatastore import InvalidObjectResourceError,\
- NoSuchObjectResourceError, InternalDataStoreError
+from txdav.common.icommondatastore import NoSuchObjectResourceError, InternalDataStoreError
from txdav.datastore.file import hidden, writeOperation
from txdav.propertystore.base import PropertyName
@@ -110,7 +110,7 @@
return self._home
def resourceType(self):
- return ResourceType.addressbook
+ return ResourceType.addressbook #@UndefinedVariable
ownerAddressBookHome = CommonHomeChild.ownerHome
addressbookObjects = CommonHomeChild.objectResources
@@ -158,25 +158,9 @@
@writeOperation
- def setComponent(self, component):
- if not isinstance(component, VComponent):
- raise TypeError(type(component))
+ def setComponent(self, component, inserting=False):
+ validateAddressBookComponent(self, self._addressbook, component, inserting)
- try:
- if component.resourceUID() != self.uid():
- raise InvalidObjectResourceError(
- "UID may not change (%s != %s)" % (
- component.resourceUID(), self.uid()
- )
- )
- except NoSuchObjectResourceError:
- pass
-
- try:
- self._addressbook._doValidate(component)
- except InvalidVCardDataError, e:
- raise InvalidObjectResourceError(e)
-
self._addressbook.retrieveOldIndex().addResource(
self.name(), component
)
Added: CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/sql.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/sql.py (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/sql.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,241 @@
+# -*- test-case-name: txcarddav.addressbookstore.test.test_sql -*-
+##
+# 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__ = [
+ "AddressBookHome",
+ "AddressBook",
+ "AddressBookObject",
+]
+
+from twext.web2.dav.element.rfc2518 import ResourceType
+from twext.web2.http_headers import MimeType
+
+from twistedcaldav import carddavxml, customxml
+from twistedcaldav.vcard import Component as VCard
+
+from txdav.common.datastore.sql_legacy import \
+ PostgresLegacyABIndexEmulator, PostgresLegacyABInvitesEmulator,\
+ PostgresLegacyABSharesEmulator
+
+from txcarddav.addressbookstore.util import validateAddressBookComponent
+from txcarddav.iaddressbookstore import IAddressBookHome, IAddressBook,\
+ IAddressBookObject
+
+from txdav.common.datastore.sql import CommonHome, CommonHomeChild,\
+ CommonObjectResource
+from txdav.common.datastore.sql_tables import ADDRESSBOOK_TABLE,\
+ ADDRESSBOOK_BIND_TABLE, ADDRESSBOOK_OBJECT_REVISIONS_TABLE,\
+ ADDRESSBOOK_OBJECT_TABLE
+from txdav.propertystore.base import PropertyName
+
+from zope.interface.declarations import implements
+
+class AddressBookHome(CommonHome):
+
+ implements(IAddressBookHome)
+
+ def __init__(self, transaction, ownerUID, resourceID, notifier):
+ super(AddressBookHome, self).__init__(transaction, ownerUID, resourceID, notifier)
+
+ self._shares = PostgresLegacyABSharesEmulator(self)
+ self._childClass = AddressBook
+ self._childTable = ADDRESSBOOK_TABLE
+ self._bindTable = ADDRESSBOOK_BIND_TABLE
+
+ addressbooks = CommonHome.children
+ listAddressbooks = CommonHome.listChildren
+ addressbookWithName = CommonHome.childWithName
+ createAddressBookWithName = CommonHome.createChildWithName
+ removeAddressBookWithName = CommonHome.removeChildWithName
+
+ def createdHome(self):
+ self.createAddressBookWithName("addressbook")
+
+class AddressBook(CommonHomeChild):
+ """
+ File-based implementation of L{IAddressBook}.
+ """
+ implements(IAddressBook)
+
+ def __init__(self, home, name, resourceID, notifier):
+ """
+ Initialize an addressbook pointing at a path on disk.
+
+ @param name: the subdirectory of addressbookHome where this addressbook
+ resides.
+ @type name: C{str}
+
+ @param addressbookHome: the home containing this addressbook.
+ @type addressbookHome: L{AddressBookHome}
+
+ @param realName: If this addressbook was just created, the name which it
+ will eventually have on disk.
+ @type realName: C{str}
+ """
+
+ super(AddressBook, self).__init__(home, name, resourceID, notifier)
+
+ self._index = PostgresLegacyABIndexEmulator(self)
+ self._invites = PostgresLegacyABInvitesEmulator(self)
+ self._objectResourceClass = AddressBookObject
+ self._bindTable = ADDRESSBOOK_BIND_TABLE
+ self._homeChildTable = ADDRESSBOOK_TABLE
+ self._revisionsTable = ADDRESSBOOK_OBJECT_REVISIONS_TABLE
+ self._objectTable = ADDRESSBOOK_OBJECT_TABLE
+
+ @property
+ def _addressbookHome(self):
+ return self._home
+
+ def resourceType(self):
+ return ResourceType.addressbook #@UndefinedVariable
+
+ ownerAddressBookHome = CommonHomeChild.ownerHome
+ addressbookObjects = CommonHomeChild.objectResources
+ listAddressbookObjects = CommonHomeChild.listObjectResources
+ addressbookObjectWithName = CommonHomeChild.objectResourceWithName
+ addressbookObjectWithUID = CommonHomeChild.objectResourceWithUID
+ createAddressBookObjectWithName = CommonHomeChild.createObjectResourceWithName
+ removeAddressBookObjectWithName = CommonHomeChild.removeObjectResourceWithName
+ removeAddressBookObjectWithUID = CommonHomeChild.removeObjectResourceWithUID
+ addressbookObjectsSinceToken = CommonHomeChild.objectResourcesSinceToken
+
+
+ def initPropertyStore(self, props):
+ # Setup peruser special properties
+ props.setSpecialProperties(
+ (
+ PropertyName.fromElement(carddavxml.AddressBookDescription),
+ ),
+ (
+ PropertyName.fromElement(customxml.GETCTag),
+ ),
+ )
+
+ def _doValidate(self, component):
+ component.validForCardDAV()
+
+ def contentType(self):
+ """
+ The content type of Addresbook objects is text/vcard.
+ """
+ return MimeType.fromString("text/vcard; charset=utf-8")
+
+class AddressBookObject(CommonObjectResource):
+
+ implements(IAddressBookObject)
+
+ def __init__(self, name, addressbook, resid):
+
+ super(AddressBookObject, self).__init__(name, addressbook, resid)
+
+ self._objectTable = ADDRESSBOOK_OBJECT_TABLE
+
+ @property
+ def _addressbook(self):
+ return self._parentCollection
+
+ def addressbook(self):
+ return self._addressbook
+
+ def setComponent(self, component, inserting=False):
+ validateAddressBookComponent(self, self._addressbook, component, inserting)
+
+ self.updateDatabase(component, inserting=inserting)
+ if inserting:
+ self._addressbook._insertRevision(self._name)
+ else:
+ self._addressbook._updateRevision(self._name)
+
+ if self._addressbook._notifier:
+ self._addressbook._home._txn.postCommit(self._addressbook._notifier.notify)
+
+ def updateDatabase(self, component, expand_until=None, reCreate=False, inserting=False):
+ """
+ Update the database tables for the new data being written.
+
+ @param component: addressbook data to store
+ @type component: L{Component}
+ """
+
+ componentText = str(component)
+ self._objectText = componentText
+
+ # ADDRESSBOOK_OBJECT table update
+ if inserting:
+ self._resourceID = self._txn.execSQL(
+ """
+ insert into ADDRESSBOOK_OBJECT
+ (ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME, VCARD_TEXT, VCARD_UID)
+ values
+ (%s, %s, %s, %s)
+ returning RESOURCE_ID
+ """,
+ [
+ self._addressbook._resourceID,
+ self._name,
+ componentText,
+ component.resourceUID(),
+ ]
+ )[0][0]
+ else:
+ self._txn.execSQL(
+ """
+ update ADDRESSBOOK_OBJECT set
+ (VCARD_TEXT, VCARD_UID, MODIFIED)
+ =
+ (%s, %s, timezone('UTC', CURRENT_TIMESTAMP))
+ where RESOURCE_ID = %s
+ """,
+ [
+ componentText,
+ component.resourceUID(),
+ self._resourceID
+ ]
+ )
+
+ def component(self):
+ return VCard.fromString(self.vCardText())
+
+ def text(self):
+ if self._objectText is None:
+ text = self._txn.execSQL(
+ "select VCARD_TEXT from ADDRESSBOOK_OBJECT where "
+ "RESOURCE_ID = %s", [self._resourceID]
+ )[0][0]
+ self._objectText = text
+ return text
+ else:
+ return self._objectText
+
+ vCardText = text
+
+ def uid(self):
+ return self.component().resourceUID()
+
+ def name(self):
+ return self._name
+
+ def componentType(self):
+ return self.component().mainType()
+
+ # IDataStoreResource
+ def contentType(self):
+ """
+ The content type of Addressbook objects is text/x-vcard.
+ """
+ return MimeType.fromString("text/vcard; charset=utf-8")
Added: CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/test/test_sql.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/test/test_sql.py (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/test/test_sql.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,75 @@
+##
+# 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.
+##
+
+"""
+Tests for txcaldav.calendarstore.postgres, mostly based on
+L{txcaldav.calendarstore.test.common}.
+"""
+
+from txcarddav.addressbookstore.test.common import CommonTests as AddressBookCommonTests
+
+from txdav.common.datastore.test.util import SQLStoreBuilder
+
+from twisted.trial import unittest
+from twisted.internet.defer import inlineCallbacks
+from twistedcaldav.vcard import Component as VCard
+
+
+theStoreBuilder = SQLStoreBuilder()
+buildStore = theStoreBuilder.buildStore
+
+class AddressBookSQLStorageTests(AddressBookCommonTests, unittest.TestCase):
+ """
+ AddressBook SQL storage tests.
+ """
+
+ @inlineCallbacks
+ def setUp(self):
+ super(AddressBookSQLStorageTests, self).setUp()
+ self.addressbookStore = yield buildStore(self, self.notifierFactory)
+ self.populate()
+
+ def populate(self):
+ populateTxn = self.addressbookStore.newTransaction()
+ for homeUID in self.requirements:
+ addressbooks = self.requirements[homeUID]
+ if addressbooks is not None:
+ home = populateTxn.addressbookHomeWithUID(homeUID, True)
+ # We don't want the default addressbook to appear unless it's
+ # explicitly listed.
+ home.removeAddressBookWithName("addressbook")
+ for addressbookName in addressbooks:
+ addressbookObjNames = addressbooks[addressbookName]
+ if addressbookObjNames is not None:
+ home.createAddressBookWithName(addressbookName)
+ addressbook = home.addressbookWithName(addressbookName)
+ for objectName in addressbookObjNames:
+ objData = addressbookObjNames[objectName]
+ addressbook.createAddressBookObjectWithName(
+ objectName, VCard.fromString(objData)
+ )
+
+ populateTxn.commit()
+ self.notifierFactory.reset()
+
+
+
+ def storeUnderTest(self):
+ """
+ Create and return a L{AddressBookStore} for testing.
+ """
+ return self.addressbookStore
+
Added: CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/util.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/util.py (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txcarddav/addressbookstore/util.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,58 @@
+##
+# 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.
+##
+
+"""
+Utility logic common to multiple backend implementations.
+"""
+
+from twistedcaldav.vcard import Component as VCard
+from twistedcaldav.vcard import InvalidVCardDataError
+
+from txdav.common.icommondatastore import InvalidObjectResourceError,\
+ NoSuchObjectResourceError
+
+def validateAddressBookComponent(addressbookObject, vcard, component, inserting):
+ """
+ Validate an addressbook component for a particular addressbook.
+
+ @param addressbookObject: The addressbook object whose component will be replaced.
+ @type addressbookObject: L{IAddressBookObject}
+
+ @param addressbook: The addressbook which the L{IAddressBookObject} is present in.
+ @type addressbook: L{IAddressBook}
+
+ @param component: The VComponent to be validated.
+ @type component: L{VComponent}
+ """
+
+ if not isinstance(component, VCard):
+ raise TypeError(type(component))
+
+ try:
+ if not inserting and component.resourceUID() != addressbookObject.uid():
+ raise InvalidObjectResourceError(
+ "UID may not change (%s != %s)" % (
+ component.resourceUID(), addressbookObject.uid()
+ )
+ )
+ except NoSuchObjectResourceError:
+ pass
+
+ try:
+ component.validForCardDAV()
+ except InvalidVCardDataError, e:
+ raise InvalidObjectResourceError(e)
+
\ No newline at end of file
Modified: CalendarServer/branches/generic-sqlstore/txdav/common/datastore/file.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/common/datastore/file.py 2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txdav/common/datastore/file.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -39,7 +39,8 @@
from txdav.common.inotifications import INotificationCollection, \
INotificationObject
from txdav.datastore.file import DataStoreTransaction, DataStore, writeOperation, \
- hidden, isValidName, cached, FileMetaDataMixin
+ hidden, isValidName, FileMetaDataMixin
+from txdav.datastore.util import cached
from txdav.idav import IDataStore
from txdav.propertystore.base import PropertyName
from txdav.propertystore.xattr import PropertyStore
@@ -635,7 +636,7 @@
raise ObjectResourceNameAlreadyExistsError(name)
objectResource = self._objectResourceClass(name, self)
- objectResource.setComponent(component)
+ objectResource.setComponent(component, inserting=True)
self._cachedObjectResources[name] = objectResource
# Note: setComponent triggers a notification, so we don't need to
@@ -704,8 +705,8 @@
@param props: the L{PropertyStore} from C{properties()}.
"""
+ pass
-
def _doValidate(self, component):
raise NotImplementedError
@@ -742,7 +743,7 @@
@writeOperation
- def setComponent(self, component):
+ def setComponent(self, component, inserting=False):
raise NotImplementedError
@@ -761,9 +762,19 @@
def properties(self):
uid = self._parentCollection._home.uid()
props = PropertyStore(uid, lambda : self._path)
+ self.initPropertyStore(props)
self._transaction.addOperation(props.flush, "object properties flush")
return props
+ def initPropertyStore(self, props):
+ """
+ A hook for subclasses to override in order to set up their property
+ store after it's been created.
+
+ @param props: the L{PropertyStore} from C{properties()}.
+ """
+ pass
+
class CommonStubResource(object):
"""
Just enough resource to keep the collection sql DB classes going.
@@ -877,7 +888,7 @@
@writeOperation
- def setData(self, uid, xmltype, xmldata):
+ def setData(self, uid, xmltype, xmldata, inserting=False):
rname = uid + ".xml"
self._parentCollection.retrieveOldIndex().addOrUpdateRecord(
Modified: CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql.py 2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -1,4 +1,3 @@
-# -*- test-case-name: txcaldav.calendarstore.test.test_postgres -*-
##
# Copyright (c) 2010 Apple Inc. All rights reserved.
#
@@ -14,10 +13,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
##
-from twext.python.log import Logger
-from txdav.datastore.sql import memoized
-from txcaldav.calendarstore.postgres import PostgresCalendarHome,\
- PostgresAddressBookHome, PostgresNotificationCollection
"""
SQL data store.
@@ -26,20 +21,45 @@
__all__ = [
"CommonDataStore",
"CommonStoreTransaction",
+ "CommonHome",
]
-from twistedcaldav.sharing import SharedCollectionRecord #@UnusedImport
+from twext.python.log import Logger, LoggingMixIn
+from twext.web2.dav.element.rfc2518 import ResourceType
+from twext.web2.http_headers import MimeType
-from zope.interface.declarations import implements, directlyProvides
-
from twisted.application.service import Service
+from twisted.python import hashlib
+from twisted.python.modules import getModule
+from twisted.python.util import FancyEqMixin
-from txdav.idav import IDataStore, AlreadyFinishedError
+from twistedcaldav.customxml import NotificationType
+from txdav.common.datastore.sql_legacy import PostgresLegacyNotificationsEmulator
from txcaldav.icalendarstore import ICalendarTransaction
+
from txcarddav.iaddressbookstore import IAddressBookTransaction
+from txdav.common.datastore.sql_tables import CALENDAR_HOME_TABLE,\
+ ADDRESSBOOK_HOME_TABLE, NOTIFICATION_HOME_TABLE, _BIND_MODE_OWN,\
+ _BIND_STATUS_ACCEPTED
+from txdav.common.icommondatastore import HomeChildNameNotAllowedError,\
+ HomeChildNameAlreadyExistsError, NoSuchHomeChildError,\
+ ObjectResourceNameNotAllowedError, ObjectResourceNameAlreadyExistsError,\
+ NoSuchObjectResourceError
+from txdav.common.inotifications import INotificationCollection,\
+ INotificationObject
+from txdav.datastore.sql import memoized
+from txdav.datastore.util import cached
+from txdav.idav import IDataStore, AlreadyFinishedError
+from txdav.propertystore.base import PropertyName
+from txdav.propertystore.sql import PropertyStore
+from zope.interface.declarations import implements, directlyProvides
+
+v1_schema = getModule(__name__).filePath.sibling(
+ "sql_schema_v1.sql").getContent()
+
log = Logger()
ECALENDARTYPE = 0
@@ -97,8 +117,10 @@
extraInterfaces.append(IAddressBookTransaction)
directlyProvides(self, *extraInterfaces)
- CommonStoreTransaction._homeClass[ECALENDARTYPE] = PostgresCalendarHome
- CommonStoreTransaction._homeClass[EADDRESSBOOKTYPE] = PostgresAddressBookHome
+ from txcaldav.calendarstore.sql import CalendarHome
+ from txcarddav.addressbookstore.sql import AddressBookHome
+ CommonStoreTransaction._homeClass[ECALENDARTYPE] = CalendarHome
+ CommonStoreTransaction._homeClass[EADDRESSBOOKTYPE] = AddressBookHome
def store(self):
return self._store
@@ -136,19 +158,19 @@
def homeWithUID(self, storeType, uid, create=False):
if storeType == ECALENDARTYPE:
- homeTable = "CALENDAR_HOME"
+ homeTable = CALENDAR_HOME_TABLE
elif storeType == EADDRESSBOOKTYPE:
- homeTable = "ADDRESSBOOK_HOME"
+ homeTable = ADDRESSBOOK_HOME_TABLE
data = self.execSQL(
- "select RESOURCE_ID from %s where OWNER_UID = %%s" % (homeTable,),
+ "select %(column_RESOURCE_ID)s from %(name)s where %(column_OWNER_UID)s = %%s" % homeTable,
[uid]
)
if not data:
if not create:
return None
self.execSQL(
- "insert into %s (OWNER_UID) values (%%s)" % (homeTable,),
+ "insert into %(name)s (%(column_OWNER_UID)s) values (%%s)" % homeTable,
[uid]
)
home = self.homeWithUID(storeType, uid)
@@ -171,18 +193,18 @@
"""
rows = self.execSQL(
"""
- select RESOURCE_ID from NOTIFICATION_HOME where
- OWNER_UID = %s
- """, [uid])
+ select %(column_RESOURCE_ID)s from %(name)s where
+ %(column_OWNER_UID)s = %%s
+ """ % NOTIFICATION_HOME_TABLE, [uid]
+ )
if rows:
- [[resourceID]] = rows
+ resourceID = rows[0][0]
else:
- [[resourceID]] = self.execSQL("select nextval('RESOURCE_ID_SEQ')")
- resourceID = str(resourceID)
- self.execSQL(
- "insert into NOTIFICATION_HOME (RESOURCE_ID, OWNER_UID) "
- "values (%s, %s)", [resourceID, uid])
- return PostgresNotificationCollection(self, uid, resourceID)
+ resourceID = str(self.execSQL(
+ "insert into %(name)s (%(column_OWNER_UID)s) values (%%s) returning %(column_RESOURCE_ID)s" % NOTIFICATION_HOME_TABLE,
+ [uid]
+ )[0][0])
+ return NotificationCollection(self, uid, resourceID)
def abort(self):
@@ -214,3 +236,836 @@
self._postCommitOperations.append(operation)
# FIXME: implement.
+class CommonHome(LoggingMixIn):
+
+ _childClass = None
+ _childTable = None
+ _bindTable = None
+
+ def __init__(self, transaction, ownerUID, resourceID, notifier):
+ self._txn = transaction
+ self._ownerUID = ownerUID
+ self._resourceID = resourceID
+ self._shares = None
+ self._children = {}
+ self._notifier = notifier
+
+
+ def __repr__(self):
+ return "<%s: %s>" % (self.__class__.__name__, self._resourceID)
+
+ def uid(self):
+ """
+ Retrieve the unique identifier for this home.
+
+ @return: a string.
+ """
+ return self._ownerUID
+
+
+ def transaction(self):
+ return self._txn
+
+
+ def retrieveOldShares(self):
+ return self._shares
+
+
+ def name(self):
+ """
+ Implement L{IDataStoreResource.name} to return the uid.
+ """
+ return self.uid()
+
+
+ def children(self):
+ """
+ Retrieve children contained in this home.
+ """
+ names = self.listChildren()
+ for name in names:
+ yield self.childWithName(name)
+
+
+ def listChildren(self):
+ """
+ Retrieve the names of the children in this home.
+
+ @return: an iterable of C{str}s.
+ """
+ # FIXME: not specified on the interface or exercised by the tests, but
+ # required by clients of the implementation!
+ rows = self._txn.execSQL(
+ "select %(column_RESOURCE_NAME)s from %(name)s where "
+ "%(column_HOME_RESOURCE_ID)s = %%s "
+ "and %(column_BIND_MODE)s = %%s " % self._bindTable,
+ # Right now, we only show owned calendars.
+ [self._resourceID, _BIND_MODE_OWN]
+ )
+ names = [row[0] for row in rows]
+ return names
+
+
+ @memoized('name', '_children')
+ def childWithName(self, name):
+ """
+ Retrieve the child with the given C{name} contained in this
+ home.
+
+ @param name: a string.
+ @return: an L{ICalendar} or C{None} if no such child
+ exists.
+ """
+ data = self._txn.execSQL(
+ "select %(column_RESOURCE_ID)s from %(name)s where "
+ "%(column_RESOURCE_NAME)s = %%s and %(column_HOME_RESOURCE_ID)s = %%s "
+ "and %(column_BIND_MODE)s = %%s" % self._bindTable,
+ [name, self._resourceID, _BIND_MODE_OWN]
+ )
+ if not data:
+ return None
+ resourceID = data[0][0]
+ if self._notifier:
+ childID = "%s/%s" % (self.uid(), name)
+ notifier = self._notifier.clone(label="collection", id=childID)
+ else:
+ notifier = None
+ return self._childClass(self, name, resourceID, notifier)
+
+
+ def createChildWithName(self, name):
+ if name.startswith("."):
+ raise HomeChildNameNotAllowedError(name)
+
+ rows = self._txn.execSQL(
+ "select %(column_RESOURCE_NAME)s from %(name)s where "
+ "%(column_RESOURCE_NAME)s = %%s AND "
+ "%(column_HOME_RESOURCE_ID)s = %%s" % self._bindTable,
+ [name, self._resourceID]
+ )
+ if rows:
+ raise HomeChildNameAlreadyExistsError()
+
+ rows = self._txn.execSQL("select nextval('RESOURCE_ID_SEQ')")
+ resourceID = rows[0][0]
+ self._txn.execSQL(
+ "insert into %(name)s (%(column_RESOURCE_ID)s) values "
+ "(%%s)" % self._childTable,
+ [resourceID])
+
+ self._txn.execSQL("""
+ insert into %(name)s (
+ %(column_HOME_RESOURCE_ID)s,
+ %(column_RESOURCE_ID)s, %(column_RESOURCE_NAME)s, %(column_BIND_MODE)s,
+ %(column_SEEN_BY_OWNER)s, %(column_SEEN_BY_SHAREE)s, %(column_BIND_STATUS)s) values (
+ %%s, %%s, %%s, %%s, %%s, %%s, %%s)
+ """ % self._bindTable,
+ [self._resourceID, resourceID, name, _BIND_MODE_OWN, True, True,
+ _BIND_STATUS_ACCEPTED]
+ )
+
+ newChild = self.childWithName(name)
+ newChild.properties()[
+ PropertyName.fromElement(ResourceType)] = newChild.resourceType()
+ newChild._updateSyncToken()
+ self.createdChild(newChild)
+
+ if self._notifier:
+ self._txn.postCommit(self._notifier.notify)
+
+
+ def createdChild(self, child):
+ pass
+
+
+ def removeChildWithName(self, name):
+ rows = self._txn.execSQL(
+ """select %(column_RESOURCE_ID)s from %(name)s
+ where %(column_RESOURCE_NAME)s = %%s and %(column_HOME_RESOURCE_ID)s = %%s""" % self._bindTable,
+ [name, self._resourceID]
+ )
+ if not rows:
+ raise NoSuchHomeChildError()
+ resourceID = rows[0][0]
+
+ self._txn.execSQL(
+ "delete from %(name)s where %(column_RESOURCE_ID)s = %%s" % self._childTable,
+ [resourceID]
+ )
+ self._children.pop(name, None)
+ if self._txn._cursor.rowcount == 0:
+ raise NoSuchHomeChildError()
+ if self._notifier:
+ self._txn.postCommit(self._notifier.notify)
+
+
+ @cached
+ def properties(self):
+ return PropertyStore(
+ self.uid(),
+ self._txn,
+ self._resourceID
+ )
+
+
+ # IDataStoreResource
+ def contentType(self):
+ """
+ The content type of objects
+ """
+ return None
+
+
+ def md5(self):
+ return None
+
+
+ def size(self):
+ return 0
+
+
+ def created(self):
+ return None
+
+
+ def modified(self):
+ return None
+
+
+ def notifierID(self, label="default"):
+ if self._notifier:
+ return self._notifier.getID(label)
+ else:
+ return None
+
+class CommonHomeChild(LoggingMixIn, FancyEqMixin):
+ """
+ Common ancestor class of AddressBooks and Calendars.
+ """
+
+ compareAttributes = '_name _home _resourceID'.split()
+
+ _objectResourceClass = None
+ _bindTable = None
+ _homeChildTable = None
+ _revisionsTable = None
+ _objectTable = None
+
+ def __init__(self, home, name, resourceID, notifier):
+ self._home = home
+ self._name = name
+ self._resourceID = resourceID
+ self._objects = {}
+ self._notifier = notifier
+
+ self._index = None # Derived classes need to set this
+ self._invites = None # Derived classes need to set this
+
+
+ @property
+ def _txn(self):
+ return self._home._txn
+
+
+ def resourceType(self):
+ return NotImplementedError
+
+
+ def retrieveOldIndex(self):
+ return self._index
+
+
+ def retrieveOldInvites(self):
+ return self._invites
+
+ def __repr__(self):
+ return "<%s: %s>" % (self.__class__.__name__, self._resourceID)
+
+ def name(self):
+ return self._name
+
+
+ def rename(self, name):
+ oldName = self._name
+ self._txn.execSQL(
+ "update %(name)s set %(column_RESOURCE_NAME)s = %%s "
+ "where %(column_RESOURCE_ID)s = %%s AND "
+ "%(column_HOME_RESOURCE_ID)s = %%s" % self._bindTable,
+ [name, self._resourceID, self._home._resourceID]
+ )
+ self._name = name
+ # update memos
+ del self._home._children[oldName]
+ self._home._children[name] = self
+ self._updateSyncToken()
+
+ if self._notifier:
+ self._txn.postCommit(self._notifier.notify)
+
+
+ def ownerHome(self):
+ return self._home
+
+
+ def setSharingUID(self, uid):
+ self.properties()._setPerUserUID(uid)
+
+
+ def objectResources(self):
+ for name in self.listObjectResources():
+ yield self.objectResourceWithName(name)
+
+
+ def listObjectResources(self):
+ rows = self._txn.execSQL(
+ "select %(column_RESOURCE_NAME)s from %(name)s "
+ "where %(column_PARENT_RESOURCE_ID)s = %%s" % self._objectTable,
+ [self._resourceID])
+ return sorted([row[0] for row in rows])
+
+
+ @memoized('name', '_objects')
+ def objectResourceWithName(self, name):
+ rows = self._txn.execSQL(
+ "select %(column_RESOURCE_ID)s from %(name)s "
+ "where %(column_RESOURCE_NAME)s = %%s and %(column_PARENT_RESOURCE_ID)s = %%s" % self._objectTable,
+ [name, self._resourceID]
+ )
+ if not rows:
+ return None
+ resid = rows[0][0]
+ return self._objectResourceClass(name, self, resid)
+
+
+ @memoized('uid', '_objects')
+ def objectResourceWithUID(self, uid):
+ rows = self._txn.execSQL(
+ "select %(column_RESOURCE_ID)s, %(column_RESOURCE_NAME)s from %(name)s "
+ "where %(column_UID)s = %%s and %(column_PARENT_RESOURCE_ID)s = %%s" % self._objectTable,
+ [uid, self._resourceID]
+ )
+ if not rows:
+ return None
+ resid = rows[0][0]
+ name = rows[0][1]
+ return self._objectResourceClass(name, self, resid)
+
+
+ def createObjectResourceWithName(self, name, component):
+ if name.startswith("."):
+ raise ObjectResourceNameNotAllowedError(name)
+
+ rows = self._txn.execSQL(
+ "select %(column_RESOURCE_ID)s from %(name)s "
+ "where %(column_RESOURCE_NAME)s = %%s and %(column_PARENT_RESOURCE_ID)s = %%s" % self._objectTable,
+ [name, self._resourceID]
+ )
+ if rows:
+ raise ObjectResourceNameAlreadyExistsError()
+
+ objectResource = self._objectResourceClass(name, self, None)
+ objectResource.setComponent(component, inserting=True)
+
+ # Note: setComponent triggers a notification, so we don't need to
+ # call notify( ) here like we do for object removal.
+
+
+ def removeObjectResourceWithName(self, name):
+ rows = self._txn.execSQL(
+ "delete from %(name)s "
+ "where %(column_RESOURCE_NAME)s = %%s and %(column_PARENT_RESOURCE_ID)s = %%s "
+ "returning %(column_UID)s" % self._objectTable,
+ [name, self._resourceID],
+ raiseOnZeroRowCount=lambda:NoSuchObjectResourceError()
+ )
+ uid = rows[0][0]
+ self._objects.pop(name, None)
+ self._objects.pop(uid, None)
+ self._deleteRevision(name)
+
+ if self._notifier:
+ self._txn.postCommit(self._notifier.notify)
+
+
+ def removeObjectResourceWithUID(self, uid):
+ rows = self._txn.execSQL(
+ "delete from %(name)s "
+ "where %(column_UID)s = %%s and %(column_PARENT_RESOURCE_ID)s = %%s "
+ "returning %(column_RESOURCE_NAME)s" % self._objectTable,
+ [uid, self._resourceID],
+ raiseOnZeroRowCount=lambda:NoSuchObjectResourceError()
+ )
+ name = rows[0][0]
+ self._objects.pop(name, None)
+ self._objects.pop(uid, None)
+ self._deleteRevision(name)
+
+ if self._notifier:
+ self._txn.postCommit(self._notifier.notify)
+
+
+ def syncToken(self):
+ revision = self._txn.execSQL(
+ "select %(column_REVISION)s from %(name)s where %(column_RESOURCE_ID)s = %%s" % self._homeChildTable,
+ [self._resourceID])[0][0]
+ return "%s#%s" % (self._resourceID, revision,)
+
+ def objectResourcesSinceToken(self, token):
+ raise NotImplementedError()
+
+
+ def _updateSyncToken(self):
+
+ self._txn.execSQL("""
+ update %(name)s
+ set (%(column_REVISION)s) = (nextval('%(sequence)s'))
+ where %(column_RESOURCE_ID)s = %%s
+ """ % self._homeChildTable,
+ [self._resourceID]
+ )
+
+ def _insertRevision(self, name):
+ self._changeRevision("insert", name)
+
+ def _updateRevision(self, name):
+ self._changeRevision("update", name)
+
+ def _deleteRevision(self, name):
+ self._changeRevision("delete", name)
+
+ def _changeRevision(self, action, name):
+
+ nextrevision = self._txn.execSQL("""
+ select nextval('%(sequence)s')
+ """ % self._homeChildTable
+ )
+
+ if action == "delete":
+ self._txn.execSQL("""
+ update %(name)s
+ set (%(column_REVISION)s, %(column_DELETED)s) = (%%s, TRUE)
+ where %(column_RESOURCE_ID)s = %%s and %(column_RESOURCE_NAME)s = %%s
+ """ % self._revisionsTable,
+ [nextrevision, self._resourceID, name]
+ )
+ self._txn.execSQL("""
+ update %(name)s
+ set (%(column_REVISION)s) = (%%s)
+ where %(column_RESOURCE_ID)s = %%s
+ """ % self._homeChildTable,
+ [nextrevision, self._resourceID]
+ )
+ elif action == "update":
+ self._txn.execSQL("""
+ update %(name)s
+ set (%(column_REVISION)s) = (%%s)
+ where %(column_RESOURCE_ID)s = %%s and %(column_RESOURCE_NAME)s = %%s
+ """ % self._revisionsTable,
+ [nextrevision, self._resourceID, name]
+ )
+ self._txn.execSQL("""
+ update %(name)s
+ set (%(column_REVISION)s) = (%%s)
+ where %(column_RESOURCE_ID)s = %%s
+ """ % self._homeChildTable,
+ [nextrevision, self._resourceID]
+ )
+ elif action == "insert":
+ self._txn.execSQL("""
+ delete from %(name)s
+ where %(column_RESOURCE_ID)s = %%s and %(column_RESOURCE_NAME)s = %%s
+ """ % self._revisionsTable,
+ [self._resourceID, name,]
+ )
+ self._txn.execSQL("""
+ insert into %(name)s
+ (%(column_RESOURCE_ID)s, %(column_RESOURCE_NAME)s, %(column_REVISION)s, %(column_DELETED)s)
+ values (%%s, %%s, %%s, FALSE)
+ """ % self._revisionsTable,
+ [self._resourceID, name, nextrevision]
+ )
+ self._txn.execSQL("""
+ update %(name)s
+ set (%(column_REVISION)s) = (%%s)
+ where %(column_RESOURCE_ID)s = %%s
+ """ % self._homeChildTable,
+ [nextrevision, self._resourceID]
+ )
+ @cached
+ def properties(self):
+ props = PropertyStore(
+ self.ownerHome().uid(),
+ self._txn,
+ self._resourceID
+ )
+ self.initPropertyStore(props)
+ return props
+
+ def initPropertyStore(self, props):
+ """
+ A hook for subclasses to override in order to set up their property
+ store after it's been created.
+
+ @param props: the L{PropertyStore} from C{properties()}.
+ """
+ pass
+
+ def _doValidate(self, component):
+ raise NotImplementedError
+
+ def notifierID(self, label="default"):
+ if self._notifier:
+ return self._notifier.getID(label)
+ else:
+ return None
+
+
+
+ # IDataStoreResource
+ def contentType(self):
+ raise NotImplementedError()
+
+
+ def md5(self):
+ return None
+
+
+ def size(self):
+ return 0
+
+
+ def created(self):
+ created = self._txn.execSQL(
+ "select extract(EPOCH from %(column_CREATED)s) from %(name)s "
+ "where %(column_RESOURCE_ID)s = %%s" % self._homeChildTable,
+ [self._resourceID]
+ )[0][0]
+ return int(created)
+
+ def modified(self):
+ modified = self._txn.execSQL(
+ "select extract(EPOCH from %(column_MODIFIED)s) from %(name)s "
+ "where %(column_RESOURCE_ID)s = %%s" % self._homeChildTable,
+ [self._resourceID]
+ )[0][0]
+ return int(modified)
+
+class CommonObjectResource(LoggingMixIn, FancyEqMixin):
+ """
+ @ivar _path: The path of the file on disk
+
+ @type _path: L{FilePath}
+ """
+
+ compareAttributes = '_name _parentCollection'.split()
+
+ _objectTable = None
+
+ def __init__(self, name, parent, resid):
+ self._name = name
+ self._parentCollection = parent
+ self._resourceID = resid
+ self._objectText = None
+
+ def __repr__(self):
+ return "<%s: %s>" % (self.__class__.__name__, self._resourceID)
+
+ @property
+ def _txn(self):
+ return self._parentCollection._txn
+
+ def setComponent(self, component, inserting=False):
+ raise NotImplementedError
+
+
+ def component(self):
+ raise NotImplementedError
+
+
+ def text(self):
+ raise NotImplementedError
+
+
+ def uid(self):
+ raise NotImplementedError
+
+ @cached
+ def properties(self):
+ props = PropertyStore(
+ self.uid(),
+ self._txn,
+ self._resourceID
+ )
+ self.initPropertyStore(props)
+ return props
+
+ def initPropertyStore(self, props):
+ """
+ A hook for subclasses to override in order to set up their property
+ store after it's been created.
+
+ @param props: the L{PropertyStore} from C{properties()}.
+ """
+ pass
+
+ # IDataStoreResource
+ def contentType(self):
+ raise NotImplementedError()
+
+ def md5(self):
+ return None
+
+ def size(self):
+ size = self._txn.execSQL(
+ "select character_length(%(column_TEXT)s) from %(name)s "
+ "where %(column_RESOURCE_ID)s = %%s" % self._objectTable,
+ [self._resourceID]
+ )[0][0]
+ return size
+
+
+ def created(self):
+ created = self._txn.execSQL(
+ "select extract(EPOCH from %(column_CREATED)s) from %(name)s "
+ "where %(column_RESOURCE_ID)s = %%s" % self._objectTable,
+ [self._resourceID]
+ )[0][0]
+ return int(created)
+
+ def modified(self):
+ modified = self._txn.execSQL(
+ "select extract(EPOCH from %(column_MODIFIED)s) from %(name)s "
+ "where %(column_RESOURCE_ID)s = %%s" % self._objectTable,
+ [self._resourceID]
+ )[0][0]
+ return int(modified)
+
+class NotificationCollection(LoggingMixIn, FancyEqMixin):
+
+ implements(INotificationCollection)
+
+ compareAttributes = '_uid _resourceID'.split()
+
+ _objectResourceClass = None
+ _bindTable = None
+ _homeChildTable = None
+ _revisionsTable = None
+ _objectTable = None
+
+ def __init__(self, txn, uid, resourceID):
+
+ self._txn = txn
+ self._uid = uid
+ self._resourceID = resourceID
+ self._notifications = {}
+
+
+ def resourceType(self):
+ return ResourceType.notification #@UndefinedVariable
+
+ def retrieveOldIndex(self):
+ return PostgresLegacyNotificationsEmulator(self)
+
+ def __repr__(self):
+ return "<%s: %s>" % (self.__class__.__name__, self._resourceID)
+
+ def name(self):
+ return 'notification'
+
+ def uid(self):
+ return self._uid
+
+ def notificationObjects(self):
+ for name in self.listNotificationObjects():
+ yield self.notificationObjectWithName(name)
+
+ def listNotificationObjects(self):
+ rows = self._txn.execSQL(
+ "select (NOTIFICATION_UID) from NOTIFICATION "
+ "where NOTIFICATION_HOME_RESOURCE_ID = %s",
+ [self._resourceID])
+ return sorted(["%s.xml" % row[0] for row in rows])
+
+ def _nameToUID(self, name):
+ """
+ Based on the file-backed implementation, the 'name' is just uid +
+ ".xml".
+ """
+ return name.rsplit(".", 1)[0]
+
+
+ def notificationObjectWithName(self, name):
+ return self.notificationObjectWithUID(self._nameToUID(name))
+
+ @memoized('uid', '_notifications')
+ def notificationObjectWithUID(self, uid):
+ rows = self._txn.execSQL(
+ "select RESOURCE_ID from NOTIFICATION "
+ "where NOTIFICATION_UID = %s and NOTIFICATION_HOME_RESOURCE_ID = %s",
+ [uid, self._resourceID])
+ if rows:
+ resourceID = rows[0][0]
+ return NotificationObject(self, resourceID)
+ else:
+ return None
+
+
+ def writeNotificationObject(self, uid, xmltype, xmldata):
+
+ inserting = False
+ notificationObject = self.notificationObjectWithUID(uid)
+ if notificationObject is None:
+ notificationObject = NotificationObject(self, None)
+ inserting = True
+ notificationObject.setData(uid, xmltype, xmldata, inserting=inserting)
+
+
+ def removeNotificationObjectWithName(self, name):
+ self.removeNotificationObjectWithUID(self._nameToUID(name))
+
+
+ def removeNotificationObjectWithUID(self, uid):
+ self._txn.execSQL(
+ "delete from NOTIFICATION "
+ "where NOTIFICATION_UID = %s and NOTIFICATION_HOME_RESOURCE_ID = %s",
+ [uid, self._resourceID]
+ )
+ self._notifications.pop(uid, None)
+
+
+ def syncToken(self):
+ return 'dummy-sync-token'
+
+
+ def notificationObjectsSinceToken(self, token):
+ changed = []
+ removed = []
+ token = self.syncToken()
+ return (changed, removed, token)
+
+
+ @cached
+ def properties(self):
+ return PropertyStore(
+ self._uid,
+ self._txn,
+ self._resourceID
+ )
+
+class NotificationObject(LoggingMixIn, FancyEqMixin):
+ implements(INotificationObject)
+
+ compareAttributes = '_resourceID _home'.split()
+
+ def __init__(self, home, resourceID):
+ self._home = home
+ self._resourceID = resourceID
+
+
+ def __repr__(self):
+ return "<%s: %s>" % (self.__class__.__name__, self._resourceID)
+
+
+ @property
+ def _txn(self):
+ return self._home._txn
+
+
+ def notificationCollection(self):
+ return self._home
+
+
+ def name(self):
+ return self.uid() + ".xml"
+
+
+ def setData(self, uid, xmltype, xmldata, inserting=False):
+
+ xmltypeString = xmltype.toxml()
+ if inserting:
+ rows = self._txn.execSQL(
+ "insert into NOTIFICATION (NOTIFICATION_HOME_RESOURCE_ID, NOTIFICATION_UID, XML_TYPE, XML_DATA) "
+ "values (%s, %s, %s, %s) returning RESOURCE_ID",
+ [self._home._resourceID, uid, xmltypeString, xmldata]
+ )
+ self._resourceID = rows[0][0]
+ else:
+ self._txn.execSQL(
+ "update NOTIFICATION set XML_TYPE = %s, XML_DATA = %s "
+ "where NOTIFICATION_HOME_RESOURCE_ID = %s and NOTIFICATION_UID = %s",
+ [xmltypeString, xmldata, self._home._resourceID, uid])
+
+ self.properties()[PropertyName.fromElement(NotificationType)] = NotificationType(xmltype)
+
+
+ def _fieldQuery(self, field):
+ data = self._txn.execSQL(
+ "select " + field + " from NOTIFICATION "
+ "where RESOURCE_ID = %s",
+ [self._resourceID]
+ )
+ return data[0][0]
+
+
+ def xmldata(self):
+ return self._fieldQuery("XML_DATA")
+
+
+ def uid(self):
+ return self._fieldQuery("NOTIFICATION_UID")
+
+
+ @cached
+ def properties(self):
+ props = PropertyStore(
+ self._home.uid(),
+ self._txn,
+ self._resourceID
+ )
+ self.initPropertyStore(props)
+ return props
+
+ def initPropertyStore(self, props):
+ # Setup peruser special properties
+ props.setSpecialProperties(
+ (
+ ),
+ (
+ PropertyName.fromElement(NotificationType),
+ ),
+ )
+
+ def contentType(self):
+ """
+ The content type of NotificationObjects is text/xml.
+ """
+ return MimeType.fromString("text/xml")
+
+
+ def md5(self):
+ return hashlib.md5(self.xmldata()).hexdigest()
+
+
+ def size(self):
+ size = self._txn.execSQL(
+ "select character_length(XML_DATA) from NOTIFICATION "
+ "where RESOURCE_ID = %s",
+ [self._resourceID]
+ )[0][0]
+ return size
+
+
+ def created(self):
+ modified = self._txn.execSQL(
+ "select extract(EPOCH from CREATED) from NOTIFICATION "
+ "where RESOURCE_ID = %s",
+ [self._resourceID]
+ )[0][0]
+ return int(modified)
+
+ def modified(self):
+ modified = self._txn.execSQL(
+ "select extract(EPOCH from MODIFIED) from NOTIFICATION "
+ "where RESOURCE_ID = %s", [self._resourceID]
+ )[0][0]
+ return int(modified)
Added: CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_legacy.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_legacy.py (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_legacy.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,1325 @@
+# -*- test-case-name: txcaldav.calendarstore.test.test_postgres -*-
+##
+# 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.
+##
+
+
+"""
+PostgreSQL data store.
+"""
+
+import datetime
+import StringIO
+
+from twistedcaldav.sharing import SharedCollectionRecord
+
+from twisted.python import hashlib
+from twisted.internet.defer import succeed
+
+from twext.python.log import Logger, LoggingMixIn
+
+from twistedcaldav import carddavxml
+from twistedcaldav.config import config
+from twistedcaldav.dateops import normalizeForIndex
+from twistedcaldav.index import IndexedSearchException, ReservationError,\
+ SyncTokenValidException
+from twistedcaldav.memcachepool import CachePoolUserMixIn
+from twistedcaldav.notifications import NotificationRecord
+from twistedcaldav.query import calendarqueryfilter, calendarquery, \
+ addressbookquery
+from twistedcaldav.query.sqlgenerator import sqlgenerator
+from twistedcaldav.sharing import Invite
+
+from txdav.common.datastore.sql_tables import \
+ _BIND_MODE_OWN, _BIND_MODE_READ, _BIND_MODE_WRITE, _BIND_STATUS_INVITED,\
+ _BIND_STATUS_ACCEPTED, _BIND_STATUS_DECLINED, _BIND_STATUS_INVALID
+
+log = Logger()
+
+indexfbtype_to_icalfbtype = {
+ 0: '?',
+ 1: 'F',
+ 2: 'B',
+ 3: 'U',
+ 4: 'T',
+}
+
+class PostgresLegacyInvitesEmulator(object):
+ """
+ Emulator for the implicit interface specified by
+ L{twistedcaldav.sharing.InvitesDatabase}.
+ """
+
+
+ def __init__(self, calendar):
+ self._calendar = calendar
+
+
+ @property
+ def _txn(self):
+ return self._calendar._txn
+
+
+ def create(self):
+ "No-op, because the index implicitly always exists in the database."
+
+
+ def remove(self):
+ "No-op, because the index implicitly always exists in the database."
+
+
+ def allRecords(self):
+ for row in self._txn.execSQL(
+ """
+ select
+ INVITE.INVITE_UID, INVITE.NAME, INVITE.RECIPIENT_ADDRESS,
+ CALENDAR_HOME.OWNER_UID, CALENDAR_BIND.BIND_MODE,
+ CALENDAR_BIND.BIND_STATUS, CALENDAR_BIND.MESSAGE
+ from
+ INVITE, CALENDAR_HOME, CALENDAR_BIND
+ where
+ INVITE.RESOURCE_ID = %s and
+ INVITE.HOME_RESOURCE_ID =
+ CALENDAR_HOME.RESOURCE_ID and
+ CALENDAR_BIND.CALENDAR_RESOURCE_ID =
+ INVITE.RESOURCE_ID and
+ CALENDAR_BIND.CALENDAR_HOME_RESOURCE_ID =
+ INVITE.HOME_RESOURCE_ID
+ order by
+ INVITE.NAME asc
+ """, [self._calendar._resourceID]):
+ [inviteuid, common_name, userid, ownerUID,
+ bindMode, bindStatus, summary] = row
+ # FIXME: this is really the responsibility of the protocol layer.
+ state = {
+ _BIND_STATUS_INVITED: "NEEDS-ACTION",
+ _BIND_STATUS_ACCEPTED: "ACCEPTED",
+ _BIND_STATUS_DECLINED: "DECLINED",
+ _BIND_STATUS_INVALID: "INVALID",
+ }[bindStatus]
+ access = {
+ _BIND_MODE_READ: "read-only",
+ _BIND_MODE_WRITE: "read-write"
+ }[bindMode]
+ principalURL = "/principals/__uids__/%s/" % (ownerUID,)
+ yield Invite(
+ inviteuid, userid, principalURL, common_name,
+ access, state, summary
+ )
+
+
+ def recordForUserID(self, userid):
+ for record in self.allRecords():
+ if record.userid == userid:
+ return record
+
+
+ def recordForPrincipalURL(self, principalURL):
+ for record in self.allRecords():
+ if record.principalURL == principalURL:
+ return record
+
+
+ def recordForInviteUID(self, inviteUID):
+ for record in self.allRecords():
+ if record.inviteuid == inviteUID:
+ return record
+
+
+ def addOrUpdateRecord(self, record):
+ bindMode = {'read-only': _BIND_MODE_READ,
+ 'read-write': _BIND_MODE_WRITE}[record.access]
+ bindStatus = {
+ "NEEDS-ACTION": _BIND_STATUS_INVITED,
+ "ACCEPTED": _BIND_STATUS_ACCEPTED,
+ "DECLINED": _BIND_STATUS_DECLINED,
+ "INVALID": _BIND_STATUS_INVALID,
+ }[record.state]
+ # principalURL is derived from a directory record's principalURL() so
+ # it will always contain the UID. The form is '/principals/__uids__/x'
+ # (and may contain a trailing slash).
+ principalUID = record.principalURL.split("/")[3]
+ shareeHome = self._txn.calendarHomeWithUID(principalUID, create=True)
+ rows = self._txn.execSQL(
+ "select RESOURCE_ID, HOME_RESOURCE_ID from INVITE where RECIPIENT_ADDRESS = %s",
+ [record.userid]
+ )
+ if rows:
+ [[resourceID, homeResourceID]] = rows
+ # Invite(inviteuid, userid, principalURL, common_name, access, state, summary)
+ self._txn.execSQL("""
+ update CALENDAR_BIND set BIND_MODE = %s,
+ BIND_STATUS = %s, MESSAGE = %s
+ where
+ CALENDAR_RESOURCE_ID = %s and
+ CALENDAR_HOME_RESOURCE_ID = %s
+ """, [bindMode, bindStatus, record.summary,
+ resourceID, homeResourceID])
+ self._txn.execSQL("""
+ update INVITE set NAME = %s, INVITE_UID = %s
+ where RECIPIENT_ADDRESS = %s
+ """,
+ [record.name, record.inviteuid, record.userid]
+ )
+ else:
+ self._txn.execSQL(
+ """
+ insert into INVITE (
+ INVITE_UID, NAME,
+ HOME_RESOURCE_ID, RESOURCE_ID,
+ RECIPIENT_ADDRESS
+ )
+ values (%s, %s, %s, %s, %s)
+ """,
+ [
+ record.inviteuid, record.name,
+ shareeHome._resourceID, self._calendar._resourceID,
+ record.userid
+ ])
+ self._txn.execSQL(
+ """
+ insert into CALENDAR_BIND
+ (
+ CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID,
+ CALENDAR_RESOURCE_NAME, BIND_MODE, BIND_STATUS,
+ SEEN_BY_OWNER, SEEN_BY_SHAREE, MESSAGE
+ )
+ values (%s, %s, %s, %s, %s, %s, %s, %s)
+ """,
+ [
+ shareeHome._resourceID,
+ self._calendar._resourceID,
+ None, # this is NULL because it is not bound yet, let's be
+ # explicit about that.
+ bindMode,
+ bindStatus,
+ False,
+ False,
+ record.summary
+ ])
+
+
+ def removeRecordForUserID(self, userid):
+ rec = self.recordForUserID(userid)
+ self.removeRecordForInviteUID(rec.inviteuid)
+
+
+ def removeRecordForPrincipalURL(self, principalURL):
+ raise NotImplementedError("removeRecordForPrincipalURL")
+
+
+ def removeRecordForInviteUID(self, inviteUID):
+ rows = self._txn.execSQL("""
+ select HOME_RESOURCE_ID, RESOURCE_ID from INVITE where
+ INVITE_UID = %s
+ """, [inviteUID])
+ if rows:
+ [[homeID, resourceID]] = rows
+ self._txn.execSQL(
+ "delete from CALENDAR_BIND where "
+ "CALENDAR_HOME_RESOURCE_ID = %s and CALENDAR_RESOURCE_ID = %s",
+ [homeID, resourceID])
+ self._txn.execSQL("delete from INVITE where INVITE_UID = %s",
+ [inviteUID])
+
+
+
+class PostgresLegacySharesEmulator(object):
+
+ def __init__(self, home):
+ self._home = home
+
+
+ @property
+ def _txn(self):
+ return self._home._txn
+
+
+ def create(self):
+ pass
+
+
+ def remove(self):
+ pass
+
+
+ def allRecords(self):
+ # This should have been a smart join that got all these columns at
+ # once, but let's not bother to fix it, since the actual query we
+ # _want_ to do (just look for calendar binds in a particular homes) is
+ # much simpler anyway; we should just do that.
+ shareRows = self._txn.execSQL(
+ """
+ select CALENDAR_RESOURCE_ID, CALENDAR_RESOURCE_NAME, MESSAGE
+ from CALENDAR_BIND
+ where CALENDAR_HOME_RESOURCE_ID = %s and
+ BIND_MODE != %s and
+ CALENDAR_RESOURCE_NAME is not null
+ """, [self._home._resourceID, _BIND_MODE_OWN])
+ for resourceID, resourceName, summary in shareRows:
+ [[shareuid]] = self._txn.execSQL(
+ """
+ select INVITE_UID
+ from INVITE
+ where RESOURCE_ID = %s and HOME_RESOURCE_ID = %s
+ """, [resourceID, self._home._resourceID])
+ sharetype = 'I'
+ [[ownerHomeID, ownerResourceName]] = self._txn.execSQL(
+ """
+ select CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_NAME
+ from CALENDAR_BIND
+ where CALENDAR_RESOURCE_ID = %s and
+ BIND_MODE = %s
+ """, [resourceID, _BIND_MODE_OWN]
+ )
+ [[ownerUID]] = self._txn.execSQL(
+ "select OWNER_UID from CALENDAR_HOME where RESOURCE_ID = %s",
+ [ownerHomeID])
+ hosturl = '/calendars/__uids__/%s/%s' % (
+ ownerUID, ownerResourceName
+ )
+ localname = resourceName
+ record = SharedCollectionRecord(
+ shareuid, sharetype, hosturl, localname, summary
+ )
+ yield record
+
+
+ def _search(self, **kw):
+ [[key, value]] = kw.items()
+ for record in self.allRecords():
+ if getattr(record, key) == value:
+ return record
+
+ def recordForLocalName(self, localname):
+ return self._search(localname=localname)
+
+ def recordForShareUID(self, shareUID):
+ return self._search(shareuid=shareUID)
+
+
+ def addOrUpdateRecord(self, record):
+# print '*** SHARING***: Adding or updating this record:'
+# import pprint
+# pprint.pprint(record.__dict__)
+ # record.hosturl -> /calendars/__uids__/<uid>/<calendarname>
+ splithost = record.hosturl.split('/')
+ ownerUID = splithost[3]
+ ownerCalendarName = splithost[4]
+ ownerHome = self._txn.calendarHomeWithUID(ownerUID)
+ ownerCalendar = ownerHome.calendarWithName(ownerCalendarName)
+ calendarResourceID = ownerCalendar._resourceID
+
+ # There needs to be a bind already, one that corresponds to the
+ # invitation. The invitation's UID is the same as the share UID. I
+ # just need to update its 'localname', i.e.
+ # CALENDAR_BIND.CALENDAR_RESOURCE_NAME.
+
+ self._txn.execSQL(
+ """
+ update CALENDAR_BIND set CALENDAR_RESOURCE_NAME = %s
+ where CALENDAR_HOME_RESOURCE_ID = %s and CALENDAR_RESOURCE_ID = %s
+ """,
+ [record.localname, self._home._resourceID, calendarResourceID]
+ )
+
+
+ def removeRecordForLocalName(self, localname):
+ self._txn.execSQL(
+ "delete from CALENDAR_BIND where CALENDAR_RESOURCE_NAME = %s "
+ "and CALENDAR_HOME_RESOURCE_ID = %s",
+ [localname, self._home._resourceID]
+ )
+
+
+ def removeRecordForShareUID(self, shareUID):
+ pass
+# c = self._home._cursor()
+# c.execute(
+# "delete from CALENDAR_BIND where CALENDAR_RESOURCE_NAME = %s "
+# "and CALENDAR_HOME_RESOURCE_ID = %s",
+# [self._home._resourceID]
+# )
+
+
+
+class postgresqlgenerator(sqlgenerator):
+ """
+ Query generator for postgreSQL indexed searches. (Currently unused: work
+ in progress.)
+ """
+
+ ISOP = " = "
+ CONTAINSOP = " LIKE "
+ NOTCONTAINSOP = " NOT LIKE "
+ FIELDS = {
+ "TYPE": "CALENDAR_OBJECT.ICALENDAR_TYPE",
+ "UID": "CALENDAR_OBJECT.ICALENDAR_UID",
+ }
+
+ def __init__(self, expr, calendarid, userid):
+ self.RESOURCEDB = "CALENDAR_OBJECT"
+ self.TIMESPANDB = "TIME_RANGE"
+ self.TIMESPANTEST = "((TIME_RANGE.FLOATING = FALSE AND TIME_RANGE.START_DATE < %s AND TIME_RANGE.END_DATE > %s) OR (TIME_RANGE.FLOATING = TRUE AND TIME_RANGE.START_DATE < %s AND TIME_RANGE.END_DATE > %s))"
+ self.TIMESPANTEST_NOEND = "((TIME_RANGE.FLOATING = FALSE AND TIME_RANGE.END_DATE > %s) OR (TIME_RANGE.FLOATING = TRUE AND TIME_RANGE.END_DATE > %s))"
+ self.TIMESPANTEST_NOSTART = "((TIME_RANGE.FLOATING = FALSE AND TIME_RANGE.START_DATE < %s) OR (TIME_RANGE.FLOATING = TRUE AND TIME_RANGE.START_DATE < %s))"
+ self.TIMESPANTEST_TAIL_PIECE = " AND TIME_RANGE.CALENDAR_OBJECT_RESOURCE_ID = CALENDAR_OBJECT.RESOURCE_ID AND CALENDAR_OBJECT.CALENDAR_RESOURCE_ID = %s"
+ self.TIMESPANTEST_JOIN_ON_PIECE = "TIME_RANGE.INSTANCE_ID = TRANSPARENCY.TIME_RANGE_INSTANCE_ID AND TRANSPARENCY.USER_ID = %s"
+
+ super(postgresqlgenerator, self).__init__(expr, calendarid, userid)
+
+
+ def generate(self):
+ """
+ Generate the actual SQL 'where ...' expression from the passed in
+ expression tree.
+
+ @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the
+ partial SQL statement, and the C{list} is the list of argument
+ substitutions to use with the SQL API execute method.
+ """
+
+ # Init state
+ self.sout = StringIO.StringIO()
+ self.arguments = []
+ self.substitutions = []
+ self.usedtimespan = False
+
+ # Generate ' where ...' partial statement
+ self.sout.write(self.WHERE)
+ self.generateExpression(self.expression)
+
+ # Prefix with ' from ...' partial statement
+ select = self.FROM + self.RESOURCEDB
+ if self.usedtimespan:
+ self.frontArgument(self.userid)
+ select += ", %s LEFT OUTER JOIN %s ON (%s)" % (
+ self.TIMESPANDB,
+ self.TRANSPARENCYDB,
+ self.TIMESPANTEST_JOIN_ON_PIECE
+ )
+ select += self.sout.getvalue()
+
+ select = select % tuple(self.substitutions)
+
+ return select, self.arguments
+
+
+ def addArgument(self, arg):
+ self.arguments.append(arg)
+ self.substitutions.append("%s")
+ self.sout.write("%s")
+
+ def setArgument(self, arg):
+ self.arguments.append(arg)
+ self.substitutions.append("%s")
+
+ def frontArgument(self, arg):
+ self.arguments.insert(0, arg)
+ self.substitutions.insert(0, "%s")
+
+ def containsArgument(self, arg):
+ return "%%%s%%" % (arg,)
+
+
+class MemcachedUIDReserver(CachePoolUserMixIn, LoggingMixIn):
+ def __init__(self, index, cachePool=None):
+ self.index = index
+ self._cachePool = cachePool
+
+ def _key(self, uid):
+ return 'reservation:%s' % (
+ hashlib.md5('%s:%s' % (uid,
+ self.index.resource._resourceID)).hexdigest())
+
+ def reserveUID(self, uid):
+ uid = uid.encode('utf-8')
+ self.log_debug("Reserving UID %r @ %r" % (
+ uid,
+ self.index.resource))
+
+ def _handleFalse(result):
+ if result is False:
+ raise ReservationError(
+ "UID %s already reserved for calendar collection %s."
+ % (uid, self.index.resource._name)
+ )
+
+ d = self.getCachePool().add(self._key(uid),
+ 'reserved',
+ expireTime=config.UIDReservationTimeOut)
+ d.addCallback(_handleFalse)
+ return d
+
+
+ def unreserveUID(self, uid):
+ uid = uid.encode('utf-8')
+ self.log_debug("Unreserving UID %r @ %r" % (
+ uid,
+ self.index.resource))
+
+ def _handleFalse(result):
+ if result is False:
+ raise ReservationError(
+ "UID %s is not reserved for calendar collection %s."
+ % (uid, self.index.resource._resourceID)
+ )
+
+ d = self.getCachePool().delete(self._key(uid))
+ d.addCallback(_handleFalse)
+ return d
+
+
+ def isReservedUID(self, uid):
+ uid = uid.encode('utf-8')
+ self.log_debug("Is reserved UID %r @ %r" % (
+ uid,
+ self.index.resource))
+
+ def _checkValue((flags, value)):
+ if value is None:
+ return False
+ else:
+ return True
+
+ d = self.getCachePool().get(self._key(uid))
+ d.addCallback(_checkValue)
+ return d
+
+class DummyUIDReserver(LoggingMixIn):
+
+ def __init__(self, index):
+ self.index = index
+ self.reservations = set()
+
+ def _key(self, uid):
+ return 'reservation:%s' % (
+ hashlib.md5('%s:%s' % (uid,
+ self.index.resource._resourceID)).hexdigest())
+
+ def reserveUID(self, uid):
+ uid = uid.encode('utf-8')
+ self.log_debug("Reserving UID %r @ %r" % (
+ uid,
+ self.index.resource))
+
+ key = self._key(uid)
+ if key in self.reservations:
+ raise ReservationError(
+ "UID %s already reserved for calendar collection %s."
+ % (uid, self.index.resource._name)
+ )
+ self.reservations.add(key)
+ return succeed(None)
+
+
+ def unreserveUID(self, uid):
+ uid = uid.encode('utf-8')
+ self.log_debug("Unreserving UID %r @ %r" % (
+ uid,
+ self.index.resource))
+
+ key = self._key(uid)
+ if key in self.reservations:
+ self.reservations.remove(key)
+ return succeed(None)
+
+
+ def isReservedUID(self, uid):
+ uid = uid.encode('utf-8')
+ self.log_debug("Is reserved UID %r @ %r" % (
+ uid,
+ self.index.resource))
+ key = self._key(uid)
+ return succeed(key in self.reservations)
+
+class PostgresLegacyIndexEmulator(LoggingMixIn):
+ """
+ Emulator for L{twistedcaldv.index.Index} and
+ L{twistedcaldv.index.IndexSchedule}.
+ """
+
+ def __init__(self, calendar):
+ self.resource = self.calendar = calendar
+ if (
+ hasattr(config, "Memcached") and
+ config.Memcached.Pools.Default.ClientEnabled
+ ):
+ self.reserver = MemcachedUIDReserver(self)
+ else:
+ # This is only used with unit tests
+ self.reserver = DummyUIDReserver(self)
+
+ @property
+ def _txn(self):
+ return self.calendar._txn
+
+
+ def reserveUID(self, uid):
+ if self.calendar._name == "inbox":
+ return succeed(None)
+ else:
+ return self.reserver.reserveUID(uid)
+
+
+ def unreserveUID(self, uid):
+ if self.calendar._name == "inbox":
+ return succeed(None)
+ else:
+ return self.reserver.unreserveUID(uid)
+
+
+ def isReservedUID(self, uid):
+ if self.calendar._name == "inbox":
+ return succeed(False)
+ else:
+ return self.reserver.isReservedUID(uid)
+
+
+ def isAllowedUID(self, uid, *names):
+ """
+ Checks to see whether to allow an operation which would add the
+ specified UID to the index. Specifically, the operation may not
+ violate the constraint that UIDs must be unique.
+ @param uid: the UID to check
+ @param names: the names of resources being replaced or deleted by the
+ operation; UIDs associated with these resources are not checked.
+ @return: True if the UID is not in the index and is not reserved,
+ False otherwise.
+ """
+ if self.calendar._name == "inbox":
+ return True
+ else:
+ rname = self.resourceNameForUID(uid)
+ return (rname is None or rname in names)
+
+ def resourceUIDForName(self, name):
+ obj = self.calendar.calendarObjectWithName(name)
+ if obj is None:
+ return None
+ return obj.uid()
+
+
+ def resourceNameForUID(self, uid):
+ obj = self.calendar.calendarObjectWithUID(uid)
+ if obj is None:
+ return None
+ return obj.name()
+
+
+ def notExpandedBeyond(self, minDate):
+ """
+ Gives all resources which have not been expanded beyond a given date
+ in the database. (Unused; see above L{postgresqlgenerator}.
+ """
+ return [row[0] for row in self._txn.execSQL(
+ "select RESOURCE_NAME from CALENDAR_OBJECT "
+ "where RECURRANCE_MAX < %s and CALENDAR_RESOURCE_ID = %s",
+ [normalizeForIndex(minDate), self.calendar._resourceID]
+ )]
+
+
+ def reExpandResource(self, name, expand_until):
+ """
+ Given a resource name, remove it from the database and re-add it
+ with a longer expansion.
+ """
+ obj = self.calendar.calendarObjectWithName(name)
+ obj.updateDatabase(obj.component(), expand_until=expand_until, reCreate=True)
+
+ def testAndUpdateIndex(self, minDate):
+ # Find out if the index is expanded far enough
+ names = self.notExpandedBeyond(minDate)
+
+ # Actually expand recurrence max
+ for name in names:
+ self.log_info("Search falls outside range of index for %s %s" % (name, minDate))
+ self.reExpandResource(name, minDate)
+
+ def whatchanged(self, revision):
+
+ results = [
+ (name.encode("utf-8"), deleted)
+ for name, deleted in
+ self._txn.execSQL(
+ """select RESOURCE_NAME, DELETED from CALENDAR_OBJECT_REVISIONS
+ where REVISION > %s and CALENDAR_RESOURCE_ID = %s""",
+ [revision, self.calendar._resourceID],
+ )
+ ]
+ results.sort(key=lambda x:x[1])
+
+ changed = []
+ deleted = []
+ for name, wasdeleted in results:
+ if name:
+ if wasdeleted:
+ if revision:
+ deleted.append(name)
+ else:
+ changed.append(name)
+ else:
+ raise SyncTokenValidException
+
+ return changed, deleted,
+
+ def indexedSearch(self, filter, useruid='', fbtype=False):
+ """
+ Finds resources matching the given qualifiers.
+ @param filter: the L{Filter} for the calendar-query to execute.
+ @return: an iterable of tuples for each resource matching the
+ given C{qualifiers}. The tuples are C{(name, uid, type)}, where
+ C{name} is the resource name, C{uid} is the resource UID, and
+ C{type} is the resource iCalendar component type.x
+ """
+
+ # Make sure we have a proper Filter element and get the partial SQL
+ # statement to use.
+ if isinstance(filter, calendarqueryfilter.Filter):
+ qualifiers = calendarquery.sqlcalendarquery(filter, self.calendar._resourceID, useruid, generator=postgresqlgenerator)
+ if qualifiers is not None:
+ # Determine how far we need to extend the current expansion of
+ # events. If we have an open-ended time-range we will expand one
+ # year past the start. That should catch bounded recurrences - unbounded
+ # will have been indexed with an "infinite" value always included.
+ maxDate, isStartDate = filter.getmaxtimerange()
+ if maxDate:
+ maxDate = maxDate.date()
+ if isStartDate:
+ maxDate += datetime.timedelta(days=365)
+ self.testAndUpdateIndex(maxDate)
+ else:
+ # We cannot handler this filter in an indexed search
+ raise IndexedSearchException()
+
+ else:
+ qualifiers = None
+
+ # Perform the search
+ if qualifiers is None:
+ rowiter = self._txn.execSQL(
+ "select RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = %s",
+ [self.calendar._resourceID, ],
+ )
+ else:
+ if fbtype:
+ # For a free-busy time-range query we return all instances
+ rowiter = self._txn.execSQL(
+ """select DISTINCT
+ CALENDAR_OBJECT.RESOURCE_NAME, CALENDAR_OBJECT.ICALENDAR_UID, CALENDAR_OBJECT.ICALENDAR_TYPE, CALENDAR_OBJECT.ORGANIZER,
+ TIME_RANGE.FLOATING, TIME_RANGE.START_DATE, TIME_RANGE.END_DATE, TIME_RANGE.FBTYPE, TIME_RANGE.TRANSPARENT, TRANSPARENCY.TRANSPARENT""" +
+ qualifiers[0],
+ qualifiers[1]
+ )
+ else:
+ rowiter = self._txn.execSQL(
+ "select DISTINCT CALENDAR_OBJECT.RESOURCE_NAME, CALENDAR_OBJECT.ICALENDAR_UID, CALENDAR_OBJECT.ICALENDAR_TYPE" +
+ qualifiers[0],
+ qualifiers[1]
+ )
+
+ # Check result for missing resources
+
+ for row in rowiter:
+ if fbtype:
+ row = list(row)
+ row[4] = 'Y' if row[4] else 'N'
+ row[7] = indexfbtype_to_icalfbtype[row[7]]
+ row[8] = 'T' if row[9] else 'F'
+ del row[9]
+ yield row
+
+
+ def bruteForceSearch(self):
+ return self._txn.execSQL(
+ "select RESOURCE_NAME, ICALENDAR_UID, ICALENDAR_TYPE from "
+ "CALENDAR_OBJECT where CALENDAR_RESOURCE_ID = %s",
+ [self.calendar._resourceID]
+ )
+
+
+ def resourcesExist(self, names):
+ return list(set(names).intersection(
+ set(self.calendar.listCalendarObjects())))
+
+
+ def resourceExists(self, name):
+ return bool(
+ self._txn.execSQL(
+ "select RESOURCE_NAME from CALENDAR_OBJECT where "
+ "RESOURCE_NAME = %s and CALENDAR_RESOURCE_ID = %s",
+ [name, self.calendar._resourceID]
+ )
+ )
+
+
+
+
+class PostgresLegacyNotificationsEmulator(object):
+ def __init__(self, notificationsCollection):
+ self._collection = notificationsCollection
+
+
+ def _recordForObject(self, notificationObject):
+ return NotificationRecord(
+ notificationObject.uid(),
+ notificationObject.name(),
+ notificationObject._fieldQuery("XML_TYPE"))
+
+
+ def recordForName(self, name):
+ return self._recordForObject(
+ self._collection.notificationObjectWithName(name)
+ )
+
+
+ def recordForUID(self, uid):
+ return self._recordForObject(
+ self._collection.notificationObjectWithUID(uid)
+ )
+
+
+ def removeRecordForUID(self, uid):
+ self._collection.removeNotificationObjectWithUID(uid)
+
+
+ def removeRecordForName(self, name):
+ self._collection.removeNotificationObjectWithName(name)
+
+
+
+
+# CARDDAV
+
+class postgresqladbkgenerator(sqlgenerator):
+ """
+ Query generator for postgreSQL indexed searches. (Currently unused: work
+ in progress.)
+ """
+
+ ISOP = " = "
+ CONTAINSOP = " LIKE "
+ NOTCONTAINSOP = " NOT LIKE "
+ FIELDS = {
+ "UID": "ADDRESSBOOK_OBJECT.VCARD_UID",
+ }
+
+ def __init__(self, expr, addressbookid):
+ self.RESOURCEDB = "ADDRESSBOOK_OBJECT"
+
+ super(postgresqladbkgenerator, self).__init__(expr, addressbookid)
+
+
+ def generate(self):
+ """
+ Generate the actual SQL 'where ...' expression from the passed in
+ expression tree.
+
+ @return: a C{tuple} of (C{str}, C{list}), where the C{str} is the
+ partial SQL statement, and the C{list} is the list of argument
+ substitutions to use with the SQL API execute method.
+ """
+
+ # Init state
+ self.sout = StringIO.StringIO()
+ self.arguments = []
+ self.substitutions = []
+
+ # Generate ' where ...' partial statement
+ self.sout.write(self.WHERE)
+ self.generateExpression(self.expression)
+
+ # Prefix with ' from ...' partial statement
+ select = self.FROM + self.RESOURCEDB
+ select += self.sout.getvalue()
+
+ select = select % tuple(self.substitutions)
+
+ return select, self.arguments
+
+
+ def addArgument(self, arg):
+ self.arguments.append(arg)
+ self.substitutions.append("%s")
+ self.sout.write("%s")
+
+ def setArgument(self, arg):
+ self.arguments.append(arg)
+ self.substitutions.append("%s")
+
+ def frontArgument(self, arg):
+ self.arguments.insert(0, arg)
+ self.substitutions.insert(0, "%s")
+
+ def containsArgument(self, arg):
+ return "%%%s%%" % (arg,)
+
+
+class PostgresLegacyABIndexEmulator(object):
+ """
+ Emulator for L{twistedcaldv.index.Index} and
+ L{twistedcaldv.index.IndexSchedule}.
+ """
+
+ def __init__(self, addressbook):
+ self.resource = self.addressbook = addressbook
+ if (
+ hasattr(config, "Memcached") and
+ config.Memcached.Pools.Default.ClientEnabled
+ ):
+ self.reserver = MemcachedUIDReserver(self)
+ else:
+ # This is only used with unit tests
+ self.reserver = DummyUIDReserver(self)
+
+
+ @property
+ def _txn(self):
+ return self.addressbook._txn
+
+
+ def reserveUID(self, uid):
+ return self.reserver.reserveUID(uid)
+
+
+ def unreserveUID(self, uid):
+ return self.reserver.unreserveUID(uid)
+
+
+ def isReservedUID(self, uid):
+ return self.reserver.isReservedUID(uid)
+
+
+ def isAllowedUID(self, uid, *names):
+ """
+ Checks to see whether to allow an operation which would add the
+ specified UID to the index. Specifically, the operation may not
+ violate the constraint that UIDs must be unique.
+ @param uid: the UID to check
+ @param names: the names of resources being replaced or deleted by the
+ operation; UIDs associated with these resources are not checked.
+ @return: True if the UID is not in the index and is not reserved,
+ False otherwise.
+ """
+ rname = self.resourceNameForUID(uid)
+ return (rname is None or rname in names)
+
+
+ def resourceUIDForName(self, name):
+ obj = self.addressbook.addressbookObjectWithName(name)
+ if obj is None:
+ return None
+ return obj.uid()
+
+
+ def resourceNameForUID(self, uid):
+ obj = self.addressbook.addressbookObjectWithUID(uid)
+ if obj is None:
+ return None
+ return obj.name()
+
+
+ def whatchanged(self, revision):
+
+ results = [
+ (name.encode("utf-8"), deleted)
+ for name, deleted in
+ self._txn.execSQL(
+ """select RESOURCE_NAME, DELETED from ADDRESSBOOK_OBJECT_REVISIONS
+ where REVISION > %s and ADDRESSBOOK_RESOURCE_ID = %s""",
+ [revision, self.addressbook._resourceID],
+ )
+ ]
+ results.sort(key=lambda x:x[1])
+
+ changed = []
+ deleted = []
+ for name, wasdeleted in results:
+ if name:
+ if wasdeleted:
+ if revision:
+ deleted.append(name)
+ else:
+ changed.append(name)
+ else:
+ raise SyncTokenValidException
+
+ return changed, deleted,
+
+ def searchValid(self, filter):
+ if isinstance(filter, carddavxml.Filter):
+ qualifiers = addressbookquery.sqladdressbookquery(filter)
+ else:
+ qualifiers = None
+
+ return qualifiers is not None
+
+ def search(self, filter):
+ """
+ Finds resources matching the given qualifiers.
+ @param filter: the L{Filter} for the addressbook-query to execute.
+ @return: an iterable of tuples for each resource matching the
+ given C{qualifiers}. The tuples are C{(name, uid, type)}, where
+ C{name} is the resource name, C{uid} is the resource UID, and
+ C{type} is the resource iCalendar component type.x
+ """
+
+ # Make sure we have a proper Filter element and get the partial SQL statement to use.
+ if isinstance(filter, carddavxml.Filter):
+ qualifiers = addressbookquery.sqladdressbookquery(filter, self.addressbook._resourceID, generator=postgresqladbkgenerator)
+ else:
+ qualifiers = None
+ if qualifiers is not None:
+ rowiter = self._txn.execSQL(
+ "select DISTINCT ADDRESSBOOK_OBJECT.RESOURCE_NAME, ADDRESSBOOK_OBJECT.VCARD_UID" +
+ qualifiers[0],
+ qualifiers[1]
+ )
+ else:
+ rowiter = self._txn.execSQL(
+ "select RESOURCE_NAME, VCARD_UID from ADDRESSBOOK_OBJECT where ADDRESSBOOK_RESOURCE_ID = %s",
+ [self.addressbook._resourceID, ],
+ )
+
+ for row in rowiter:
+ yield row
+
+ def indexedSearch(self, filter, useruid='', fbtype=False):
+ """
+ Always raise L{IndexedSearchException}, since these indexes are not
+ fully implemented yet.
+ """
+ raise IndexedSearchException()
+
+
+ def bruteForceSearch(self):
+ return self._txn.execSQL(
+ "select RESOURCE_NAME, VCARD_UID from "
+ "ADDRESSBOOK_OBJECT where ADDRESSBOOK_RESOURCE_ID = %s",
+ [self.addressbook._resourceID]
+ )
+
+
+ def resourcesExist(self, names):
+ return list(set(names).intersection(
+ set(self.addressbook.listAddressbookObjects())))
+
+
+ def resourceExists(self, name):
+ return bool(
+ self._txn.execSQL(
+ "select RESOURCE_NAME from ADDRESSBOOK_OBJECT where "
+ "RESOURCE_NAME = %s and ADDRESSBOOK_RESOURCE_ID = %s",
+ [name, self.addressbook._resourceID]
+ )
+ )
+
+
+class PostgresLegacyABInvitesEmulator(object):
+ """
+ Emulator for the implicit interface specified by
+ L{twistedcaldav.sharing.InvitesDatabase}.
+ """
+
+
+ def __init__(self, addressbook):
+ self._addressbook = addressbook
+
+
+ @property
+ def _txn(self):
+ return self._addressbook._txn
+
+
+ def create(self):
+ "No-op, because the index implicitly always exists in the database."
+
+
+ def remove(self):
+ "No-op, because the index implicitly always exists in the database."
+
+
+ def allRecords(self):
+ for row in self._txn.execSQL(
+ """
+ select
+ INVITE.INVITE_UID, INVITE.NAME, INVITE.RECIPIENT_ADDRESS,
+ ADDRESSBOOK_HOME.OWNER_UID, ADDRESSBOOK_BIND.BIND_MODE,
+ ADDRESSBOOK_BIND.BIND_STATUS, ADDRESSBOOK_BIND.MESSAGE
+ from
+ INVITE, ADDRESSBOOK_HOME, ADDRESSBOOK_BIND
+ where
+ INVITE.RESOURCE_ID = %s and
+ INVITE.HOME_RESOURCE_ID =
+ ADDRESSBOOK_HOME.RESOURCE_ID and
+ ADDRESSBOOK_BIND.ADDRESSBOOK_RESOURCE_ID =
+ INVITE.RESOURCE_ID and
+ ADDRESSBOOK_BIND.ADDRESSBOOK_HOME_RESOURCE_ID =
+ INVITE.HOME_RESOURCE_ID
+ order by
+ INVITE.NAME asc
+ """, [self._addressbook._resourceID]):
+ [inviteuid, common_name, userid, ownerUID,
+ bindMode, bindStatus, summary] = row
+ # FIXME: this is really the responsibility of the protocol layer.
+ state = {
+ _BIND_STATUS_INVITED: "NEEDS-ACTION",
+ _BIND_STATUS_ACCEPTED: "ACCEPTED",
+ _BIND_STATUS_DECLINED: "DECLINED",
+ _BIND_STATUS_INVALID: "INVALID",
+ }[bindStatus]
+ access = {
+ _BIND_MODE_READ: "read-only",
+ _BIND_MODE_WRITE: "read-write"
+ }[bindMode]
+ principalURL = "/principals/__uids__/%s/" % (ownerUID,)
+ yield Invite(
+ inviteuid, userid, principalURL, common_name,
+ access, state, summary
+ )
+
+
+ def recordForUserID(self, userid):
+ for record in self.allRecords():
+ if record.userid == userid:
+ return record
+
+
+ def recordForPrincipalURL(self, principalURL):
+ for record in self.allRecords():
+ if record.principalURL == principalURL:
+ return record
+
+
+ def recordForInviteUID(self, inviteUID):
+ for record in self.allRecords():
+ if record.inviteuid == inviteUID:
+ return record
+
+
+ def addOrUpdateRecord(self, record):
+ bindMode = {'read-only': _BIND_MODE_READ,
+ 'read-write': _BIND_MODE_WRITE}[record.access]
+ bindStatus = {
+ "NEEDS-ACTION": _BIND_STATUS_INVITED,
+ "ACCEPTED": _BIND_STATUS_ACCEPTED,
+ "DECLINED": _BIND_STATUS_DECLINED,
+ "INVALID": _BIND_STATUS_INVALID,
+ }[record.state]
+ # principalURL is derived from a directory record's principalURL() so
+ # it will always contain the UID. The form is '/principals/__uids__/x'
+ # (and may contain a trailing slash).
+ principalUID = record.principalURL.split("/")[3]
+ shareeHome = self._txn.addressbookHomeWithUID(principalUID, create=True)
+ rows = self._txn.execSQL(
+ "select RESOURCE_ID, HOME_RESOURCE_ID from INVITE where RECIPIENT_ADDRESS = %s",
+ [record.userid]
+ )
+ if rows:
+ [[resourceID, homeResourceID]] = rows
+ # Invite(inviteuid, userid, principalURL, common_name, access, state, summary)
+ self._txn.execSQL("""
+ update ADDRESSBOOK_BIND set BIND_MODE = %s,
+ BIND_STATUS = %s, MESSAGE = %s
+ where
+ ADDRESSBOOK_RESOURCE_ID = %s and
+ ADDRESSBOOK_HOME_RESOURCE_ID = %s
+ """, [bindMode, bindStatus, record.summary,
+ resourceID, homeResourceID])
+ self._txn.execSQL("""
+ update INVITE set NAME = %s, INVITE_UID = %s
+ where RECIPIENT_ADDRESS = %s
+ """,
+ [record.name, record.inviteuid, record.userid]
+ )
+ else:
+ self._txn.execSQL(
+ """
+ insert into INVITE (
+ INVITE_UID, NAME,
+ HOME_RESOURCE_ID, RESOURCE_ID,
+ RECIPIENT_ADDRESS
+ )
+ values (%s, %s, %s, %s, %s)
+ """,
+ [
+ record.inviteuid, record.name,
+ shareeHome._resourceID, self._addressbook._resourceID,
+ record.userid
+ ])
+ self._txn.execSQL(
+ """
+ insert into ADDRESSBOOK_BIND
+ (
+ ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_ID,
+ ADDRESSBOOK_RESOURCE_NAME, BIND_MODE, BIND_STATUS,
+ SEEN_BY_OWNER, SEEN_BY_SHAREE, MESSAGE
+ )
+ values (%s, %s, %s, %s, %s, %s, %s, %s)
+ """,
+ [
+ shareeHome._resourceID,
+ self._addressbook._resourceID,
+ None, # this is NULL because it is not bound yet, let's be
+ # explicit about that.
+ bindMode,
+ bindStatus,
+ False,
+ False,
+ record.summary
+ ])
+
+
+ def removeRecordForUserID(self, userid):
+ rec = self.recordForUserID(userid)
+ self.removeRecordForInviteUID(rec.inviteuid)
+
+
+ def removeRecordForPrincipalURL(self, principalURL):
+ raise NotImplementedError("removeRecordForPrincipalURL")
+
+
+ def removeRecordForInviteUID(self, inviteUID):
+ rows = self._txn.execSQL("""
+ select HOME_RESOURCE_ID, RESOURCE_ID from INVITE where
+ INVITE_UID = %s
+ """, [inviteUID])
+ if rows:
+ [[homeID, resourceID]] = rows
+ self._txn.execSQL(
+ "delete from ADDRESSBOOK_BIND where "
+ "ADDRESSBOOK_HOME_RESOURCE_ID = %s and ADDRESSBOOK_RESOURCE_ID = %s",
+ [homeID, resourceID])
+ self._txn.execSQL("delete from INVITE where INVITE_UID = %s",
+ [inviteUID])
+
+
+
+class PostgresLegacyABSharesEmulator(object):
+
+ def __init__(self, home):
+ self._home = home
+
+
+ @property
+ def _txn(self):
+ return self._home._txn
+
+
+ def create(self):
+ pass
+
+
+ def remove(self):
+ pass
+
+
+ def allRecords(self):
+ # This should have been a smart join that got all these columns at
+ # once, but let's not bother to fix it, since the actual query we
+ # _want_ to do (just look for addressbook binds in a particular homes) is
+ # much simpler anyway; we should just do that.
+ shareRows = self._txn.execSQL(
+ """
+ select ADDRESSBOOK_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME, MESSAGE
+ from ADDRESSBOOK_BIND
+ where ADDRESSBOOK_HOME_RESOURCE_ID = %s and
+ BIND_MODE != %s and
+ ADDRESSBOOK_RESOURCE_NAME is not null
+ """, [self._home._resourceID, _BIND_MODE_OWN])
+ for resourceID, resourceName, summary in shareRows:
+ [[shareuid]] = self._txn.execSQL(
+ """
+ select INVITE_UID
+ from INVITE
+ where RESOURCE_ID = %s and HOME_RESOURCE_ID = %s
+ """, [resourceID, self._home._resourceID])
+ sharetype = 'I'
+ [[ownerHomeID, ownerResourceName]] = self._txn.execSQL(
+ """
+ select ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME
+ from ADDRESSBOOK_BIND
+ where ADDRESSBOOK_RESOURCE_ID = %s and
+ BIND_MODE = %s
+ """, [resourceID, _BIND_MODE_OWN]
+ )
+ [[ownerUID]] = self._txn.execSQL(
+ "select OWNER_UID from ADDRESSBOOK_HOME where RESOURCE_ID = %s",
+ [ownerHomeID])
+ hosturl = '/addressbooks/__uids__/%s/%s' % (
+ ownerUID, ownerResourceName
+ )
+ localname = resourceName
+ record = SharedCollectionRecord(
+ shareuid, sharetype, hosturl, localname, summary
+ )
+ yield record
+
+
+ def _search(self, **kw):
+ [[key, value]] = kw.items()
+ for record in self.allRecords():
+ if getattr(record, key) == value:
+ return record
+
+ def recordForLocalName(self, localname):
+ return self._search(localname=localname)
+
+ def recordForShareUID(self, shareUID):
+ return self._search(shareuid=shareUID)
+
+
+ def addOrUpdateRecord(self, record):
+# print '*** SHARING***: Adding or updating this record:'
+# import pprint
+# pprint.pprint(record.__dict__)
+ # record.hosturl -> /addressbooks/__uids__/<uid>/<addressbookname>
+ splithost = record.hosturl.split('/')
+ ownerUID = splithost[3]
+ ownerAddressBookName = splithost[4]
+ ownerHome = self._txn.addressbookHomeWithUID(ownerUID)
+ ownerAddressBook = ownerHome.addressbookWithName(ownerAddressBookName)
+ addressbookResourceID = ownerAddressBook._resourceID
+
+ # There needs to be a bind already, one that corresponds to the
+ # invitation. The invitation's UID is the same as the share UID. I
+ # just need to update its 'localname', i.e.
+ # ADDRESSBOOK_BIND.ADDRESSBOOK_RESOURCE_NAME.
+
+ self._txn.execSQL(
+ """
+ update ADDRESSBOOK_BIND set ADDRESSBOOK_RESOURCE_NAME = %s
+ where ADDRESSBOOK_HOME_RESOURCE_ID = %s and ADDRESSBOOK_RESOURCE_ID = %s
+ """,
+ [record.localname, self._home._resourceID, addressbookResourceID]
+ )
+
+
+ def removeRecordForLocalName(self, localname):
+ self._txn.execSQL(
+ "delete from ADDRESSBOOK_BIND where ADDRESSBOOK_RESOURCE_NAME = %s "
+ "and ADDRESSBOOK_HOME_RESOURCE_ID = %s",
+ [localname, self._home._resourceID]
+ )
+
+
+ def removeRecordForShareUID(self, shareUID):
+ pass
+# c = self._home._cursor()
+# c.execute(
+# "delete from ADDRESSBOOK_BIND where ADDRESSBOOK_RESOURCE_NAME = %s "
+# "and ADDRESSBOOK_HOME_RESOURCE_ID = %s",
+# [self._home._resourceID]
+# )
Added: CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_schema_v1.sql
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_schema_v1.sql (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_schema_v1.sql 2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,342 @@
+-----------------
+-- Resource ID --
+-----------------
+
+create sequence RESOURCE_ID_SEQ;
+
+
+-------------------
+-- Calendar Home --
+-------------------
+
+create table CALENDAR_HOME (
+ RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'),
+ OWNER_UID varchar(255) not null unique
+);
+
+
+--------------
+-- Calendar --
+--------------
+
+create table CALENDAR (
+ RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'),
+ REVISION integer default 0,
+ CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+ MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+------------------------
+-- Sharing Invitation --
+------------------------
+
+create table INVITE (
+ INVITE_UID varchar(255) not null,
+ NAME varchar(255) not null,
+ RECIPIENT_ADDRESS varchar(255) not null,
+ HOME_RESOURCE_ID integer not null,
+ RESOURCE_ID integer not null
+);
+
+
+---------------------------
+-- Sharing Notifications --
+---------------------------
+
+create table NOTIFICATION_HOME (
+ RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'),
+ OWNER_UID varchar(255) not null unique
+);
+
+
+create table NOTIFICATION (
+ RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'),
+ NOTIFICATION_HOME_RESOURCE_ID integer not null references NOTIFICATION_HOME,
+ NOTIFICATION_UID varchar(255) not null,
+ XML_TYPE varchar not null,
+ XML_DATA varchar not null,
+ CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+ MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+
+ unique(NOTIFICATION_UID, NOTIFICATION_HOME_RESOURCE_ID)
+);
+
+
+-------------------
+-- Calendar Bind --
+-------------------
+
+-- Joins CALENDAR_HOME and CALENDAR
+
+create table CALENDAR_BIND (
+ CALENDAR_HOME_RESOURCE_ID integer not null references CALENDAR_HOME,
+ CALENDAR_RESOURCE_ID integer not null references CALENDAR on delete cascade,
+
+ -- An invitation which hasn't been accepted yet will not yet have a resource
+ -- name, so this field may be null.
+
+ CALENDAR_RESOURCE_NAME varchar(255),
+ BIND_MODE integer not null, -- enum CALENDAR_BIND_MODE
+ BIND_STATUS integer not null, -- enum CALENDAR_BIND_STATUS
+ SEEN_BY_OWNER boolean not null,
+ SEEN_BY_SHAREE boolean not null,
+ MESSAGE text,
+
+ primary key(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_ID),
+ unique(CALENDAR_HOME_RESOURCE_ID, CALENDAR_RESOURCE_NAME)
+);
+
+-- Enumeration of calendar bind modes
+
+create table CALENDAR_BIND_MODE (
+ ID integer primary key,
+ DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_MODE values (0, 'own' );
+insert into CALENDAR_BIND_MODE values (1, 'read' );
+insert into CALENDAR_BIND_MODE values (2, 'write');
+
+-- Enumeration of statuses
+
+create table CALENDAR_BIND_STATUS (
+ ID integer primary key,
+ DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_BIND_STATUS values (0, 'invited' );
+insert into CALENDAR_BIND_STATUS values (1, 'accepted');
+insert into CALENDAR_BIND_STATUS values (2, 'declined');
+insert into CALENDAR_BIND_STATUS values (3, 'invalid');
+
+
+---------------------
+-- Calendar Object --
+---------------------
+
+create table CALENDAR_OBJECT (
+ RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'),
+ CALENDAR_RESOURCE_ID integer not null references CALENDAR on delete cascade,
+ RESOURCE_NAME varchar(255) not null,
+ ICALENDAR_TEXT text not null,
+ ICALENDAR_UID varchar(255) not null,
+ ICALENDAR_TYPE varchar(255) not null,
+ ATTACHMENTS_MODE integer not null, -- enum CALENDAR_OBJECT_ATTACHMENTS_MODE
+ ORGANIZER varchar(255),
+ ORGANIZER_OBJECT integer references CALENDAR_OBJECT,
+ RECURRANCE_MAX date, -- maximum date that recurrences have been expanded to.
+ CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+ MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+
+ unique(CALENDAR_RESOURCE_ID, RESOURCE_NAME)
+
+ -- since the 'inbox' is a 'calendar resource' for the purpose of storing
+ -- calendar objects, this constraint has to be selectively enforced by the
+ -- application layer.
+
+ -- unique(CALENDAR_RESOURCE_ID, ICALENDAR_UID)
+);
+
+-- Enumeration of attachment modes
+
+create table CALENDAR_OBJECT_ATTACHMENTS_MODE (
+ ID integer primary key,
+ DESCRIPTION varchar(16) not null unique
+);
+
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (0, 'read' );
+insert into CALENDAR_OBJECT_ATTACHMENTS_MODE values (1, 'write');
+
+
+-----------------
+-- Instance ID --
+-----------------
+
+create sequence INSTANCE_ID_SEQ;
+
+
+----------------
+-- Time Range --
+----------------
+
+create table TIME_RANGE (
+ INSTANCE_ID integer primary key default nextval('INSTANCE_ID_SEQ'),
+ CALENDAR_RESOURCE_ID integer not null references CALENDAR on delete cascade,
+ CALENDAR_OBJECT_RESOURCE_ID integer not null references CALENDAR_OBJECT on delete cascade,
+ FLOATING boolean not null,
+ START_DATE timestamp not null,
+ END_DATE timestamp not null,
+ FBTYPE integer not null,
+ TRANSPARENT boolean not null
+);
+
+-- Enumeration of free/busy types
+
+create table FREE_BUSY_TYPE (
+ ID integer primary key,
+ DESCRIPTION varchar(16) not null unique
+);
+
+insert into FREE_BUSY_TYPE values (0, 'unknown' );
+insert into FREE_BUSY_TYPE values (1, 'free' );
+insert into FREE_BUSY_TYPE values (2, 'busy' );
+insert into FREE_BUSY_TYPE values (3, 'busy-unavailable');
+insert into FREE_BUSY_TYPE values (4, 'busy-tentative' );
+
+
+------------------
+-- Transparency --
+------------------
+
+create table TRANSPARENCY (
+ TIME_RANGE_INSTANCE_ID integer not null references TIME_RANGE on delete cascade,
+ USER_ID varchar(255) not null,
+ TRANSPARENT boolean not null
+);
+
+
+----------------
+-- Attachment --
+----------------
+
+create table ATTACHMENT (
+ CALENDAR_OBJECT_RESOURCE_ID integer not null references CALENDAR_OBJECT on delete cascade,
+ CONTENT_TYPE varchar(255) not null,
+ SIZE integer not null,
+ MD5 char(32) not null,
+ CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+ MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+ PATH varchar(1024) not null unique
+);
+
+
+------------------------------
+-- Calendar Object Revision --
+------------------------------
+
+create sequence CALENDAR_OBJECT_REVISION_SEQ;
+
+
+-------------------------------
+-- Calendar Object Revisions --
+-------------------------------
+
+create table CALENDAR_OBJECT_REVISIONS (
+ CALENDAR_RESOURCE_ID integer not null references CALENDAR on delete cascade,
+ RESOURCE_NAME varchar(255) not null,
+ REVISION integer not null,
+ DELETED boolean not null,
+
+ unique(CALENDAR_RESOURCE_ID, RESOURCE_NAME)
+);
+
+
+------------------
+-- iTIP Message --
+------------------
+
+create table ITIP_MESSAGE (
+ CALENDAR_RESOURCE_ID integer not null references CALENDAR,
+ ICALENDAR_TEXT text not null,
+ ICALENDAR_UID varchar(255) not null,
+ MD5 char(32) not null,
+ CHANGES text not null
+);
+
+
+-----------------------
+-- Resource Property --
+-----------------------
+
+create table RESOURCE_PROPERTY (
+ RESOURCE_ID integer not null, -- foreign key: *.RESOURCE_ID
+ NAME varchar(255) not null,
+ VALUE text not null, -- FIXME: xml?
+ VIEWER_UID varchar(255),
+
+ primary key(RESOURCE_ID, NAME, VIEWER_UID)
+);
+
+
+----------------------
+-- AddressBook Home --
+----------------------
+
+create table ADDRESSBOOK_HOME (
+ RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'),
+ OWNER_UID varchar(255) not null unique
+);
+
+
+-----------------
+-- AddressBook --
+-----------------
+
+create table ADDRESSBOOK (
+ RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'),
+ REVISION integer default 0,
+ CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+ MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP)
+);
+
+
+----------------------
+-- AddressBook Bind --
+----------------------
+
+-- Joins ADDRESSBOOK_HOME and ADDRESSBOOK
+
+create table ADDRESSBOOK_BIND (
+ ADDRESSBOOK_HOME_RESOURCE_ID integer not null references ADDRESSBOOK_HOME,
+ ADDRESSBOOK_RESOURCE_ID integer not null references ADDRESSBOOK on delete cascade,
+
+ -- An invitation which hasn't been accepted yet will not yet have a resource
+ -- name, so this field may be null.
+
+ ADDRESSBOOK_RESOURCE_NAME varchar(255),
+ BIND_MODE integer not null, -- enum CALENDAR_BIND_MODE
+ BIND_STATUS integer not null, -- enum CALENDAR_BIND_STATUS
+ SEEN_BY_OWNER boolean not null,
+ SEEN_BY_SHAREE boolean not null,
+ MESSAGE text, -- FIXME: xml?
+
+ primary key(ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_ID),
+ unique(ADDRESSBOOK_HOME_RESOURCE_ID, ADDRESSBOOK_RESOURCE_NAME)
+);
+
+
+create table ADDRESSBOOK_OBJECT (
+ RESOURCE_ID integer primary key default nextval('RESOURCE_ID_SEQ'),
+ ADDRESSBOOK_RESOURCE_ID integer not null references ADDRESSBOOK on delete cascade,
+ RESOURCE_NAME varchar(255) not null,
+ VCARD_TEXT text not null,
+ VCARD_UID varchar(255) not null,
+ CREATED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+ MODIFIED timestamp default timezone('UTC', CURRENT_TIMESTAMP),
+
+ unique(ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME),
+ unique(ADDRESSBOOK_RESOURCE_ID, VCARD_UID)
+);
+
+------------------------------
+-- AddressBook Object Revision --
+------------------------------
+
+create sequence ADDRESSBOOK_OBJECT_REVISION_SEQ;
+
+
+-------------------------------
+-- AddressBook Object Revisions --
+-------------------------------
+
+create table ADDRESSBOOK_OBJECT_REVISIONS (
+ ADDRESSBOOK_RESOURCE_ID integer not null references ADDRESSBOOK on delete cascade,
+ RESOURCE_NAME varchar(255) not null,
+ REVISION integer not null,
+ DELETED boolean not null,
+
+ unique(ADDRESSBOOK_RESOURCE_ID, RESOURCE_NAME)
+);
+
+
Added: CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_tables.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_tables.py (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txdav/common/datastore/sql_tables.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,132 @@
+# -*- test-case-name: txcaldav.calendarstore.test.test_postgres -*-
+##
+# 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.
+##
+
+"""
+SQL Table definitions.
+"""
+
+CALENDAR_HOME_TABLE = {
+ "name" : "CALENDAR_HOME",
+ "column_RESOURCE_ID" : "RESOURCE_ID",
+ "column_OWNER_UID" : "OWNER_UID",
+}
+
+ADDRESSBOOK_HOME_TABLE = {
+ "name" : "ADDRESSBOOK_HOME",
+ "column_RESOURCE_ID" : "RESOURCE_ID",
+ "column_OWNER_UID" : "OWNER_UID",
+}
+
+NOTIFICATION_HOME_TABLE = {
+ "name" : "NOTIFICATION_HOME",
+ "column_RESOURCE_ID" : "RESOURCE_ID",
+ "column_OWNER_UID" : "OWNER_UID",
+}
+
+CALENDAR_TABLE = {
+ "name" : "CALENDAR",
+ "column_RESOURCE_ID" : "RESOURCE_ID",
+ "column_REVISION" : "REVISION",
+ "column_CREATED" : "CREATED",
+ "column_MODIFIED" : "MODIFIED",
+ "sequence" : "CALENDAR_OBJECT_REVISION_SEQ",
+}
+
+ADDRESSBOOK_TABLE = {
+ "name" : "ADDRESSBOOK",
+ "column_RESOURCE_ID" : "RESOURCE_ID",
+ "column_REVISION" : "REVISION",
+ "column_CREATED" : "CREATED",
+ "column_MODIFIED" : "MODIFIED",
+ "sequence" : "ADDRESSBOOK_OBJECT_REVISION_SEQ",
+}
+
+CALENDAR_BIND_TABLE = {
+ "name" : "CALENDAR_BIND",
+ "column_HOME_RESOURCE_ID" : "CALENDAR_HOME_RESOURCE_ID",
+ "column_RESOURCE_ID" : "CALENDAR_RESOURCE_ID",
+ "column_RESOURCE_NAME" : "CALENDAR_RESOURCE_NAME",
+ "column_BIND_MODE" : "BIND_MODE",
+ "column_BIND_STATUS" : "BIND_STATUS",
+ "column_SEEN_BY_OWNER" : "SEEN_BY_OWNER",
+ "column_SEEN_BY_SHAREE" : "SEEN_BY_SHAREE",
+ "column_MESSAGE" : "MESSAGE",
+}
+
+ADDRESSBOOK_BIND_TABLE = {
+ "name" : "ADDRESSBOOK_BIND",
+ "column_HOME_RESOURCE_ID" : "ADDRESSBOOK_HOME_RESOURCE_ID",
+ "column_RESOURCE_ID" : "ADDRESSBOOK_RESOURCE_ID",
+ "column_RESOURCE_NAME" : "ADDRESSBOOK_RESOURCE_NAME",
+ "column_BIND_MODE" : "BIND_MODE",
+ "column_BIND_STATUS" : "BIND_STATUS",
+ "column_SEEN_BY_OWNER" : "SEEN_BY_OWNER",
+ "column_SEEN_BY_SHAREE" : "SEEN_BY_SHAREE",
+ "column_MESSAGE" : "MESSAGE",
+}
+
+CALENDAR_OBJECT_REVISIONS_TABLE = {
+ "name" : "CALENDAR_OBJECT_REVISIONS",
+ "column_RESOURCE_ID" : "CALENDAR_RESOURCE_ID",
+ "column_RESOURCE_NAME" : "RESOURCE_NAME",
+ "column_REVISION" : "REVISION",
+ "column_DELETED" : "DELETED",
+}
+
+ADDRESSBOOK_OBJECT_REVISIONS_TABLE = {
+ "name" : "ADDRESSBOOK_OBJECT_REVISIONS",
+ "column_RESOURCE_ID" : "ADDRESSBOOK_RESOURCE_ID",
+ "column_RESOURCE_NAME" : "RESOURCE_NAME",
+ "column_REVISION" : "REVISION",
+ "column_DELETED" : "DELETED",
+}
+
+CALENDAR_OBJECT_TABLE = {
+ "name" : "CALENDAR_OBJECT",
+ "column_RESOURCE_ID" : "RESOURCE_ID",
+ "column_PARENT_RESOURCE_ID" : "CALENDAR_RESOURCE_ID",
+ "column_RESOURCE_NAME" : "RESOURCE_NAME",
+ "column_TEXT" : "ICALENDAR_TEXT",
+ "column_UID" : "ICALENDAR_UID",
+ "column_CREATED" : "CREATED",
+ "column_MODIFIED" : "MODIFIED",
+}
+
+ADDRESSBOOK_OBJECT_TABLE = {
+ "name" : "ADDRESSBOOK_OBJECT",
+ "column_RESOURCE_ID" : "RESOURCE_ID",
+ "column_PARENT_RESOURCE_ID" : "ADDRESSBOOK_RESOURCE_ID",
+ "column_RESOURCE_NAME" : "RESOURCE_NAME",
+ "column_TEXT" : "VCARD_TEXT",
+ "column_UID" : "VCARD_UID",
+ "column_CREATED" : "CREATED",
+ "column_MODIFIED" : "MODIFIED",
+}
+
+
+# Various constants
+
+_BIND_STATUS_INVITED = 0
+_BIND_STATUS_ACCEPTED = 1
+_BIND_STATUS_DECLINED = 2
+_BIND_STATUS_INVALID = 3
+
+_ATTACHMENTS_MODE_WRITE = 1
+
+_BIND_MODE_OWN = 0
+_BIND_MODE_READ = 1
+_BIND_MODE_WRITE = 2
Added: CalendarServer/branches/generic-sqlstore/txdav/common/datastore/test/__init__.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/common/datastore/test/__init__.py (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txdav/common/datastore/test/__init__.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,20 @@
+# -*- test-case-name: txcarddav.addressbookstore.test -*-
+##
+# 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.
+##
+
+"""
+Store tests
+"""
Added: CalendarServer/branches/generic-sqlstore/txdav/common/datastore/test/util.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/common/datastore/test/util.py (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txdav/common/datastore/test/util.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,139 @@
+# -*- test-case-name: txcarddav.addressbookstore.test -*-
+##
+# 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.
+##
+
+"""
+Store test utility functions
+"""
+
+import gc
+
+from twext.python.filepath import CachingFilePath
+
+from twisted.internet import reactor
+from twisted.internet.defer import Deferred, succeed
+from twisted.internet.task import deferLater
+from twisted.python import log
+
+from txdav.common.datastore.sql import CommonDataStore, v1_schema
+from txdav.datastore.subpostgres import PostgresService,\
+ DiagnosticConnectionWrapper
+
+def allInstancesOf(cls):
+ for o in gc.get_referrers(cls):
+ if isinstance(o, cls):
+ yield o
+
+
+
+def dumpConnectionStatus():
+ print '+++ ALL CONNECTIONS +++'
+ for connection in allInstancesOf(DiagnosticConnectionWrapper):
+ print connection.label, connection.state
+ print '--- CONNECTIONS END ---'
+
+class SQLStoreBuilder(object):
+ """
+ Test-fixture-builder which can construct a PostgresStore.
+ """
+ sharedService = None
+ currentTestID = None
+
+ SHARED_DB_PATH = "../_test_postgres_db"
+
+ def buildStore(self, testCase, notifierFactory):
+ """
+ Do the necessary work to build a store for a particular test case.
+
+ @return: a L{Deferred} which fires with an L{IDataStore}.
+ """
+ currentTestID = testCase.id()
+ dbRoot = CachingFilePath(self.SHARED_DB_PATH)
+ if self.sharedService is None:
+ ready = Deferred()
+ def getReady(connectionFactory):
+ attachmentRoot = dbRoot.child("attachments")
+ try:
+ attachmentRoot.createDirectory()
+ except OSError:
+ pass
+ try:
+ self.store = CommonDataStore(
+ lambda label=None: connectionFactory(
+ label or currentTestID
+ ),
+ notifierFactory,
+ attachmentRoot
+ )
+ except:
+ ready.errback()
+ raise
+ else:
+ self.cleanDatabase(testCase)
+ ready.callback(self.store)
+ return self.store
+ self.sharedService = PostgresService(
+ dbRoot, getReady, v1_schema, "caldav", resetSchema=True,
+ testMode=True
+ )
+ self.sharedService.startService()
+ def startStopping():
+ log.msg("Starting stopping.")
+ self.sharedService.unpauseMonitor()
+ return self.sharedService.stopService()
+ reactor.addSystemEventTrigger(#@UndefinedVariable
+ "before", "shutdown", startStopping)
+ result = ready
+ else:
+ self.store.notifierFactory = notifierFactory
+ self.cleanDatabase(testCase)
+ result = succeed(self.store)
+
+ def cleanUp():
+ # FIXME: clean up any leaked connections and report them with an
+ # immediate test failure.
+ def stopit():
+ self.sharedService.pauseMonitor()
+ return deferLater(reactor, 0.1, stopit)
+ testCase.addCleanup(cleanUp)
+ return result
+
+
+ def cleanDatabase(self, testCase):
+ cleanupConn = self.store.connectionFactory(
+ "%s schema-cleanup" % (testCase.id(),)
+ )
+ cursor = cleanupConn.cursor()
+ tables = ['INVITE',
+ 'RESOURCE_PROPERTY',
+ 'ATTACHMENT',
+ 'ADDRESSBOOK_OBJECT',
+ 'CALENDAR_OBJECT',
+ 'CALENDAR_BIND',
+ 'ADDRESSBOOK_BIND',
+ 'CALENDAR',
+ 'ADDRESSBOOK',
+ 'CALENDAR_HOME',
+ 'ADDRESSBOOK_HOME',
+ 'NOTIFICATION',
+ 'NOTIFICATION_HOME']
+ for table in tables:
+ try:
+ cursor.execute("delete from "+table)
+ except:
+ log.err()
+ cleanupConn.commit()
+ cleanupConn.close()
Modified: CalendarServer/branches/generic-sqlstore/txdav/common/inotifications.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/common/inotifications.py 2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txdav/common/inotifications.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -152,7 +152,7 @@
An notification object describes an XML notification.
"""
- def setData(uid, xmltype, xmldata):
+ def setData(uid, xmltype, xmldata, inserting=False):
"""
Rewrite this notification object to match the given C{xmltype} and
C{xmldata}. C{xmldata} must have the same UID as this notification object.
Modified: CalendarServer/branches/generic-sqlstore/txdav/datastore/file.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/datastore/file.py 2010-08-25 20:45:09 UTC (rev 6183)
+++ CalendarServer/branches/generic-sqlstore/txdav/datastore/file.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -46,35 +46,6 @@
return path.sibling('.' + path.basename())
-_unset = object()
-
-class cached(object):
- """
- This object is a decorator for a 0-argument method which should be called
- only once, and its result cached so that future invocations just return the
- same result without calling the underlying method again.
-
- @ivar thunk: the function to call to generate a cached value.
- """
-
- def __init__(self, thunk):
- self.thunk = thunk
-
-
- def __get__(self, oself, owner):
- def inner():
- cacheKey = "_" + self.thunk.__name__ + "_cached"
- cached = getattr(oself, cacheKey, _unset)
- if cached is _unset:
- value = self.thunk(oself)
- setattr(oself, cacheKey, value)
- return value
- else:
- return cached
- return inner
-
-
-
def writeOperation(thunk):
# FIXME: tests
def inner(self, *a, **kw):
Added: CalendarServer/branches/generic-sqlstore/txdav/datastore/util.py
===================================================================
--- CalendarServer/branches/generic-sqlstore/txdav/datastore/util.py (rev 0)
+++ CalendarServer/branches/generic-sqlstore/txdav/datastore/util.py 2010-08-26 00:52:30 UTC (rev 6184)
@@ -0,0 +1,47 @@
+# -*- test-case-name: txcaldav.calendarstore.test.test_file -*-
+##
+# 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.
+##
+
+"""
+Common utility functions for a datastores.
+"""
+
+_unset = object()
+
+class cached(object):
+ """
+ This object is a decorator for a 0-argument method which should be called
+ only once, and its result cached so that future invocations just return the
+ same result without calling the underlying method again.
+
+ @ivar thunk: the function to call to generate a cached value.
+ """
+
+ def __init__(self, thunk):
+ self.thunk = thunk
+
+
+ def __get__(self, oself, owner):
+ def inner():
+ cacheKey = "_" + self.thunk.__name__ + "_cached"
+ cached = getattr(oself, cacheKey, _unset)
+ if cached is _unset:
+ value = self.thunk(oself)
+ setattr(oself, cacheKey, value)
+ return value
+ else:
+ return cached
+ return inner
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100825/1c6664f5/attachment-0001.html>
More information about the calendarserver-changes
mailing list