[CalendarServer-changes] [11639] CalendarServer/branches/users/cdaboo/json

source_changes at macosforge.org source_changes at macosforge.org
Mon Aug 26 09:43:05 PDT 2013


Revision: 11639
          http://trac.calendarserver.org//changeset/11639
Author:   cdaboo at apple.com
Date:     2013-08-26 09:43:05 -0700 (Mon, 26 Aug 2013)
Log Message:
-----------
Support content-negotiated variants of calendar and contact data in requests and responses.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/caldavxml.py
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/carddavxml.py
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/ical.py
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/get.py
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/propfind.py
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_calendar_query.py
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_common.py
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_freebusy.py
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/resource.py
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/scheduling_store/caldav/resource.py
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/stdconfig.py
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/storebridge.py
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_addressbookmultiget.py
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_caldavxml.py
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_localization.py
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_multiget.py
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_props.py
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/timezonestdservice.py
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/util.py
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/vcard.py
    CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/caldav/test/test_scheduler.py
    CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/imip/test/test_delivery.py
    CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/imip/test/test_inbound.py
    CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/implicit.py
    CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/resource.py
    CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/scheduler.py
    CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/test/test_resource.py
    CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/scheduler.py

Added Paths:
-----------
    CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_util.py

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/caldavxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/caldavxml.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/caldavxml.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -135,18 +135,97 @@
 
 
 
-class CalDAVTimeZoneElement (CalDAVTextElement):
+class CalDAVDataMixin(object):
     """
-    CalDAV element containing iCalendar data with a single VTIMEZONE component.
+    A mixin to support accept/returning data in various formats.
     """
+
+    def __init__(self, *children, **attributes):
+
+        if "content-type" in attributes:
+            self.content_type = attributes["content-type"]
+        else:
+            self.content_type = "text/calendar"
+
+        if "version" in attributes:
+            self.version = attributes["version"]
+        else:
+            self.version = "2.0"
+
+        super(CalDAVDataMixin, self).__init__(*children, **attributes)
+
+
+    def verifyTypeVersion(self):
+        """
+        Make sure any content-type and version matches at least one supported set.
+
+        @return: C{True} if there is at least one match, C{False} otherwise.
+        """
+        allowedTypes = set()
+        allowedTypes.add(("text/calendar", "2.0",))
+        if config.EnableJSONData:
+            allowedTypes.add(("application/calendar+json", "2.0",))
+        for format, version in allowedTypes:
+            if (format == self.content_type) and (version == self.version):
+                return True
+
+        return False
+
+
+    @classmethod
+    def fromCalendar(clazz, calendar, format=None):
+        attrs = {}
+        if format is not None and format != "text/calendar":
+            attrs["content-type"] = format
+
+        if isinstance(calendar, str):
+            if not calendar:
+                raise ValueError("Missing calendar data")
+            return clazz(PCDATAElement(calendar), **attrs)
+        elif isinstance(calendar, iComponent):
+            assert calendar.name() == "VCALENDAR", "Not a calendar: %r" % (calendar,)
+            return clazz(PCDATAElement(calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference, format=format)), **attrs)
+        else:
+            raise ValueError("Not a calendar: %s" % (calendar,))
+
+    fromTextData = fromCalendar
+    fromComponent = fromCalendar
+
     def calendar(self):
         """
-        Returns a calendar component derived from this element, which contains
-        exactly one VTIMEZONE component.
+        Returns a calendar component derived from this element.
         """
-        return iComponent.fromString(str(self))
+        data = self.calendarData()
+        if data:
+            return iComponent.fromString(data, format=self.content_type)
+        else:
+            return None
 
+    generateComponent = calendar
 
+
+    def calendarData(self):
+        """
+        Returns the calendar data derived from this element.
+        """
+        for data in self.children:
+            if not isinstance(data, PCDATAElement):
+                return None
+            else:
+                # We guaranteed in __init__() that there is only one child...
+                break
+
+        return str(data)
+
+    textData = calendarData
+
+
+
+class CalDAVTimeZoneElement (CalDAVDataMixin, CalDAVTextElement):
+    """
+    CalDAV element containing iCalendar data with a single VTIMEZONE component.
+    """
+
     def gettimezone(self):
         """
         Get the timezone to use. If none, return UTC timezone.
@@ -227,8 +306,13 @@
     name = "calendar-timezone"
     hidden = True
 
+    allowed_attributes = {
+        "content-type": False,
+        "version"     : False,
+    }
 
 
+
 @registerElement
 class SupportedCalendarComponentSets (CalDAVElement):
     """
@@ -424,7 +508,7 @@
 
 
 @registerElement
-class CalendarData (CalDAVElement):
+class CalendarData (CalDAVDataMixin, CalDAVElement):
     """
     Defines which parts of a calendar component object should be returned by a
     report.
@@ -444,21 +528,6 @@
         "version"     : False,
     }
 
-    @classmethod
-    def fromCalendar(clazz, calendar):
-        if isinstance(calendar, str):
-            if not calendar:
-                raise ValueError("Missing calendar data")
-            return clazz(PCDATAElement(calendar))
-        elif isinstance(calendar, iComponent):
-            assert calendar.name() == "VCALENDAR", "Not a calendar: %r" % (calendar,)
-            return clazz(PCDATAElement(calendar.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference)))
-        else:
-            raise ValueError("Not a calendar: %s" % (calendar,))
-
-    fromTextData = fromCalendar
-
-
     def __init__(self, *children, **attributes):
         super(CalendarData, self).__init__(*children, **attributes)
 
@@ -511,61 +580,8 @@
                 # optimize them originals away
                 self.children = (data,)
 
-        if "content-type" in attributes:
-            self.content_type = attributes["content-type"]
-        else:
-            self.content_type = "text/calendar"
 
-        if "version" in attributes:
-            self.version = attributes["version"]
-        else:
-            self.version = "2.0"
 
-
-    def verifyTypeVersion(self, types_and_versions):
-        """
-        Make sure any content-type and version matches at least one of the supplied set.
-
-        @param types_and_versions: a list of (content-type, version) tuples to test against.
-        @return:                   True if there is at least one match, False otherwise.
-        """
-        for item in types_and_versions:
-            if (item[0] == self.content_type) and (item[1] == self.version):
-                return True
-
-        return False
-
-
-    def calendar(self):
-        """
-        Returns a calendar component derived from this element.
-        """
-        data = self.calendarData()
-        if data:
-            return iComponent.fromString(data)
-        else:
-            return None
-
-    generateComponent = calendar
-
-
-    def calendarData(self):
-        """
-        Returns the calendar data derived from this element.
-        """
-        for data in self.children:
-            if not isinstance(data, PCDATAElement):
-                return None
-            else:
-                # We guaranteed in __init__() that there is only one child...
-                break
-
-        return str(data)
-
-    textData = calendarData
-
-
-
 @registerElement
 class CalendarComponent (CalDAVElement):
     """
@@ -878,8 +894,13 @@
     """
     name = "timezone"
 
+    allowed_attributes = {
+        "content-type": False,
+        "version"     : False,
+    }
 
 
+
 @registerElement
 class TimeRange (CalDAVTimeRangeElement):
     """

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/carddavxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/carddavxml.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/carddavxml.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -30,6 +30,7 @@
 from txdav.xml.element import WebDAVEmptyElement, WebDAVTextElement
 from txdav.xml.element import ResourceType, Collection
 
+from twistedcaldav.config import config
 from twistedcaldav.vcard import Component
 
 ##
@@ -67,6 +68,92 @@
 
 
 
+class CardDAVDataMixin(object):
+    """
+    A mixin to support accept/returning data in various formats.
+    """
+
+    def __init__(self, *children, **attributes):
+
+        if "content-type" in attributes:
+            self.content_type = attributes["content-type"]
+        else:
+            self.content_type = "text/vcard"
+
+        if "version" in attributes:
+            self.version = attributes["version"]
+        else:
+            self.version = "3.0"
+
+        super(CardDAVDataMixin, self).__init__(*children, **attributes)
+
+
+    def verifyTypeVersion(self):
+        """
+        Make sure any content-type and version matches at least one supported set.
+
+        @return: C{True} if there is at least one match, C{False} otherwise.
+        """
+        allowedTypes = set()
+        allowedTypes.add(("text/vcard", "3.0",))
+        if config.EnableJSONData:
+            allowedTypes.add(("application/vcard+json", "3.0",))
+        for format, version in allowedTypes:
+            if (format == self.content_type) and (version == self.version):
+                return True
+
+        return False
+
+
+    @classmethod
+    def fromAddress(clazz, address, format=None):
+        attrs = {}
+        if format is not None and format != "text/vcard":
+            attrs["content-type"] = format
+
+        if isinstance(address, str):
+            if not address:
+                raise ValueError("Missing address data")
+            return clazz(PCDATAElement(address), **attrs)
+        elif isinstance(address, Component):
+            assert address.name() == "VCARD", "Not a vCard: %r" % (address,)
+            return clazz(PCDATAElement(address.getText(format)), **attrs)
+        else:
+            raise ValueError("Not an address: %s" % (address,))
+
+    fromTextData = fromAddress
+    fromComponent = fromAddress
+
+    def address(self):
+        """
+        Returns an address component derived from this element.
+        """
+        data = self.addressData()
+        if data:
+            return Component.fromString(data, format=self.content_type)
+        else:
+            return None
+
+    generateComponent = address
+
+
+    def addressData(self):
+        """
+        Returns the address data derived from this element.
+        """
+        for data in self.children:
+            if not isinstance(data, PCDATAElement):
+                return None
+            else:
+                # We guaranteed in __init__() that there is only one child...
+                break
+
+        return str(data)
+
+    textData = addressData
+
+
+
 @registerElement
 class AddressBookHomeSet (CardDAVElement):
     """
@@ -201,7 +288,7 @@
 
 
 @registerElement
-class AddressData (CardDAVElement):
+class AddressData (CardDAVDataMixin, CardDAVElement):
     """
     Defines which parts of a address component object should be returned by a
     report.
@@ -219,23 +306,6 @@
         "version"     : False,
     }
 
-    @classmethod
-    def fromAddress(clazz, address):
-        assert address.name() == "VCARD", "Not a vCard: %r" % (address,)
-        return clazz(PCDATAElement(str(address)))
-
-
-    @classmethod
-    def fromAddressData(clazz, addressdata):
-        """
-        Return a AddressData element comprised of the supplied address data.
-        @param addressdata: a string of valid address data.
-        @return: a L{Addressata} element.
-        """
-        return clazz(PCDATAElement(addressdata))
-
-    fromTextData = fromAddressData
-
     def __init__(self, *children, **attributes):
         super(AddressData, self).__init__(*children, **attributes)
 
@@ -284,60 +354,8 @@
                 # optimize them originals away
                 self.children = (data,)
 
-        if "content-type" in attributes:
-            self.content_type = attributes["content-type"]
-        else:
-            self.content_type = "text/vcard"
 
-        if "version" in attributes:
-            self.version = attributes["version"]
-        else:
-            self.version = "3.0"
 
-
-    def verifyTypeVersion(self, types_and_versions):
-        """
-        Make sure any content-type and version matches at least one of the supplied set.
-
-        @param types_and_versions: a list of (content-type, version) tuples to test against.
-        @return:                   True if there is at least one match, False otherwise.
-        """
-        for item in types_and_versions:
-            if (item[0] == self.content_type) and (item[1] == self.version):
-                return True
-
-        return False
-
-
-    def address(self):
-        """
-        Returns an address component derived from this element.
-        """
-        data = self.addressData()
-        if data:
-            return Component.fromString(data)
-        else:
-            return None
-
-    generateComponent = address
-
-    def addressData(self):
-        """
-        Returns an address component derived from this element.
-        """
-        for data in self.children:
-            if not isinstance(data, PCDATAElement):
-                return None
-            else:
-                # We guaranteed in __init__() that there is only one child...
-                break
-
-        return str(data)
-
-    textData = addressData
-
-
-
 @registerElement
 class AllProperties (CardDAVEmptyElement):
     """

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/ical.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/ical.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -28,7 +28,6 @@
     "tzexpand",
 ]
 
-import cStringIO as StringIO
 import codecs
 from difflib import unified_diff
 import heapq
@@ -376,66 +375,95 @@
     # Hidden instance.
     HIDDEN_INSTANCE_PROPERTY = "X-CALENDARSERVER-HIDDEN-INSTANCE"
 
+    allowedTypesList = None
+
+
     @classmethod
-    def allFromString(clazz, string):
+    def allowedTypes(cls):
+        if cls.allowedTypesList is None:
+            cls.allowedTypesList = ["text/calendar"]
+            if config.EnableJSONData:
+                cls.allowedTypesList.append("application/calendar+json")
+        return cls.allowedTypesList
+
+
+    @classmethod
+    def allFromString(clazz, string, format=None):
         """
         Just default to reading a single VCALENDAR
         """
-        return clazz.fromString(string)
+        return clazz.fromString(string, format)
 
 
     @classmethod
-    def allFromStream(clazz, stream):
+    def allFromStream(clazz, stream, format=None):
         """
         Just default to reading a single VCALENDAR
         """
-        return clazz.fromStream(stream)
+        return clazz.fromStream(stream, format)
 
 
     @classmethod
-    def fromString(clazz, string):
+    def fromString(clazz, string, format=None):
         """
         Construct a L{Component} from a string.
         @param string: a string containing iCalendar data.
         @return: a L{Component} representing the first component described by
             C{string}.
         """
-        if type(string) is unicode:
-            string = string.encode("utf-8")
-        else:
-            # Valid utf-8 please
-            string.decode("utf-8")
+        return clazz._fromData(string, False, format)
 
-        # No BOMs please
-        if string[:3] == codecs.BOM_UTF8:
-            string = string[3:]
 
-        return clazz.fromStream(StringIO.StringIO(string))
+    @classmethod
+    def fromStream(clazz, stream, format=None):
+        """
+        Construct a L{Component} from a stream.
+        @param stream: a C{read()}able stream containing iCalendar data.
+        @return: a L{Component} representing the first component described by
+            C{stream}.
+        """
+        return clazz._fromData(stream, True, format)
 
 
     @classmethod
-    def fromStream(clazz, stream):
+    def _fromData(clazz, data, isstream, format=None):
         """
         Construct a L{Component} from a stream.
         @param stream: a C{read()}able stream containing iCalendar data.
+        @param format: a C{str} indicating whether the data is iCalendar or jCal
         @return: a L{Component} representing the first component described by
             C{stream}.
         """
-        cal = Calendar()
+
+        if isstream:
+            pass
+        else:
+            if type(data) is unicode:
+                data = data.encode("utf-8")
+            else:
+                # Valid utf-8 please
+                data.decode("utf-8")
+
+            # No BOMs please
+            if data[:3] == codecs.BOM_UTF8:
+                data = data[3:]
+
         errmsg = "Unknown"
         try:
-            result = cal.parse(stream)
+            result = Calendar.parseData(data, format)
         except ErrorBase, e:
             errmsg = "%s: %s" % (e.mReason, e.mData,)
             result = None
         if not result:
-            stream.seek(0)
-            raise InvalidICalendarDataError("%s\n%s" % (errmsg, stream.read(),))
-        return clazz(None, pycalendar=cal)
+            if isstream:
+                data.seek(0)
+                data = data.read()
+            raise InvalidICalendarDataError("%s\n%s" % (errmsg, data,))
+        return clazz(None, pycalendar=result)
 
 
     @classmethod
-    def fromIStream(clazz, stream):
+    def fromIStream(clazz, stream, format=None):
         """
         Construct a L{Component} from a stream.
         @param stream: an L{IStream} containing iCalendar data.
@@ -449,7 +477,7 @@
         #   request stream.
         #
         def parse(data):
-            return clazz.fromString(data)
+            return clazz.fromString(data, format)
         return allDataFromStream(IStream(stream), parse)
 
 
@@ -543,13 +571,20 @@
         return self._pycalendar == other._pycalendar
 
 
-    def getTextWithTimezones(self, includeTimezones):
+    def getText(self, format=None):
+        return self.getTextWithTimezones(False, format)
+
+
+    def getTextWithTimezones(self, includeTimezones, format=None):
         """
-        Return text representation and include timezones if the option is on
+        Return text representation and include timezones if the option is on.
         """
         assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
 
-        return self._pycalendar.getText(includeTimezones=includeTimezones)
+        result = self._pycalendar.getText(includeTimezones=includeTimezones, format=format)
+        if result is None:
+            raise ValueError("Unknown format requested for calendar data.")
+        return result
 
 
     # FIXME: Should this not be in __eq__?

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/get.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/get.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/get.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -25,7 +25,7 @@
 from txdav.xml import element as davxml
 from twext.web2.dav.http import ErrorResponse
 from twext.web2.dav.util import parentForURL
-from twext.web2.http import HTTPError
+from twext.web2.http import HTTPError, StatusResponse
 from twext.web2.http import Response
 from twext.web2.http_headers import MimeType
 from twext.web2.stream import MemoryStream
@@ -34,8 +34,10 @@
 from twistedcaldav.customxml import calendarserver_namespace
 from twistedcaldav.datafilters.hiddeninstance import HiddenInstanceFilter
 from twistedcaldav.datafilters.privateevents import PrivateEventFilter
+from twistedcaldav.ical import Component
 from twistedcaldav.resource import isPseudoCalendarCollectionResource, \
     CalDAVResource
+from twistedcaldav.util import bestAcceptType
 
 @inlineCallbacks
 def http_GET(self, request):
@@ -70,6 +72,8 @@
                 returnValue(response)
 
         else:
+            # FIXME: this should be implemented in storebridge.CalendarObject.render
+
             # Look for calendar access restriction on existing resource.
             parentURL = parentForURL(request.uri)
             parent = (yield request.locateResource(parentURL))
@@ -78,6 +82,11 @@
                 # Check authorization first
                 yield self.authorize(request, (davxml.Read(),))
 
+                # Accept header handling
+                accepted_type = bestAcceptType(request.headers.getHeader("accept"), Component.allowedTypes())
+                if accepted_type is None:
+                    raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type"))
+
                 caldata = (yield self.iCalendarForUser(request))
 
                 # Filter any attendee hidden instances
@@ -92,8 +101,8 @@
                     caldata = PrivateEventFilter(self.accessMode, isowner).filter(caldata)
 
                 response = Response()
-                response.stream = MemoryStream(caldata.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference))
-                response.headers.setHeader("content-type", MimeType.fromString("text/calendar; charset=utf-8"))
+                response.stream = MemoryStream(caldata.getTextWithTimezones(includeTimezones=not config.EnableTimezonesByReference, format=accepted_type))
+                response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (accepted_type,)))
 
                 # Add Schedule-Tag header if property is present
                 if self.scheduleTag:

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/propfind.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/propfind.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/propfind.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -98,7 +98,7 @@
             search_properties = "names"
         elif isinstance(container, davxml.PropertyContainer):
             properties = container.children
-            search_properties = [(p.namespace, p.name) for p in properties]
+            search_properties = properties
         else:
             raise AssertionError("Unexpected element type in %s: %s"
                                  % (davxml.PropertyFind.sname(), container))
@@ -245,7 +245,11 @@
 # Utilities
 ##
 
-def propertyName(name):
+def propertyName(prop):
+    if type(prop) is tuple:
+        name = prop
+    else:
+        name = prop.qname()
     property_namespace, property_name = name
     pname = davxml.WebDAVUnknownElement()
     pname.namespace = property_namespace

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_calendar_query.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_calendar_query.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_calendar_query.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -32,7 +32,8 @@
 from twext.web2.http import HTTPError, StatusResponse
 
 from twistedcaldav import caldavxml
-from twistedcaldav.caldavxml import caldav_namespace, MaxInstances
+from twistedcaldav.caldavxml import caldav_namespace, MaxInstances, \
+    CalendarTimeZone
 from twistedcaldav.config import config
 from txdav.common.icommondatastore import IndexedSearchException, \
     ConcurrentModification
@@ -171,10 +172,10 @@
         if calresource.isPseudoCalendarCollection():
             # Get the timezone property from the collection if one was not set in the query,
             # and store in the query filter for later use
-            has_prop = (yield calresource.hasProperty((caldav_namespace, "calendar-timezone"), request))
+            has_prop = (yield calresource.hasProperty(CalendarTimeZone(), request))
             timezone = query_timezone
             if query_tz is None and has_prop:
-                tz = (yield calresource.readProperty((caldav_namespace, "calendar-timezone"), request))
+                tz = (yield calresource.readProperty(CalendarTimeZone(), request))
                 filter.settimezone(tz)
                 timezone = tuple(tz.calendar().subcomponents())[0]
 
@@ -233,9 +234,9 @@
                 parent = (yield calresource.locateParent(request, uri))
                 assert parent is not None and parent.isPseudoCalendarCollection()
 
-                has_prop = (yield parent.hasProperty((caldav_namespace, "calendar-timezone"), request))
+                has_prop = (yield parent.hasProperty(CalendarTimeZone(), request))
                 if has_prop:
-                    tz = (yield parent.readProperty((caldav_namespace, "calendar-timezone"), request))
+                    tz = (yield parent.readProperty(CalendarTimeZone(), request))
                     filter.settimezone(tz)
                     timezone = tuple(tz.calendar().subcomponents())[0]
 

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_common.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_common.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -52,7 +52,7 @@
 
 from twistedcaldav import caldavxml
 from twistedcaldav import carddavxml
-from twistedcaldav.caldavxml import caldav_namespace, CalendarData, TimeRange
+from twistedcaldav.caldavxml import CalendarData, CalendarTimeZone, TimeRange
 from twistedcaldav.carddavxml import AddressData
 from twistedcaldav.config import config
 from twistedcaldav.datafilters.calendardata import CalendarDataFilter
@@ -299,7 +299,7 @@
     generate_calendar_data = False
     for property in prop.children:
         if isinstance(property, caldavxml.CalendarData):
-            if not property.verifyTypeVersion([("text/calendar", "2.0")]):
+            if not property.verifyTypeVersion():
                 result = False
                 message = "Calendar-data element type/version not supported: content-type: %s, version: %s" % (property.content_type, property.version)
             generate_calendar_data = True
@@ -324,7 +324,7 @@
     generate_address_data = False
     for property in prop.children:
         if isinstance(property, carddavxml.AddressData):
-            if not property.verifyTypeVersion([("text/vcard", "3.0")]):
+            if not property.verifyTypeVersion():
                 result = False
                 message = "Address-data element type/version not supported: content-type: %s, version: %s" % (property.content_type, property.version)
             generate_address_data = True
@@ -371,7 +371,7 @@
             filtered = HiddenInstanceFilter().filter(calendar)
             filtered = PrivateEventFilter(resource.accessMode, isowner).filter(filtered)
             filtered = CalendarDataFilter(property, timezone).filter(filtered)
-            propvalue = CalendarData().fromCalendar(filtered)
+            propvalue = CalendarData.fromCalendar(filtered, format=property.content_type)
             properties_by_status[responsecode.OK].append(propvalue)
             continue
 
@@ -379,7 +379,7 @@
             if vcard is None:
                 vcard = (yield resource.vCard())
             filtered = AddressDataFilter(property).filter(vcard)
-            propvalue = AddressData().fromAddress(filtered)
+            propvalue = AddressData.fromAddress(filtered, format=property.content_type)
             properties_by_status[responsecode.OK].append(propvalue)
             continue
 
@@ -392,7 +392,7 @@
 
         if has:
             try:
-                prop = (yield resource.readProperty(qname, request))
+                prop = (yield resource.readProperty(property, request))
                 if prop is not None:
                     properties_by_status[responsecode.OK].append(prop)
                 elif not returnMinimal:
@@ -515,9 +515,9 @@
         useruid = ""
 
     # Get the timezone property from the collection.
-    has_prop = (yield calresource.hasProperty((caldav_namespace, "calendar-timezone"), request))
+    has_prop = (yield calresource.hasProperty(CalendarTimeZone(), request))
     if has_prop:
-        tz = (yield calresource.readProperty((caldav_namespace, "calendar-timezone"), request))
+        tz = (yield calresource.readProperty(CalendarTimeZone(), request))
     else:
         tz = None
 

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_freebusy.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_freebusy.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/method/report_freebusy.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -31,7 +31,9 @@
 from twext.web2.stream import MemoryStream
 
 from twistedcaldav import caldavxml
+from twistedcaldav.ical import Component
 from twistedcaldav.method import report_common
+from twistedcaldav.util import bestAcceptType
 
 from txdav.caldav.icalendarstore import TimeRangeLowerLimit, TimeRangeUpperLimit
 from txdav.xml import element as davxml
@@ -60,6 +62,11 @@
 
     matchcount = [0]
 
+    accepted_type = bestAcceptType(request.headers.getHeader("accept"), Component.allowedTypes())
+    if accepted_type is None:
+        raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type"))
+
+
     def generateFreeBusyInfo(calresource, uri): #@UnusedVariable
         """
         Run a free busy report on the specified calendar collection
@@ -104,7 +111,7 @@
     fbcalendar = report_common.buildFreeBusyResult(fbinfo, timerange)
 
     response = Response()
-    response.stream = MemoryStream(str(fbcalendar))
-    response.headers.setHeader("content-type", MimeType.fromString("text/calendar; charset=utf-8"))
+    response.stream = MemoryStream(fbcalendar.getText(accepted_type))
+    response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (accepted_type,)))
 
     returnValue(response)

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/resource.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/resource.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -257,10 +257,11 @@
                 # Redirect to include trailing '/' in URI
                 return RedirectResponse(request.unparseURL(path=urllib.quote(urllib.unquote(request.path), safe=':/') + '/'))
 
-            def _defer(data):
+            def _defer(result):
+                data, accepted_type = result
                 response = Response()
-                response.stream = MemoryStream(str(data))
-                response.headers.setHeader("content-type", MimeType.fromString("text/calendar"))
+                response.stream = MemoryStream(data.getText(accepted_type))
+                response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (accepted_type,)))
                 return response
 
             d = self.iCalendarRolledup(request)
@@ -365,7 +366,7 @@
                                   (self,))
 
 
-    def storeStream(self, stream):
+    def storeStream(self, stream, format):
         """
         Store the content of the stream in this resource, as it would via a PUT.
 
@@ -375,8 +376,7 @@
         @return: a L{Deferred} which fires with an HTTP response.
         @rtype: L{Deferred}
         """
-        raise NotImplementedError("%s does not implement storeStream" %
-                                  (self,))
+        raise NotImplementedError("%s does not implement storeStream" % (self,))
 
     # End transitional new-store interface
 
@@ -595,12 +595,21 @@
             returnValue(self.getSupportedComponentSet())
 
         elif qname == caldavxml.SupportedCalendarData.qname() and self.isPseudoCalendarCollection():
-            returnValue(caldavxml.SupportedCalendarData(
+            dataTypes = []
+            dataTypes.append(
                 caldavxml.CalendarData(**{
                     "content-type": "text/calendar",
                     "version"     : "2.0",
                 }),
-            ))
+            )
+            if config.EnableJSONData:
+                dataTypes.append(
+                    caldavxml.CalendarData(**{
+                        "content-type": "application/calendar+json",
+                        "version"     : "2.0",
+                    }),
+                )
+            returnValue(caldavxml.SupportedCalendarData(*dataTypes))
 
         elif qname == caldavxml.MaxResourceSize.qname() and self.isPseudoCalendarCollection():
             if config.MaxResourceSize:
@@ -632,12 +641,21 @@
 
         elif qname == carddavxml.SupportedAddressData.qname() and self.isAddressBookCollection():
             # CardDAV, section 6.2.2
-            returnValue(carddavxml.SupportedAddressData(
+            dataTypes = []
+            dataTypes.append(
                 carddavxml.AddressDataType(**{
                     "content-type": "text/vcard",
                     "version"     : "3.0",
                 }),
-            ))
+            )
+            if config.EnableJSONData:
+                dataTypes.append(
+                    carddavxml.AddressDataType(**{
+                        "content-type": "application/vcard+json",
+                        "version"     : "3.0",
+                    }),
+                )
+            returnValue(carddavxml.SupportedAddressData(*dataTypes))
 
         elif qname == carddavxml.MaxResourceSize.qname() and self.isAddressBookCollection() and not self.isDirectoryBackedAddressBookCollection():
             # CardDAV, section 6.2.3

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/scheduling_store/caldav/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/scheduling_store/caldav/resource.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/scheduling_store/caldav/resource.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -407,7 +407,7 @@
         # Check authentication and access controls
         yield self.authorize(request, (caldavxml.ScheduleSend(),))
 
-        calendar = (yield self.loadCalendarFromRequest(request))
+        calendar, format = (yield self.loadCalendarFromRequest(request))
         originator = (yield self.loadOriginatorFromRequestDetails(request))
         recipients = self.loadRecipientsFromCalendarData(calendar)
 
@@ -427,14 +427,25 @@
 
         # Do the POST processing treating
         result = (yield scheduler.doSchedulingViaPOST(originator, recipients, calendar))
-        returnValue(result.response())
+        returnValue(result.response(format=format))
 
 
+    def determineType(self, content_type):
+        """
+        Determine if the supplied content-type is valid for storing and return the matching PyCalendar type.
+        """
+        format = None
+        if content_type is not None:
+            format = "%s/%s" % (content_type.mediaType, content_type.mediaSubtype,)
+        return format if format in Component.allowedTypes() else None
+
+
     @inlineCallbacks
     def loadCalendarFromRequest(self, request):
         # Must be content-type text/calendar
         contentType = request.headers.getHeader("content-type")
-        if contentType is not None and (contentType.mediaType, contentType.mediaSubtype) != ("text", "calendar"):
+        format = self.determineType(contentType)
+        if format is None:
             self.log.error("MIME type %s not allowed in calendar collection" % (contentType,))
             raise HTTPError(ErrorResponse(
                 responsecode.FORBIDDEN,
@@ -444,7 +455,7 @@
 
         # Parse the calendar object from the HTTP request stream
         try:
-            calendar = (yield Component.fromIStream(request.stream))
+            calendar = (yield Component.fromIStream(request.stream, format=format))
         except:
             # FIXME: Bare except
             self.log.error("Error while handling POST: %s" % (Failure(),))
@@ -454,7 +465,7 @@
                 description="Can't parse calendar data"
             ))
 
-        returnValue(calendar)
+        returnValue((calendar, format,))
 
 
     @inlineCallbacks

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/stdconfig.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/stdconfig.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -539,6 +539,11 @@
     "EnableManagedAttachments"    : False, # Support Managed Attachments
 
     #
+    # Generic CalDAV/CardDAV extensions
+    #
+    "EnableJSONData"          : True, # Allow clients to send/receive JSON jCal and jCard format data
+
+    #
     # Non-standard CalDAV extensions
     #
     "EnableDropBox"           : False, # Calendar Drop Box

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/storebridge.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/storebridge.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/storebridge.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -82,6 +82,7 @@
 from twistedcaldav.customxml import calendarserver_namespace
 from twistedcaldav.instance import InvalidOverriddenInstanceError, \
     TooManyInstancesError
+from twistedcaldav.util import bestAcceptType
 import collections
 
 """
@@ -626,14 +627,17 @@
         # Read in all data
         data = (yield allDataFromStream(request.stream))
 
-        components = self.componentsFromData(data)
+        format = request.headers.getHeader("content-type")
+        if format:
+            format = "%s/%s" % (format.mediaType, format.mediaSubtype,)
+        components = self.componentsFromData(data, format)
         if components is None:
             raise HTTPError(StatusResponse(BAD_REQUEST, "Could not parse valid data from request body"))
 
         # Build response
         xmlresponses = [None] * len(components)
         indexedComponents = [idxComponent for idxComponent in enumerate(components)]
-        yield self.bulkCreate(indexedComponents, request, return_changed, xmlresponses)
+        yield self.bulkCreate(indexedComponents, request, return_changed, xmlresponses, format)
 
         result = MultiStatusResponse(xmlresponses)
 
@@ -650,7 +654,7 @@
 
 
     @inlineCallbacks
-    def bulkCreate(self, indexedComponents, request, return_changed, xmlresponses):
+    def bulkCreate(self, indexedComponents, request, return_changed, xmlresponses, format):
         """
         Do create from simpleBatchPOST or crudCreate()
         Subclasses may override
@@ -664,7 +668,7 @@
                 # Get a resource for the new item
                 newchildURL = joinURL(request.path, name)
                 newchild = (yield request.locateResource(newchildURL))
-                changedData = (yield self.storeResourceData(newchild, component, returnChangedData=return_changed))
+                changedComponent = (yield self.storeResourceData(newchild, component, returnChangedData=return_changed))
 
             except HTTPError, e:
                 # Extract the pre-condition
@@ -674,30 +678,30 @@
                     error = (error.namespace, error.name,)
 
                 xmlresponses[index] = (
-                    yield self.bulkCreateResponse(component, newchildURL, newchild, None, code, error)
+                    yield self.bulkCreateResponse(component, newchildURL, newchild, None, code, error, format)
                 )
 
             except Exception:
                 xmlresponses[index] = (
-                    yield self.bulkCreateResponse(component, newchildURL, newchild, None, code=BAD_REQUEST, error=None)
+                    yield self.bulkCreateResponse(component, newchildURL, newchild, None, BAD_REQUEST, None, format)
                 )
 
             else:
                 if not return_changed:
-                    changedData = None
+                    changedComponent = None
                 xmlresponses[index] = (
-                    yield self.bulkCreateResponse(component, newchildURL, newchild, changedData, code=None, error=None)
+                    yield self.bulkCreateResponse(component, newchildURL, newchild, changedComponent, None, None, format)
                 )
 
 
     @inlineCallbacks
-    def bulkCreateResponse(self, component, newchildURL, newchild, changedData, code, error):
+    def bulkCreateResponse(self, component, newchildURL, newchild, changedComponent, code, error, format):
         """
         generate one xmlresponse for bulk create
         """
         if code is None:
             etag = (yield newchild.etag())
-            if changedData is None:
+            if changedComponent is None:
                 returnValue(
                     davxml.PropertyStatusResponse(
                         davxml.HRef.fromString(newchildURL),
@@ -717,7 +721,7 @@
                         davxml.PropertyStatus(
                             davxml.PropertyContainer(
                                 davxml.GETETag.fromString(etag.generate()),
-                                self.xmlDataElementType().fromTextData(changedData),
+                                self.xmlDataElementType().fromComponent(changedComponent, format),
                             ),
                             davxml.Status.fromResponseCode(OK),
                         )
@@ -822,6 +826,7 @@
             for index, xmldata in crudCreateInfo:
 
                 component = xmldata.generateComponent()
+                format = xmldata.content_type
 
                 if hasPrivilege is not True:
                     e = hasPrivilege # use same code pattern as exception
@@ -830,13 +835,13 @@
                         error = e.response.error
                         error = (error.namespace, error.name,)
 
-                    xmlresponse = yield self.bulkCreateResponse(component, None, None, None, code, error)
+                    xmlresponse = yield self.bulkCreateResponse(component, None, None, None, code, error, format)
                     xmlresponses[index] = xmlresponse
 
                 else:
                     indexedComponents.append((index, component,))
 
-            yield self.bulkCreate(indexedComponents, request, return_changed, xmlresponses)
+            yield self.bulkCreate(indexedComponents, request, return_changed, xmlresponses, format)
 
 
     @inlineCallbacks
@@ -847,8 +852,8 @@
             code = None
             error = None
             try:
-                componentdata = xmldata.textData()
                 component = xmldata.generateComponent()
+                format = xmldata.content_type
 
                 updateResource = (yield request.locateResource(href))
                 if not updateResource.exists():
@@ -862,7 +867,7 @@
                 if ifmatch and ifmatch != etag.generate():
                     raise HTTPError(PRECONDITION_FAILED)
 
-                changedData = yield self.storeResourceData(updateResource, component, componentdata)
+                changedComponent = yield self.storeResourceData(updateResource, component, returnChangedData=return_changed)
 
             except HTTPError, e:
                 # Extract the pre-condition
@@ -875,7 +880,7 @@
                 code = BAD_REQUEST
 
             if code is None:
-                if not return_changed or changedData is None:
+                if changedComponent is None:
                     xmlresponses[index] = davxml.PropertyStatusResponse(
                         davxml.HRef.fromString(href),
                         davxml.PropertyStatus(
@@ -891,7 +896,7 @@
                         davxml.PropertyStatus(
                             davxml.PropertyContainer(
                                 davxml.GETETag.fromString(etag.generate()),
-                                self.xmlDataElementType().fromTextData(changedData),
+                                self.xmlDataElementType().fromComponentData(changedComponent, format),
                             ),
                             davxml.Status.fromResponseCode(OK),
                         )
@@ -1053,6 +1058,8 @@
 
         if config.EnableBatchUpload:
             self._postHandlers[("text", "calendar")] = _CommonHomeChildCollectionMixin.simpleBatchPOST
+            if config.EnableJSONData:
+                self._postHandlers[("application", "calendar+json")] = _CommonHomeChildCollectionMixin.simpleBatchPOST
             self.xmlDocHandlers[customxml.Multiput] = _CommonHomeChildCollectionMixin.crudBatchPOST
 
 
@@ -1088,6 +1095,11 @@
     def iCalendarRolledup(self, request):
         # FIXME: uncached: implement cache in the storage layer
 
+        # Accept header handling
+        accepted_type = bestAcceptType(request.headers.getHeader("accept"), Component.allowedTypes())
+        if accepted_type is None:
+            raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type"))
+
         # Generate a monolithic calendar
         calendar = VCalendar("VCALENDAR")
         calendar.addProperty(VProperty("VERSION", "2.0"))
@@ -1138,17 +1150,13 @@
 
                     calendar.addComponent(component)
 
-        # Cache the data
-        data = str(calendar)
-        data = (yield self.getInternalSyncToken()) + "\r\n" + data
+        returnValue((calendar, accepted_type,))
 
-        returnValue(calendar)
-
     createCalendarCollection = _CommonHomeChildCollectionMixin.createCollection
 
 
     @classmethod
-    def componentsFromData(cls, data):
+    def componentsFromData(cls, data, format):
         """
         Need to split a single VCALENDAR into separate ones based on UID with the
         appropriate VTIEMZONES included.
@@ -1158,7 +1166,7 @@
 
         # Split into components by UID and TZID
         try:
-            vcal = VCalendar.fromString(data)
+            vcal = VCalendar.fromString(data, format)
         except InvalidICalendarDataError:
             return None
 
@@ -1242,7 +1250,8 @@
 
         elif qname == caldavxml.CalendarTimeZone.qname():
             timezone = self._newStoreObject.getTimezone()
-            returnValue(caldavxml.CalendarTimeZone.fromString(str(timezone)) if timezone else None)
+            format = property.content_type if isinstance(property, caldavxml.CalendarTimeZone) else None
+            returnValue(caldavxml.CalendarTimeZone.fromCalendar(timezone, format=format) if timezone else None)
 
         result = (yield super(CalendarCollectionResource, self).readProperty(property, request))
         returnValue(result)
@@ -1288,7 +1297,7 @@
         yield newchild.storeComponent(component)
         if returnChangedData and newchild._newStoreObject._componentChanged:
             result = (yield newchild.componentForUser())
-            returnValue(str(result))
+            returnValue(result)
         else:
             returnValue(None)
 
@@ -2210,16 +2219,38 @@
         return self._newStoreObject.component()
 
 
+    def allowedTypes(self):
+        """
+        Return a dict of allowed MIME types for storing, mapped to equivalent PyCalendar types.
+        """
+        raise NotImplementedError
+
+
+    def determineType(self, content_type):
+        """
+        Determine if the supplied content-type is valid for storing and return the matching PyCalendar type.
+        """
+        format = None
+        if content_type is not None:
+            format = "%s/%s" % (content_type.mediaType, content_type.mediaSubtype,)
+        return format if format in self.allowedTypes() else None
+
+
     @inlineCallbacks
     def render(self, request):
         if not self.exists():
             log.debug("Resource not found: %s" % (self,))
             raise HTTPError(NOT_FOUND)
 
+        # Accept header handling
+        accepted_type = bestAcceptType(request.headers.getHeader("accept"), self.allowedTypes())
+        if accepted_type is None:
+            raise HTTPError(StatusResponse(responsecode.NOT_ACCEPTABLE, "Cannot generate requested data type"))
+
         output = yield self.component()
 
-        response = Response(OK, {}, str(output))
-        response.headers.setHeader("content-type", self.contentType())
+        response = Response(OK, {}, output.getText(accepted_type))
+        response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (accepted_type,)))
         returnValue(response)
 
     # The following are used to map store exceptions into HTTP error responses
@@ -2344,10 +2375,10 @@
 
 
     @inlineCallbacks
-    def storeStream(self, stream):
+    def storeStream(self, stream, format):
 
         # FIXME: direct tests
-        component = self._componentFromStream((yield allDataFromStream(stream)))
+        component = self._componentFromStream((yield allDataFromStream(stream)), format)
         result = (yield self.storeComponent(component))
         returnValue(result)
 
@@ -2479,6 +2510,13 @@
 
     _componentFromStream = VCalendar.fromString
 
+    def allowedTypes(self):
+        """
+        Return a tuple of allowed MIME types for storing.
+        """
+        return Component.allowedTypes()
+
+
     @inlineCallbacks
     def inNewTransaction(self, request, label=""):
         """
@@ -2600,7 +2638,8 @@
 
         # Content-type check
         content_type = request.headers.getHeader("content-type")
-        if content_type is not None and (content_type.mediaType, content_type.mediaSubtype) != ("text", "calendar"):
+        format = self.determineType(content_type)
+        if format is None:
             log.error("MIME type %s not allowed in calendar collection" % (content_type,))
             raise HTTPError(ErrorResponse(
                 responsecode.FORBIDDEN,
@@ -2628,7 +2667,7 @@
                 ))
 
             try:
-                component = Component.fromString(calendardata)
+                component = Component.fromString(calendardata, format)
             except ValueError, e:
                 log.error(str(e))
                 raise HTTPError(ErrorResponse(
@@ -2899,6 +2938,8 @@
 
         if config.EnableBatchUpload:
             self._postHandlers[("text", "vcard")] = AddressBookCollectionResource.simpleBatchPOST
+            if config.EnableJSONData:
+                self._postHandlers[("application", "vcard+json")] = _CommonHomeChildCollectionMixin.simpleBatchPOST
             self.xmlDocHandlers[customxml.Multiput] = AddressBookCollectionResource.crudBatchPOST
 
 
@@ -2930,9 +2971,9 @@
 
 
     @classmethod
-    def componentsFromData(cls, data):
+    def componentsFromData(cls, data, format):
         try:
-            return VCard.allFromString(data)
+            return VCard.allFromString(data, format)
         except InvalidVCardDataError:
             return None
 
@@ -2953,7 +2994,7 @@
         yield newchild.storeComponent(component)
         if returnChangedData and newchild._newStoreObject._componentChanged:
             result = (yield newchild.component())
-            returnValue(str(result))
+            returnValue(result)
         else:
             returnValue(None)
 
@@ -2994,7 +3035,7 @@
 
 
     @inlineCallbacks
-    def bulkCreate(self, indexedComponents, request, return_changed, xmlresponses):
+    def bulkCreate(self, indexedComponents, request, return_changed, xmlresponses, format):
         """
         bulk create allowing groups to contain member UIDs added during the same bulk create
         """
@@ -3009,7 +3050,7 @@
                 # Get a resource for the new item
                 newchildURL = joinURL(request.path, name)
                 newchild = (yield request.locateResource(newchildURL))
-                changedData = (yield self.storeResourceData(newchild, component, returnChangedData=return_changed))
+                changedComponent = (yield self.storeResourceData(newchild, component, returnChangedData=return_changed))
 
             except GroupWithUnsharedAddressNotAllowedError, e:
                 # save off info and try again below
@@ -3024,20 +3065,20 @@
                     error = (error.namespace, error.name,)
 
                 xmlresponses[index] = (
-                    yield self.bulkCreateResponse(component, newchildURL, newchild, None, code, error)
+                    yield self.bulkCreateResponse(component, newchildURL, newchild, None, code, error, format)
                 )
 
             except Exception:
                 xmlresponses[index] = (
-                    yield self.bulkCreateResponse(component, newchildURL, newchild, None, code=BAD_REQUEST, error=None)
+                    yield self.bulkCreateResponse(component, newchildURL, newchild, None, BAD_REQUEST, None, format)
                 )
 
             else:
                 if not return_changed:
-                    changedData = None
+                    changedComponent = None
                 coaddedUIDs |= set([component.resourceUID()])
                 xmlresponses[index] = (
-                    yield self.bulkCreateResponse(component, newchildURL, newchild, changedData, code=None, error=None)
+                    yield self.bulkCreateResponse(component, newchildURL, newchild, changedComponent, None, None, format)
                 )
 
         if groupRetries:
@@ -3055,7 +3096,7 @@
                 # give FORBIDDEN response
                 index, component, newchildURL, newchild, missingUIDs = groupRetry
                 xmlresponses[index] = (
-                    yield self.bulkCreateResponse(component, newchildURL, newchild, changedData=None, code=FORBIDDEN, error=None)
+                    yield self.bulkCreateResponse(component, newchildURL, newchild, None, FORBIDDEN, None, format)
                 )
                 coaddedUIDs -= set([component.resourceUID()]) # group uid not added
                 groupRetries.remove(groupRetry) # remove this retry
@@ -3065,11 +3106,11 @@
                 newchild._metadata["coaddedUIDs"] = coaddedUIDs
 
                 # don't catch errors, abort the whole transaction
-                changedData = yield self.storeResourceData(newchild, component, returnChangedData=return_changed)
+                changedComponent = yield self.storeResourceData(newchild, component, returnChangedData=return_changed)
                 if not return_changed:
-                    changedData = None
+                    changedComponent = None
                 xmlresponses[index] = (
-                    yield self.bulkCreateResponse(component, newchildURL, newchild, changedData, code=None, error=None)
+                    yield self.bulkCreateResponse(component, newchildURL, newchild, changedComponent, None, None, format)
                 )
 
 
@@ -3164,6 +3205,13 @@
 
     _componentFromStream = VCard.fromString
 
+    def allowedTypes(self):
+        """
+        Return a tuple of allowed MIME types for storing.
+        """
+        return VCard.allowedTypes()
+
+
     @inlineCallbacks
     def vCardText(self):
         data = yield self.vCard()
@@ -3238,7 +3286,8 @@
 
         # Content-type check
         content_type = request.headers.getHeader("content-type")
-        if content_type is not None and (content_type.mediaType, content_type.mediaSubtype) != ("text", "vcard"):
+        format = self.determineType(content_type)
+        if format is None:
             log.error("MIME type %s not allowed in vcard collection" % (content_type,))
             raise HTTPError(ErrorResponse(
                 responsecode.FORBIDDEN,
@@ -3263,7 +3312,7 @@
                 ))
 
             try:
-                component = VCard.fromString(vcarddata)
+                component = VCard.fromString(vcarddata, format)
             except ValueError, e:
                 log.error(str(e))
                 raise HTTPError(ErrorResponse(

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_addressbookmultiget.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_addressbookmultiget.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_addressbookmultiget.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -19,17 +19,19 @@
 from twext.web2 import responsecode
 from twext.web2.iweb import IResponse
 from twext.web2.stream import MemoryStream
-from txdav.xml import element as davxml
 from twext.web2.dav.util import davXMLFromStream, joinURL
+from twext.web2.http_headers import Headers, MimeType
 
 from twistedcaldav import carddavxml
 from twistedcaldav import vcard
-
 from twistedcaldav.config import config
 from twistedcaldav.test.util import StoreTestCase, SimpleStoreRequest
+
 from twisted.python.filepath import FilePath
 from twisted.internet.defer import inlineCallbacks, returnValue
 
+from txdav.xml import element as davxml
+
 class AddressBookMultiget (StoreTestCase):
     """
     addressbook-multiget REPORT
@@ -214,7 +216,13 @@
             '''
             if data:
                 for filename, icaldata in data.iteritems():
-                    request = SimpleStoreRequest(self, "PUT", joinURL(addressbook_uri, filename + ".vcf"), authid="wsanchez")
+                    request = SimpleStoreRequest(
+                        self,
+                        "PUT",
+                        joinURL(addressbook_uri, filename + ".vcf"),
+                        headers=Headers({"content-type": MimeType.fromString("text/vcard")}),
+                        authid="wsanchez"
+                    )
                     request.stream = MemoryStream(icaldata)
                     yield self.send(request)
             else:
@@ -222,7 +230,13 @@
                 for child in FilePath(self.vcards_dir).children():
                     if os.path.splitext(child.basename())[1] != ".vcf":
                         continue
-                    request = SimpleStoreRequest(self, "PUT", joinURL(addressbook_uri, child.basename()), authid="wsanchez")
+                    request = SimpleStoreRequest(
+                        self,
+                        "PUT",
+                        joinURL(addressbook_uri, child.basename()),
+                        headers=Headers({"content-type": MimeType.fromString("text/vcard")}),
+                        authid="wsanchez"
+                    )
                     request.stream = MemoryStream(child.getContent())
                     yield self.send(request)
 

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_caldavxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_caldavxml.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_caldavxml.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -14,9 +14,16 @@
 # limitations under the License.
 ##
 
+import twistedcaldav.test.util
 from twistedcaldav import caldavxml
-import twistedcaldav.test.util
+from twistedcaldav.caldavxml import CalendarData
+from twistedcaldav.ical import normalize_iCalStr, Component
 
+def normalizeJSON(j):
+    return "".join(map(str.strip, j.splitlines())).replace(", ", ",").replace(": ", ":")
+
+
+
 class CustomXML (twistedcaldav.test.util.TestCase):
 
 
@@ -50,3 +57,99 @@
 
         tr = caldavxml.CalDAVTimeRangeElement(start="20110201T120000Z", end="20110202")
         self.assertFalse(tr.valid())
+
+
+    def test_CalendarDataTextAndJSON(self):
+        """
+        Text that we can both parse and generate CalendarData elements with both text and json formats.
+        """
+        dataText = """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DTSTAMP:20080601T120000Z
+EXDATE:20080602T120000Z
+EXDATE:20080603T120000Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY;COUNT=400
+SUMMARY:Test
+END:VEVENT
+END:VCALENDAR
+""".replace("\n", "\r\n")
+
+        dataXML = """<?xml version='1.0' encoding='UTF-8'?>
+<calendar-data xmlns='urn:ietf:params:xml:ns:caldav'><![CDATA[%s]]></calendar-data>""" % (dataText,)
+
+        jsonText = """[
+  "vcalendar",
+  [
+    ["version", {}, "text", "2.0"],
+    ["prodid", {}, "text", "-//CALENDARSERVER.ORG//NONSGML Version 1//EN"]
+  ],
+  [
+    ["vevent",
+      [
+        ["uid", {}, "text", "12345-67890"],
+        ["dtstart", {}, "date-time", "2008-06-01T12:00:00Z"],
+        ["dtend", {}, "date-time", "2008-06-01T13:00:00Z"],
+        ["attendee", {}, "cal-address", "mailto:user1 at example.com"],
+        ["attendee", {}, "cal-address", "mailto:user2 at example.com"],
+        ["dtstamp", {}, "date-time", "2008-06-01T12:00:00Z"],
+        ["exdate", {}, "date-time", "2008-06-02T12:00:00Z"],
+        ["exdate", {}, "date-time", "2008-06-03T12:00:00Z"],
+        ["organizer", {"cn": "User 01"}, "cal-address", "mailto:user1 at example.com"],
+        ["rrule", {}, "recur", {"count": 400, "freq": "DAILY"}],
+        ["summary", {}, "text", "Test"]
+      ],
+      [
+      ]
+    ]
+  ]
+]
+"""
+
+        jsonXML = """<?xml version='1.0' encoding='UTF-8'?>
+<calendar-data content-type='application/calendar+json' xmlns='urn:ietf:params:xml:ns:caldav'><![CDATA[%s]]></calendar-data>""" % (jsonText,)
+
+        cd = CalendarData.fromTextData(dataText)
+        self.assertEqual(normalize_iCalStr(cd.calendar().getTextWithTimezones(True, format="text/calendar")), normalize_iCalStr(dataText))
+        self.assertEqual(normalizeJSON(cd.calendar().getTextWithTimezones(True, format="application/calendar+json")), normalizeJSON(jsonText))
+        self.assertEqual(cd.content_type, "text/calendar")
+        self.assertEqual(cd.toxml(), dataXML)
+
+        comp = Component.fromString(dataText)
+        cd = CalendarData.fromCalendar(comp)
+        self.assertEqual(normalize_iCalStr(cd.calendar().getTextWithTimezones(True, format="text/calendar")), normalize_iCalStr(dataText))
+        self.assertEqual(normalizeJSON(cd.calendar().getTextWithTimezones(True, format="application/calendar+json")), normalizeJSON(jsonText))
+        self.assertEqual(cd.content_type, "text/calendar")
+        self.assertEqual(cd.toxml(), dataXML)
+
+        cd = CalendarData.fromCalendar(comp, format="application/calendar+json")
+        self.assertEqual(normalize_iCalStr(cd.calendar().getTextWithTimezones(True, format="text/calendar")), normalize_iCalStr(dataText))
+        self.assertEqual(normalizeJSON(cd.calendar().getTextWithTimezones(True, format="application/calendar+json")), normalizeJSON(jsonText))
+        self.assertEqual(cd.content_type, "application/calendar+json")
+        self.assertEqual(normalizeJSON(cd.toxml()), normalizeJSON(jsonXML))
+
+        cd = CalendarData.fromTextData(jsonText, format="application/calendar+json")
+        self.assertEqual(normalize_iCalStr(cd.calendar().getTextWithTimezones(True, format="text/calendar")), normalize_iCalStr(dataText))
+        self.assertEqual(normalizeJSON(cd.calendar().getTextWithTimezones(True, format="application/calendar+json")), normalizeJSON(jsonText))
+        self.assertEqual(cd.content_type, "application/calendar+json")
+        self.assertEqual(cd.toxml(), jsonXML)
+
+        comp = Component.fromString(jsonText, format="application/calendar+json")
+        cd = CalendarData.fromCalendar(comp)
+        self.assertEqual(normalize_iCalStr(cd.calendar().getTextWithTimezones(True, format="text/calendar")), normalize_iCalStr(dataText))
+        self.assertEqual(normalizeJSON(cd.calendar().getTextWithTimezones(True, format="application/calendar+json")), normalizeJSON(jsonText))
+        self.assertEqual(cd.content_type, "text/calendar")
+        self.assertEqual(cd.toxml(), dataXML)
+
+        cd = CalendarData.fromCalendar(comp, format="application/calendar+json")
+        self.assertEqual(normalize_iCalStr(cd.calendar().getTextWithTimezones(True, format="text/calendar")), normalize_iCalStr(dataText))
+        self.assertEqual(normalizeJSON(cd.calendar().getTextWithTimezones(True, format="application/calendar+json")), normalizeJSON(jsonText))
+        self.assertEqual(cd.content_type, "application/calendar+json")
+        self.assertEqual(normalizeJSON(cd.toxml()), normalizeJSON(jsonXML))

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_localization.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_localization.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_localization.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -20,6 +20,7 @@
 from twistedcaldav.ical import Component
 from twistedcaldav.test.util import TestCase
 from twistedcaldav.config import ConfigDict
+from twistedcaldav.timezones import TimezoneCache
 from pycalendar.datetime import DateTime
 
 import os
@@ -52,6 +53,11 @@
 
 class LocalizationTests(TestCase):
 
+    def setUp(self):
+        super(LocalizationTests, self).setUp()
+        TimezoneCache.create()
+
+
     def test_BasicStringLocalization(self):
 
         with translationTo('pig', localeDir=localeDir):

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_multiget.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_multiget.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_multiget.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -16,6 +16,7 @@
 from twext.python.filepath import CachingFilePath as FilePath
 from twext.web2 import responsecode
 from twext.web2.dav.util import davXMLFromStream, joinURL
+from twext.web2.http_headers import Headers, MimeType
 from twext.web2.iweb import IResponse
 from twext.web2.stream import MemoryStream
 
@@ -268,7 +269,13 @@
 
             if data:
                 for filename, icaldata in data.iteritems():
-                    request = SimpleStoreRequest(self, "PUT", joinURL(calendar_uri, filename + ".ics"), authid="wsanchez")
+                    request = SimpleStoreRequest(
+                        self,
+                        "PUT",
+                        joinURL(calendar_uri, filename + ".ics"),
+                        headers=Headers({"content-type": MimeType.fromString("text/calendar")}),
+                        authid="wsanchez"
+                    )
                     request.stream = MemoryStream(icaldata)
                     yield self.send(request)
             else:
@@ -276,7 +283,13 @@
                 for child in FilePath(self.holidays_dir).children():
                     if os.path.splitext(child.basename())[1] != ".ics":
                         continue
-                    request = SimpleStoreRequest(self, "PUT", joinURL(calendar_uri, child.basename()), authid="wsanchez")
+                    request = SimpleStoreRequest(
+                        self,
+                        "PUT",
+                        joinURL(calendar_uri, child.basename()),
+                        headers=Headers({"content-type": MimeType.fromString("text/calendar")}),
+                        authid="wsanchez"
+                    )
                     request.stream = MemoryStream(child.getContent())
                     yield self.send(request)
 

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_props.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_props.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_props.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -88,7 +88,7 @@
                         self.fail("Expected CalDAV:supported-calendar-data element; but got none.")
 
                     for calendar in supported_calendar.children:
-                        if calendar.content_type != "text/calendar":
+                        if calendar.content_type not in ("text/calendar", "application/calendar+json"):
                             self.fail("Expected a text/calendar calendar-data type restriction")
                         if calendar.version != "2.0":
                             self.fail("Expected a version 2.0 calendar-data restriction")

Added: CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_util.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/test/test_util.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -0,0 +1,144 @@
+##
+# Copyright (c) 2005-2013 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twext.web2.http_headers import Headers
+
+import twistedcaldav.test.util
+from twistedcaldav.util import bestAcceptType
+
+class AcceptType(twistedcaldav.test.util.TestCase):
+    """
+    L{bestAcceptType} tests
+    """
+    def test_bestAcceptType(self):
+
+        data = (
+            (
+                "#1.1",
+                ("Accept", "text/plain"),
+                ["text/plain"],
+                "text/plain",
+            ),
+            (
+                "#1.2",
+                ("Accept", "text/plain"),
+                ["text/calendar"],
+                None,
+            ),
+            (
+                "#1.3",
+                ("Accept", "text/*"),
+                ["text/plain"],
+                "text/plain",
+            ),
+            (
+                "#1.4",
+                ("Accept", "*/*"),
+                ["text/plain"],
+                "text/plain",
+            ),
+            (
+                "#2.1",
+                ("Accept", "text/plain"),
+                ["text/plain", "application/text", ],
+                "text/plain",
+            ),
+            (
+                "#2.2",
+                ("Accept", "text/plain"),
+                ["text/calendar", "application/text", ],
+                None,
+            ),
+            (
+                "#2.3",
+                ("Accept", "text/*"),
+                ["text/plain", "application/text", ],
+                "text/plain",
+            ),
+            (
+                "#2.4",
+                ("Accept", "*/*"),
+                ["text/plain", "application/text", ],
+                "text/plain",
+            ),
+            (
+                "#2.5",
+                ("Accept", "application/text"),
+                ["text/plain", "application/text", ],
+                "application/text",
+            ),
+            (
+                "#2.6",
+                ("Accept", "application/*"),
+                ["text/plain", "application/text", ],
+                "application/text",
+            ),
+            (
+                "#3.1",
+                ("Accept", "text/plain;q=0.5, application/text;q=0.3"),
+                ["text/plain", "application/text", ],
+                "text/plain",
+            ),
+            (
+                "#3.2",
+                ("Accept", "text/plain;q=0.5, application/text;q=0.3"),
+                ["text/calendar", "application/calendar", ],
+                None,
+            ),
+            (
+                "#3.3",
+                ("Accept", "text/plain;q=0.5, application/text;q=0.3"),
+                ["text/plain", "application/text", ],
+                "text/plain",
+            ),
+            (
+                "#3.4",
+                ("Accept", "text/plain;q=0.5, application/text;q=0.3"),
+                ["text/plain", "application/text", ],
+                "text/plain",
+            ),
+            (
+                "#3.5",
+                ("Accept", "text/plain;q=0.3, application/text;q=0.5"),
+                ["text/plain", "application/text", ],
+                "application/text",
+            ),
+            (
+                "#3.6",
+                ("Accept", "text/plain;q=0.5, application/*;q=0.3"),
+                ["text/plain", "application/text", ],
+                "text/plain",
+            ),
+            (
+                "#4.1",
+                ("Accept", "text/plain;q=0.5, application/text;q=0.2, text/*;q=0.3"),
+                ["text/calendar", "application/text", ],
+                "text/calendar",
+            ),
+            (
+                "#5.1",
+                None,
+                ["text/calendar", "application/text", ],
+                "text/calendar",
+            ),
+        )
+
+        for title, hdr, allowedTypes, result in data:
+            hdrs = Headers()
+            if hdr:
+                hdrs.addRawHeader(*hdr)
+            check = bestAcceptType(hdrs.getHeader("accept"), allowedTypes)
+            self.assertEqual(check, result, msg="Failed %s" % (title,))

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/timezonestdservice.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/timezonestdservice.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/timezonestdservice.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -89,7 +89,13 @@
         else:
             raise ValueError("Invalid TimezoneService mode: %s" % (config.TimezoneService.Mode,))
 
+        self.formats = []
+        self.formats.append("text/calendar")
+        self.formats.append("text/plain")
+        if config.EnableJSONData:
+            self.formats.append("application/calendar+json")
 
+
     def _initPrimaryService(self):
         tzpath = TimezoneCache.getDBPath()
         xmlfile = os.path.join(tzpath, "timezones.xml")
@@ -268,7 +274,7 @@
                 {
                     "name": "get",
                     "parameters": [
-                        {"name": "format", "required": False, "multi": False, "values": ["text/calendar", "text/plain", ], },
+                        {"name": "format", "required": False, "multi": False, "values": self.formats, },
                         {"name": "tzid", "required": True, "multi": False, },
                     ],
                 },
@@ -348,7 +354,7 @@
             ))
 
         format = request.args.get("format", ("text/calendar",))
-        if len(format) != 1 or format[0] not in ("text/calendar", "text/plain",):
+        if len(format) != 1 or format[0] not in self.formats:
             raise HTTPError(JSONResponse(
                 responsecode.BAD_REQUEST,
                 {
@@ -368,7 +374,7 @@
                 }
             ))
 
-        tzdata = calendar.getText()
+        tzdata = calendar.getText(format=format if format != "text/plain" else None)
 
         response = Response()
         response.stream = MemoryStream(tzdata)

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/util.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/util.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/util.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -517,3 +517,52 @@
         cuas = principal.record.calendarUserAddresses
 
         return (fullName, rec.guid, cuas)
+
+
+
+def bestAcceptType(accepts, allowedTypes):
+    """
+    Given a set of Accept headers and the set of types the server can return, determine the best choice
+    of format to return to the client.
+
+    @param accepts: parsed accept headers
+    @type accepts: C{dict}
+    @param allowedTypes: list of allowed types in server preferred order
+    @type allowedTypes: C{list}
+    """
+
+    # If no Accept present just use the first allowed type - the server's preference
+    if not accepts:
+        return allowedTypes[0]
+
+    # Get mapping for ordered top-level types for use in subtype wildcard match
+    toptypes = {}
+    for allowed in allowedTypes:
+        mediaType = allowed.split("/")[0]
+        if mediaType not in toptypes:
+            toptypes[mediaType] = allowed
+
+    result = None
+    result_qval = 0.0
+    for content_type, qval in accepts.items():
+        # Exact match
+        ctype = "%s/%s" % (content_type.mediaType, content_type.mediaSubtype,)
+        if ctype in allowedTypes:
+            if qval > result_qval:
+                result = ctype
+                result_qval = qval
+
+        # Subtype wildcard match
+        elif content_type.mediaType != "*" and content_type.mediaSubtype == "*":
+            if content_type.mediaType in toptypes:
+                if qval > result_qval:
+                    result = toptypes[content_type.mediaType]
+                    result_qval = qval
+
+        # Full wildcard match
+        elif content_type.mediaType == "*" and content_type.mediaSubtype == "*":
+            if qval > result_qval:
+                result = allowedTypes[0]
+                result_qval = qval
+
+    return result

Modified: CalendarServer/branches/users/cdaboo/json/twistedcaldav/vcard.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/twistedcaldav/vcard.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/twistedcaldav/vcard.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -31,6 +31,8 @@
 from twext.web2.stream import IStream
 from twext.web2.dav.util import allDataFromStream
 
+from twistedcaldav.config import config
+
 from pycalendar.parameter import Parameter
 from pycalendar.componentbase import ComponentBase
 from pycalendar.exceptions import ErrorBase
@@ -219,8 +221,20 @@
     """
     X{vCard} component.
     """
+    allowedTypesList = None
+
+
     @classmethod
-    def allFromString(clazz, string):
+    def allowedTypes(cls):
+        if cls.allowedTypesList is None:
+            cls.allowedTypesList = ["text/vcard"]
+            if config.EnableJSONData:
+                cls.allowedTypesList.append("application/vcard+json")
+        return cls.allowedTypesList
+
+
+    @classmethod
+    def allFromString(clazz, string, format=None):
         """
         FIXME: Just default to reading a single VCARD - actually need more
         """
@@ -234,16 +248,16 @@
         if string[:3] == codecs.BOM_UTF8:
             string = string[3:]
 
-        return clazz.allFromStream(StringIO.StringIO(string))
+        return clazz.allFromStream(StringIO.StringIO(string), format)
 
 
     @classmethod
-    def allFromStream(clazz, stream):
+    def allFromStream(clazz, stream, format=None):
         """
         FIXME: Just default to reading a single VCARD - actually need more
         """
         try:
-            results = Card.parseMultiple(stream)
+            results = Card.parseMultipleData(stream, format)
         except ErrorBase:
             results = None
         if not results:
@@ -253,47 +267,66 @@
 
 
     @classmethod
-    def fromString(clazz, string):
+    def fromString(clazz, string, format=None):
         """
         Construct a L{Component} from a string.
         @param string: a string containing vCard data.
         @return: a L{Component} representing the first component described by
             C{string}.
         """
-        if type(string) is unicode:
-            string = string.encode("utf-8")
-        else:
-            # Valid utf-8 please
-            string.decode("utf-8")
+        return clazz._fromData(string, False, format)
 
-        # No BOMs please
-        if string[:3] == codecs.BOM_UTF8:
-            string = string[3:]
 
-        return clazz.fromStream(StringIO.StringIO(string))
+    @classmethod
+    def fromStream(clazz, stream, format=None):
+        """
+        Construct a L{Component} from a stream.
+        @param stream: a C{read()}able stream containing vCard data.
+        @return: a L{Component} representing the first component described by
+            C{stream}.
+        """
+        return clazz._fromData(stream, True, format)
 
 
     @classmethod
-    def fromStream(clazz, stream):
+    def _fromData(clazz, data, isstream, format=None):
         """
         Construct a L{Component} from a stream.
         @param stream: a C{read()}able stream containing vCard data.
+        @param format: a C{str} indicating whether the data is vCard or jCard
         @return: a L{Component} representing the first component described by
             C{stream}.
         """
-        cal = Card()
+
+        if isstream:
+            pass
+        else:
+            if type(data) is unicode:
+                data = data.encode("utf-8")
+            else:
+                # Valid utf-8 please
+                data.decode("utf-8")
+
+            # No BOMs please
+            if data[:3] == codecs.BOM_UTF8:
+                data = data[3:]
+
+        errmsg = "Unknown"
         try:
-            result = cal.parse(stream)
-        except ErrorBase:
+            result = Card.parseData(data, format)
+        except ErrorBase, e:
+            errmsg = "%s: %s" % (e.mReason, e.mData,)
             result = None
         if not result:
-            stream.seek(0)
-            raise InvalidVCardDataError("%s" % (stream.read(),))
-        return clazz(None, pycard=cal)
+            if isstream:
+                data.seek(0)
+                data = data.read()
+            raise InvalidVCardDataError("%s\n%s" % (errmsg, data,))
+        return clazz(None, pycard=result)
 
 
     @classmethod
-    def fromIStream(clazz, stream):
+    def fromIStream(clazz, stream, format=None):
         """
         Construct a L{Component} from a stream.
         @param stream: an L{IStream} containing vCard data.
@@ -307,7 +340,7 @@
         #   request stream.
         #
         def parse(data):
-            return clazz.fromString(data)
+            return clazz.fromString(data, format)
         return allDataFromStream(IStream(stream), parse)
 
 
@@ -372,6 +405,18 @@
         return self._pycard == other._pycard
 
 
+    def getText(self, format=None):
+        """
+        Return text representation
+        """
+        assert self.name() == "VCARD", "Must be a VCARD: %r" % (self,)
+
+        result = self._pycard.getText(format)
+        if result is None:
+            raise ValueError("Unknown format requested for address data.")
+        return result
+
+
     # FIXME: Should this not be in __eq__?
     def same(self, other):
         return self._pycard == other._pycard
@@ -379,7 +424,7 @@
 
     def name(self):
         """
-        @return: the name of the iCalendar type of this component.
+        @return: the name of the vCard type of this component.
         """
         return self._pycard.getType()
 
@@ -527,7 +572,7 @@
         if unfixed:
             log.debug("vCard data had unfixable problems:\n  %s" % ("\n  ".join(unfixed),))
             if doRaise:
-                raise InvalidVCardDataError("Calendar data had unfixable problems:\n  %s" % ("\n  ".join(unfixed),))
+                raise InvalidVCardDataError("Address data had unfixable problems:\n  %s" % ("\n  ".join(unfixed),))
         if fixed:
             log.debug("vCard data had fixable problems:\n  %s" % ("\n  ".join(fixed),))
 

Modified: CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/caldav/test/test_scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/caldav/test/test_scheduler.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/caldav/test/test_scheduler.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -170,9 +170,9 @@
         scheduler = CalDAVScheduler(self.transactionUnderTest(), "user01")
         result = (yield scheduler.doSchedulingViaPOST("mailto:user01 at example.com", ["mailto:user01 at example.com", ], Component.fromString(data_request)))
         self.assertEqual(len(result.responses), 1)
-        self.assertEqual(str(result.responses[0].children[0].children[0]), "mailto:user01 at example.com")
-        self.assertTrue(str(result.responses[0].children[1]).startswith("2"))
-        self.assertEqual(normalizeiCalendarText(str(result.responses[0].children[2].children[0])), data_reply.replace("\n", "\r\n"))
+        self.assertEqual(str(result.responses[0].recipient.children[0]), "mailto:user01 at example.com")
+        self.assertTrue(str(result.responses[0].reqstatus).startswith("2"))
+        self.assertEqual(normalizeiCalendarText(str(result.responses[0].calendar)), data_reply.replace("\n", "\r\n"))
 
 
     @inlineCallbacks
@@ -227,9 +227,9 @@
         scheduler = CalDAVScheduler(self.transactionUnderTest(), "user01")
         result = (yield scheduler.doSchedulingViaPOST("mailto:user01 at example.com", ["mailto:user01 at example.com", ], Component.fromString(data_request)))
         self.assertEqual(len(result.responses), 1)
-        self.assertEqual(str(result.responses[0].children[0].children[0]), "mailto:user01 at example.com")
-        self.assertTrue(str(result.responses[0].children[1]).startswith("2"))
-        self.assertEqual(normalizeiCalendarText(str(result.responses[0].children[2].children[0])), data_reply.replace("\n", "\r\n"))
+        self.assertEqual(str(result.responses[0].recipient.children[0]), "mailto:user01 at example.com")
+        self.assertTrue(str(result.responses[0].reqstatus).startswith("2"))
+        self.assertEqual(normalizeiCalendarText(str(result.responses[0].calendar)), data_reply.replace("\n", "\r\n"))
 
 
     @inlineCallbacks
@@ -294,6 +294,6 @@
         scheduler = CalDAVScheduler(self.transactionUnderTest(), "user01")
         result = (yield scheduler.doSchedulingViaPOST("mailto:user01 at example.com", ["mailto:user01 at example.com", ], Component.fromString(data_request)))
         self.assertEqual(len(result.responses), 1)
-        self.assertEqual(str(result.responses[0].children[0].children[0]), "mailto:user01 at example.com")
-        self.assertTrue(str(result.responses[0].children[1]).startswith("2"))
-        self.assertEqual(normalizeiCalendarText(str(result.responses[0].children[2].children[0])), data_reply.replace("\n", "\r\n"))
+        self.assertEqual(str(result.responses[0].recipient.children[0]), "mailto:user01 at example.com")
+        self.assertTrue(str(result.responses[0].reqstatus).startswith("2"))
+        self.assertEqual(normalizeiCalendarText(str(result.responses[0].calendar)), data_reply.replace("\n", "\r\n"))

Modified: CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/imip/test/test_delivery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/imip/test/test_delivery.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/imip/test/test_delivery.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -64,7 +64,7 @@
         yield delivery.generateSchedulingResponses()
 
         self.assertEqual(len(responses.responses), 1)
-        self.assertEqual(str(responses.responses[0].children[1]), iTIPRequestStatus.SERVICE_UNAVAILABLE)
+        self.assertEqual(str(responses.responses[0].reqstatus), iTIPRequestStatus.SERVICE_UNAVAILABLE)
 
 
     @inlineCallbacks

Modified: CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/imip/test/test_inbound.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/imip/test/test_inbound.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/imip/test/test_inbound.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -297,7 +297,7 @@
         yield txn.commit()
         self.assertEquals(
             "1.2;Scheduling message has been delivered",
-            result.responses[0].children[1].toString()
+            result.responses[0].reqstatus.toString()
         )
 
 
@@ -330,7 +330,7 @@
         yield txn.commit()
         self.assertEquals(
             "3.7;Invalid Calendar User",
-            result.responses[0].children[1].toString()
+            result.responses[0].reqstatus.toString()
         )
 
 

Modified: CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/implicit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/implicit.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/implicit.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -21,7 +21,6 @@
 from twext.web2 import responsecode
 from twext.web2.http import HTTPError
 
-from twistedcaldav import caldavxml
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.config import config
 from twistedcaldav.ical import Property
@@ -1047,9 +1046,8 @@
         # Map each recipient in the response to a status code
         responses = {}
         for item in response.responses:
-            assert isinstance(item, caldavxml.Response), "Wrong element in response"
-            recipient = str(item.children[0].children[0])
-            status = str(item.children[1])
+            recipient = str(item.recipient.children[0])
+            status = str(item.reqstatus)
             responses[recipient] = status
 
             # Now apply to each ATTENDEE/ORGANIZER in the original data

Modified: CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/resource.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/resource.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -163,6 +163,20 @@
         maxDateTime = DateTime(now.getYear(), 1, 1, 0, 0, 0, Timezone(utc=True))
         maxDateTime.offsetYear(10)
 
+        dataTypes = []
+        dataTypes.append(
+            ischedulexml.CalendarDataType(**{
+                "content-type": "text/calendar",
+                "version": "2.0",
+            })
+        )
+        if config.EnableJSONData:
+            dataTypes.append(
+                ischedulexml.CalendarDataType(**{
+                    "content-type": "application/calendar+json",
+                    "version": "2.0",
+                })
+            )
         result = ischedulexml.QueryResult(
 
             ischedulexml.Capabilities(
@@ -188,12 +202,7 @@
                         name="VFREEBUSY"
                     ),
                 ),
-                ischedulexml.CalendarDataTypes(
-                    ischedulexml.CalendarDataType(**{
-                            "content-type": "text/calendar",
-                            "version": "2.0",
-                    }),
-                ),
+                ischedulexml.CalendarDataTypes(*dataTypes),
                 ischedulexml.Attachments(
                     ischedulexml.External(),
                 ),
@@ -222,24 +231,48 @@
         # This is a server-to-server scheduling operation.
         scheduler = IScheduleScheduler(txn, None)
 
+        # Check content first
+        contentType = request.headers.getHeader("content-type")
+        format = self.determineType(contentType)
+
+        if format is None:
+            msg = "MIME type %s not allowed in iSchedule request" % (contentType,)
+            self.log.error(msg)
+            raise HTTPError(scheduler.errorResponse(
+                responsecode.FORBIDDEN,
+                (ischedule_namespace, "invalid-calendar-data-type"),
+                msg,
+            ))
+
         originator = self.loadOriginatorFromRequestHeaders(request)
         recipients = self.loadRecipientsFromRequestHeaders(request)
         body = (yield allDataFromStream(request.stream))
+        calendar = Component.fromString(body, format=format)
 
         # Do the POST processing treating this as a non-local schedule
         try:
-            result = (yield scheduler.doSchedulingViaPOST(request.remoteAddr, request.headers, body, originator, recipients))
+            result = (yield scheduler.doSchedulingViaPOST(request.remoteAddr, request.headers, body, calendar, originator, recipients))
         except Exception:
             ex = Failure()
             yield txn.abort()
             ex.raiseException()
         else:
             yield txn.commit()
-        response = result.response()
+        response = result.response(format=format)
         response.headers.addRawHeader(ISCHEDULE_CAPABILITIES, str(config.Scheduling.iSchedule.SerialNumber))
         returnValue(response)
 
 
+    def determineType(self, content_type):
+        """
+        Determine if the supplied content-type is valid for storing and return the matching PyCalendar type.
+        """
+        format = None
+        if content_type is not None:
+            format = "%s/%s" % (content_type.mediaType, content_type.mediaSubtype,)
+        return format if format in Component.allowedTypes() else None
+
+
     def loadOriginatorFromRequestHeaders(self, request):
         # Must have Originator header
         originator = request.headers.getRawHeaders("originator")

Modified: CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/scheduler.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/scheduler.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -23,7 +23,7 @@
 from twisted.internet.defer import inlineCallbacks, returnValue
 
 from twistedcaldav.config import config
-from twistedcaldav.ical import normalizeCUAddress, Component
+from twistedcaldav.ical import normalizeCUAddress
 
 from txdav.caldav.datastore.scheduling import addressmapping
 from txdav.caldav.datastore.scheduling.cuaddress import RemoteCalendarUser
@@ -139,7 +139,7 @@
     }
 
     @inlineCallbacks
-    def doSchedulingViaPOST(self, remoteAddr, headers, body, originator, recipients):
+    def doSchedulingViaPOST(self, remoteAddr, headers, body, calendar, originator, recipients):
         """
         Carry out iSchedule specific processing.
         """
@@ -172,8 +172,6 @@
                     msg,
                 ))
 
-        calendar = Component.fromString(body)
-
         if self.headers.getRawHeaders('x-calendarserver-itip-refreshonly', ("F"))[0] == "T":
             self.txn.doing_attendee_refresh = 1
 

Modified: CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/test/test_resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/test/test_resource.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/ischedule/test/test_resource.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -94,6 +94,7 @@
             headers=http_headers.Headers(rawHeaders={
                 "Originator": ("mailto:wsanchez at example.com",),
                 "Recipient": ("mailto:cdaboo at example.com",),
+                "Content-Type": "text/calendar",
             }),
             content="""BEGIN:VCALENDAR
 CALSCALE:GREGORIAN
@@ -144,6 +145,7 @@
             headers=http_headers.Headers(rawHeaders={
                 "Originator": ("mailto:user01 at example.org",),
                 "Recipient": ("mailto:user02 at example.com",),
+                "Content-Type": ("text/calendar",)
             }),
             content="""BEGIN:VCALENDAR
 CALSCALE:GREGORIAN

Modified: CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/scheduler.py	2013-08-26 16:31:33 UTC (rev 11638)
+++ CalendarServer/branches/users/cdaboo/json/txdav/caldav/datastore/scheduling/scheduler.py	2013-08-26 16:43:05 UTC (rev 11639)
@@ -42,7 +42,9 @@
 from txdav.caldav.datastore.scheduling.imip.delivery import ScheduleViaIMip
 from txdav.caldav.datastore.scheduling.ischedule.delivery import ScheduleViaISchedule
 from txdav.caldav.datastore.scheduling.itip import iTIPRequestStatus
+
 import hashlib
+from collections import namedtuple
 
 """
 CalDAV/Server-to-Server scheduling behavior.
@@ -703,6 +705,11 @@
     response_description_element = davxml.ResponseDescription
     calendar_data_element = caldavxml.CalendarData
 
+    ScheduleResonseDetails = namedtuple(
+        "ScheduleResonseDetails",
+        ["recipient", "reqstatus", "calendar", "error", "message", ]
+    )
+
     def __init__(self, method, success_response):
         """
         @param method: the name of the method generating the queue.
@@ -748,16 +755,14 @@
         if not suppressErrorLog and code > 400: # Error codes only
             self.log.error("Error during %s for %s: %s" % (self.method, recipient, message))
 
-        children = []
-        children.append(self.recipient_element(davxml.HRef.fromString(recipient)) if self.recipient_uses_href else self.recipient_element.fromString(recipient))
-        children.append(self.request_status_element(reqstatus))
-        if calendar is not None:
-            children.append(self.calendar_data_element.fromCalendar(calendar))
-        if error is not None:
-            children.append(error)
-        if message is not None:
-            children.append(self.response_description_element(message))
-        self.responses.append(self.response_element(*children))
+        details = ScheduleResponseQueue.ScheduleResonseDetails(
+            self.recipient_element(davxml.HRef.fromString(recipient)) if self.recipient_uses_href else self.recipient_element.fromString(recipient),
+            self.request_status_element(reqstatus),
+            calendar,
+            error,
+            self.response_description_element(message) if message is not None else None,
+        )
+        self.responses.append(details)
 
 
     def errorForFailure(self, failure):
@@ -773,19 +778,17 @@
         @param clone: the response to clone.
         """
 
-        children = []
-        children.append(self.recipient_element(davxml.HRef.fromString(recipient)) if self.recipient_uses_href else self.recipient_element.fromString(recipient))
-        children.append(self.request_status_element.fromString(request_status))
-        if calendar_data is not None:
-            children.append(self.calendar_data_element.fromCalendar(calendar_data))
-        if error is not None:
-            children.append(self.error_element(*error))
-        if desc is not None:
-            children.append(self.response_description_element.fromString(desc))
-        self.responses.append(self.response_element(*children))
+        details = ScheduleResponseQueue.ScheduleResonseDetails(
+            self.recipient_element(davxml.HRef.fromString(recipient)) if self.recipient_uses_href else self.recipient_element.fromString(recipient),
+            self.request_status_element.fromString(request_status),
+            calendar_data,
+            self.error_element(*error) if error is not None else None,
+            self.response_description_element.fromString(desc) if desc is not None else None,
+        )
+        self.responses.append(details)
 
 
-    def response(self):
+    def response(self, format=None):
         """
         Generate a L{ScheduleResponseResponse} with the responses contained in the
         queue or, if no such responses, return the C{success_response} provided
@@ -793,6 +796,20 @@
         @return: the response.
         """
         if self.responses:
-            return ScheduleResponseResponse(self.schedule_response_element, self.responses, self.location)
+            # Convert our queue to all XML elements
+            xml_responses = []
+            for response in self.responses:
+                children = []
+                children.append(response.recipient)
+                children.append(response.reqstatus)
+                if response.calendar is not None:
+                    children.append(self.calendar_data_element.fromCalendar(response.calendar, format))
+                if response.error is not None:
+                    children.append(response.error)
+                if response.message is not None:
+                    children.append(response.message)
+                xml_responses.append(self.response_element(*children))
+
+            return ScheduleResponseResponse(self.schedule_response_element, xml_responses, self.location)
         else:
             return self.success_response
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130826/e9365ab0/attachment-0001.html>


More information about the calendarserver-changes mailing list