[CalendarServer-changes] [10135] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Fri Dec 7 10:26:55 PST 2012


Revision: 10135
          http://trac.calendarserver.org//changeset/10135
Author:   cdaboo at apple.com
Date:     2012-12-07 10:26:55 -0800 (Fri, 07 Dec 2012)
Log Message:
-----------
Update standard timezone service to latest spec.

Modified Paths:
--------------
    CalendarServer/trunk/twext/web2/http.py
    CalendarServer/trunk/twistedcaldav/ical.py
    CalendarServer/trunk/twistedcaldav/test/test_timezonestdservice.py
    CalendarServer/trunk/twistedcaldav/timezonestdservice.py

Modified: CalendarServer/trunk/twext/web2/http.py
===================================================================
--- CalendarServer/trunk/twext/web2/http.py	2012-12-07 17:50:39 UTC (rev 10134)
+++ CalendarServer/trunk/twext/web2/http.py	2012-12-07 18:26:55 UTC (rev 10135)
@@ -32,6 +32,7 @@
 """
 #        import traceback; log.msg(''.join(traceback.format_stack()))
 
+import json
 import time
 
 from twisted.internet import interfaces, error
@@ -50,13 +51,13 @@
 log = Logger()
 
 
-defaultPortForScheme = {'http': 80, 'https':443, 'ftp':21}
+defaultPortForScheme = {'http': 80, 'https': 443, 'ftp': 21}
 
 def splitHostPort(scheme, hostport):
-    """Split the host in "host:port" format into host and port fields. 
+    """Split the host in "host:port" format into host and port fields.
     If port was not specified, use the default for the given scheme, if
     known. Returns a tuple of (hostname, portnumber)."""
-    
+
     # Split hostport into host and port
     hostport = hostport.split(':', 1)
     try:
@@ -67,6 +68,7 @@
     return hostport[0], defaultPortForScheme.get(scheme, 0)
 
 
+
 def parseVersion(strversion):
     """Parse version strings of the form Protocol '/' Major '.' Minor. E.g. 'HTTP/1.1'.
     Returns (protocol, major, minor).
@@ -80,7 +82,9 @@
     return (proto.lower(), major, minor)
 
 
+
 class HTTPError(Exception):
+
     def __init__(self, codeOrResponse):
         """An Exception for propagating HTTP Error Responses.
 
@@ -91,28 +95,30 @@
         self.response = iweb.IResponse(codeOrResponse)
         Exception.__init__(self, str(self.response))
 
+
     def __repr__(self):
         return "<%s %s>" % (self.__class__.__name__, self.response)
 
 
+
 class Response(object):
     """An object representing an HTTP Response to be sent to the client.
     """
     implements(iweb.IResponse)
-    
+
     code = responsecode.OK
     headers = None
     stream = None
-    
+
     def __init__(self, code=None, headers=None, stream=None):
         """
         @param code: The HTTP status code for this Response
         @type code: C{int}
-        
+
         @param headers: Headers to be sent to the client.
-        @type headers: C{dict}, L{twext.web2.http_headers.Headers}, or 
+        @type headers: C{dict}, L{twext.web2.http_headers.Headers}, or
             C{None}
-        
+
         @param stream: Content body to send to the HTTP client
         @type stream: L{twext.web2.stream.IByteStream}
         """
@@ -123,13 +129,14 @@
         if headers is not None:
             if isinstance(headers, dict):
                 headers = http_headers.Headers(headers)
-            self.headers=headers
+            self.headers = headers
         else:
             self.headers = http_headers.Headers()
 
         if stream is not None:
             self.stream = IByteStream(stream)
 
+
     def __repr__(self):
         if self.stream is None:
             streamlen = None
@@ -139,6 +146,7 @@
         return "<%s.%s code=%d, streamlen=%s>" % (self.__module__, self.__class__.__name__, self.code, streamlen)
 
 
+
 class StatusResponseElement(Element):
     """
     Render the HTML for a L{StatusResponse}
@@ -216,10 +224,11 @@
 
         self.headers.setHeader("location", location)
 
-        
+
+
 def NotModifiedResponse(oldResponse=None):
     if oldResponse is not None:
-        headers=http_headers.Headers()
+        headers = http_headers.Headers()
         for header in (
             # Required from sec 10.3.5:
             'date', 'etag', 'content-location', 'expires',
@@ -232,8 +241,9 @@
     else:
         headers = None
     return Response(code=responsecode.NOT_MODIFIED, headers=headers)
-    
 
+
+
 def checkPreconditions(request, response=None, entityExists=True, etag=None, lastModified=None):
     """Check to see if this request passes the conditional checks specified
     by the client. May raise an HTTPError with result codes L{NOT_MODIFIED}
@@ -246,7 +256,7 @@
     However, if you are implementing other request methods, like PUT
     for your resource, you will need to call this after determining
     the etag and last-modified time of the existing resource but
-    before actually doing the requested action. In that case, 
+    before actually doing the requested action. In that case,
 
     This examines the appropriate request headers for conditionals,
     (If-Modified-Since, If-Unmodified-Since, If-Match, If-None-Match,
@@ -259,15 +269,15 @@
              shouldn't be separately specified. Not providing the
              response with a GET request may cause the emitted
              "Not Modified" responses to be non-conformant.
-             
+
     @param entityExists: Set to False if the entity in question doesn't
              yet exist. Necessary for PUT support with 'If-None-Match: *'.
-             
+
     @param etag: The etag of the resource to check against, or None.
-    
+
     @param lastModified: The last modified date of the resource to check
               against, or None.
-              
+
     @raise: HTTPError: Raised when the preconditions fail, in order to
              abort processing and emit an error page.
 
@@ -280,7 +290,8 @@
             return False
         etag = response.headers.getHeader("etag")
         lastModified = response.headers.getHeader("last-modified")
-    
+
+
     def matchETag(tags, allowWeak):
         if entityExists and '*' in tags:
             return True
@@ -322,7 +333,7 @@
     if inm:
         if request.method in ("HEAD", "GET"):
             # If it's a range request, don't allow a weak ETag, as that
-            # would break. 
+            # would break.
             canBeWeak = not request.headers.hasHeader('Range')
             if notModified != False and matchETag(inm, canBeWeak):
                 raise HTTPError(NotModifiedResponse(response))
@@ -333,6 +344,8 @@
         if notModified == True:
             raise HTTPError(NotModifiedResponse(response))
 
+
+
 def checkIfRange(request, response):
     """Checks for the If-Range header, and if it exists, checks if the
     test passes. Returns true if the server should return partial data."""
@@ -347,25 +360,29 @@
         return ifrange == response.headers.getHeader("last-modified")
 
 
+
 class _NotifyingProducerStream(stream.ProducerStream):
     doStartReading = None
 
     def __init__(self, length=None, doStartReading=None):
         stream.ProducerStream.__init__(self, length=length)
         self.doStartReading = doStartReading
-    
+
+
     def read(self):
         if self.doStartReading is not None:
             doStartReading = self.doStartReading
             self.doStartReading = None
             doStartReading()
-            
+
         return stream.ProducerStream.read(self)
 
+
     def write(self, data):
         self.doStartReading = None
         stream.ProducerStream.write(self, data)
 
+
     def finish(self):
         self.doStartReading = None
         stream.ProducerStream.finish(self)
@@ -379,18 +396,18 @@
 
     Subclasses should override the process() method to determine how
     the request will be processed.
-    
+
     @ivar method: The HTTP method that was used.
     @ivar uri: The full URI that was requested (includes arguments).
     @ivar headers: All received headers
     @ivar clientproto: client HTTP version
     @ivar stream: incoming data stream.
     """
-    
+
     implements(iweb.IRequest, interfaces.IConsumer)
-    
+
     known_expects = ('100-continue',)
-    
+
     def __init__(self, chanRequest, command, path, version, contentLength, headers):
         """
         @param chanRequest: the channel request we're associated with.
@@ -399,16 +416,17 @@
         self.method = command
         self.uri = path
         self.clientproto = version
-        
+
         self.headers = headers
-        
+
         if '100-continue' in self.headers.getHeader('expect', ()):
             doStartReading = self._sendContinue
         else:
             doStartReading = None
         self.stream = _NotifyingProducerStream(contentLength, doStartReading)
         self.stream.registerProducer(self.chanRequest, True)
-        
+
+
     def checkExpect(self):
         """Ensure there are no expectations that cannot be met.
         Checks Expect header against self.known_expects."""
@@ -416,50 +434,59 @@
         for expect in expects:
             if expect not in self.known_expects:
                 raise HTTPError(responsecode.EXPECTATION_FAILED)
-    
+
+
     def process(self):
         """Called by channel to let you process the request.
-        
+
         Can be overridden by a subclass to do something useful."""
         pass
-    
+
+
     def handleContentChunk(self, data):
         """Callback from channel when a piece of data has been received.
         Puts the data in .stream"""
         self.stream.write(data)
-    
+
+
     def handleContentComplete(self):
         """Callback from channel when all data has been received. """
         self.stream.unregisterProducer()
         self.stream.finish()
-        
+
+
     def connectionLost(self, reason):
         """connection was lost"""
         pass
 
+
     def __repr__(self):
-        return '<%s %s %s>'% (self.method, self.uri, self.clientproto)
+        return '<%s %s %s>' % (self.method, self.uri, self.clientproto)
 
+
     def _sendContinue(self):
         self.chanRequest.writeIntermediateResponse(responsecode.CONTINUE)
 
+
     def _reallyFinished(self, x):
         """We are finished writing data."""
         self.chanRequest.finish()
-        
+
+
     def _finished(self, x):
         """
         We are finished writing data.
         But we need to check that we have also finished reading all data as we
         might have sent a, for example, 401 response before we read any data.
         To make sure that the stream/producer sequencing works properly we need
-        to discard the remaining data in the request.  
+        to discard the remaining data in the request.
         """
         if self.stream.length != 0:
             return readAndDiscard(self.stream).addCallback(self._reallyFinished).addErrback(self._error)
         else:
             self._reallyFinished(x)
 
+
     def _error(self, reason):
         if reason.check(error.ConnectionLost):
             log.msg("Request error: " + reason.getErrorMessage())
@@ -467,7 +494,8 @@
             log.err(reason)
             # Only bother with cleanup on errors other than lost connection.
             self.chanRequest.abortConnection()
-        
+
+
     def writeResponse(self, response):
         """
         Write a response.
@@ -476,10 +504,10 @@
             # Expect: 100-continue was requested, but 100 response has not been
             # sent, and there's a possibility that data is still waiting to be
             # sent.
-            # 
+            #
             # Ideally this means the remote side will not send any data.
             # However, because of compatibility requirements, it might timeout,
-            # and decide to do so anyways at the same time we're sending back 
+            # and decide to do so anyways at the same time we're sending back
             # this response. Thus, the read state is unknown after this.
             # We must close the connection.
             self.chanRequest.channel.setReadPersistent(False)
@@ -493,7 +521,7 @@
             elif response.stream.length is not None:
                 response.headers.setHeader('content-length', response.stream.length)
         self.chanRequest.writeHeaders(response.code, response.headers)
-        
+
         # if this is a "HEAD" request, or a special response code,
         # don't return any data.
         if self.method == "HEAD" or response.code in NO_BODY_CODES:
@@ -501,10 +529,12 @@
                 response.stream.close()
             self._finished(None)
             return
-            
+
         d = stream.StreamProducer(response.stream).beginProducing(self.chanRequest)
         d.addCallback(self._finished).addErrback(self._error)
 
+
+
 class XMLResponse (Response):
     """
     XML L{Response} object.
@@ -517,8 +547,21 @@
         Response.__init__(self, code, stream=element.toxml())
         self.headers.setHeader("content-type", http_headers.MimeType("text", "xml"))
 
-    
+
+
+class JSONResponse (Response):
+    """
+    JSON L{Response} object.
+    Renders itself as an JSON document.
+    """
+    def __init__(self, code, jobj):
+        """
+        @param xml_responses: an iterable of davxml.Response objects.
+        """
+        Response.__init__(self, code, stream=json.dumps(jobj))
+        self.headers.setHeader("content-type", http_headers.MimeType("application", "json"))
+
+
 components.registerAdapter(Response, int, iweb.IResponse)
 
-__all__ = ['HTTPError', 'NotModifiedResponse', 'Request', 'Response', 'StatusResponse', 'RedirectResponse', 'checkIfRange', 'checkPreconditions', 'defaultPortForScheme', 'parseVersion', 'splitHostPort', "XMLResponse"]
-
+__all__ = ['HTTPError', 'NotModifiedResponse', 'Request', 'Response', 'StatusResponse', 'RedirectResponse', 'checkIfRange', 'checkPreconditions', 'defaultPortForScheme', 'parseVersion', 'splitHostPort', "XMLResponse", "JSONResponse"]

Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py	2012-12-07 17:50:39 UTC (rev 10134)
+++ CalendarServer/trunk/twistedcaldav/ical.py	2012-12-07 18:26:55 UTC (rev 10135)
@@ -3136,23 +3136,23 @@
     if tzexpanded:
         if start != tzexpanded[0][0]:
             results.append((
-                str(start),
-                PyCalendarUTCOffsetValue(tzexpanded[0][1]).getText(),
-                PyCalendarUTCOffsetValue(tzexpanded[0][1]).getText(),
+                start,
+                tzexpanded[0][1],
+                tzexpanded[0][1],
                 tzexpanded[0][3],
             ))
     else:
         results.append((
-            str(start),
-            PyCalendarUTCOffsetValue(tzcomp._pycalendar.getTimezoneOffsetSeconds(start)).getText(),
-            PyCalendarUTCOffsetValue(tzcomp._pycalendar.getTimezoneOffsetSeconds(start)).getText(),
-            tzcomp.getTZName(),
+            start,
+            tzcomp._pycalendar.getTimezoneOffsetSeconds(start),
+            tzcomp._pycalendar.getTimezoneOffsetSeconds(start),
+            tzcomp._pycalendar.getTimezoneDescriptor(start),
         ))
     for tzstart, tzoffsetfrom, tzoffsetto, name in tzexpanded:
         results.append((
-            tzstart.getText(),
-            PyCalendarUTCOffsetValue(tzoffsetfrom).getText(),
-            PyCalendarUTCOffsetValue(tzoffsetto).getText(),
+            tzstart,
+            tzoffsetfrom,
+            tzoffsetto,
             name,
         ))
 

Modified: CalendarServer/trunk/twistedcaldav/test/test_timezonestdservice.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_timezonestdservice.py	2012-12-07 17:50:39 UTC (rev 10134)
+++ CalendarServer/trunk/twistedcaldav/test/test_timezonestdservice.py	2012-12-07 18:26:55 UTC (rev 10135)
@@ -15,7 +15,7 @@
 ##
 
 from twistedcaldav.timezones import TimezoneCache
-from twistedcaldav.timezonestdservice import TimezoneInfo,\
+from twistedcaldav.timezonestdservice import TimezoneInfo, \
     PrimaryTimezoneDatabase
 from xml.etree.ElementTree import Element
 import hashlib
@@ -28,13 +28,13 @@
     """
 
     def test_generateXML(self):
-        
+
         hashed = hashlib.md5("test").hexdigest()
         info = TimezoneInfo("America/New_York", ("US/Eastern",), "20110517T120000Z", hashed)
-        
+
         node = Element("root")
         info.generateXML(node)
-        
+
         timezone = node.find("timezone")
         self.assertTrue(timezone is not None)
         self.assertEqual(timezone.findtext("tzid"), "America/New_York")
@@ -42,15 +42,16 @@
         self.assertEqual(timezone.findtext("alias"), "US/Eastern")
         self.assertEqual(timezone.findtext("md5"), hashed)
 
+
     def test_parseXML(self):
-        
+
         hashed = hashlib.md5("test").hexdigest()
         info1 = TimezoneInfo("America/New_York", ("US/Eastern",), "20110517T120000Z", hashed)
-        
+
         node = Element("root")
         info1.generateXML(node)
         timezone = node.find("timezone")
-        
+
         info2 = TimezoneInfo.readXML(timezone)
 
         self.assertEqual(info2.tzid, "America/New_York")
@@ -58,6 +59,8 @@
         self.assertEqual(info2.dtstamp, "20110517T120000Z")
         self.assertEqual(info2.md5, hashed)
 
+
+
 class TestPrimaryTimezoneDatabase (twistedcaldav.test.util.TestCase):
     """
     Timezone support tests
@@ -66,21 +69,23 @@
     def setUp(self):
         TimezoneCache.create()
 
+
     def testCreate(self):
-        
+
         xmlfile = self.mktemp()
         db = PrimaryTimezoneDatabase(TimezoneCache.getDBPath(), xmlfile)
-        
+
         db.createNewDatabase()
         self.assertTrue(os.path.exists(xmlfile))
         self.assertTrue(db.dtstamp is not None)
         self.assertTrue(len(db.timezones) > 0)
 
+
     def testUpdate(self):
-        
+
         xmlfile = self.mktemp()
         db = PrimaryTimezoneDatabase(TimezoneCache.getDBPath(), xmlfile)
-        
+
         db.createNewDatabase()
         self.assertTrue(os.path.exists(xmlfile))
 
@@ -88,8 +93,9 @@
         self.assertTrue(db.changeCount == 0)
         self.assertTrue(len(db.changed) == 0)
 
+
     def testRead(self):
-        
+
         xmlfile = self.mktemp()
         db1 = PrimaryTimezoneDatabase(TimezoneCache.getDBPath(), xmlfile)
         db1.createNewDatabase()
@@ -99,50 +105,54 @@
         db2.readDatabase()
         self.assertEqual(db1.dtstamp, db2.dtstamp)
         self.assertEqual(len(db1.timezones), len(db2.timezones))
-        
+
+
     def testList(self):
-        
+
         xmlfile = self.mktemp()
         db = PrimaryTimezoneDatabase(TimezoneCache.getDBPath(), xmlfile)
         db.createNewDatabase()
         self.assertTrue(os.path.exists(xmlfile))
-        
+
         tzids = set([tz.tzid for tz in db.listTimezones(None)])
         self.assertTrue("America/New_York" in tzids)
         self.assertTrue("US/Eastern" not in tzids)
-        
+
+
     def testListChangedSince(self):
-        
+
         xmlfile = self.mktemp()
         db = PrimaryTimezoneDatabase(TimezoneCache.getDBPath(), xmlfile)
         db.createNewDatabase()
         self.assertTrue(os.path.exists(xmlfile))
-        
+
         tzids = set([tz.tzid for tz in db.listTimezones(db.dtstamp)])
         self.assertTrue(len(tzids) == 0)
-        
+
+
     def testGetNone(self):
-        
+
         xmlfile = self.mktemp()
         db = PrimaryTimezoneDatabase(TimezoneCache.getDBPath(), xmlfile)
         db.createNewDatabase()
         self.assertTrue(os.path.exists(xmlfile))
-        
+
         tz = db.getTimezone("Bogus")
         self.assertEqual(tz, None)
-        
+
+
     def testGetOne(self):
-        
+
         xmlfile = self.mktemp()
         db = PrimaryTimezoneDatabase(TimezoneCache.getDBPath(), xmlfile)
         db.createNewDatabase()
         self.assertTrue(os.path.exists(xmlfile))
-        
+
         # Original
         tz1 = db.getTimezone("America/New_York")
         self.assertTrue(str(tz1).find("VTIMEZONE") != -1)
         self.assertTrue(str(tz1).find("TZID:America/New_York") != -1)
-        
+
         # Alias
         tz1 = db.getTimezone("US/Eastern")
         self.assertTrue(str(tz1).find("VTIMEZONE") != -1)

Modified: CalendarServer/trunk/twistedcaldav/timezonestdservice.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/timezonestdservice.py	2012-12-07 17:50:39 UTC (rev 10134)
+++ CalendarServer/trunk/twistedcaldav/timezonestdservice.py	2012-12-07 18:26:55 UTC (rev 10135)
@@ -27,30 +27,27 @@
 
 from twext.python.log import Logger
 from twext.web2 import responsecode
-from txdav.xml import element as davxml
-from twext.web2.dav.http import ErrorResponse
 from twext.web2.dav.method.propfind import http_PROPFIND
 from twext.web2.dav.noneprops import NonePropertyStore
-from twext.web2.http import HTTPError, StatusResponse
+from twext.web2.http import HTTPError, JSONResponse
 from twext.web2.http import Response
-from twext.web2.http import XMLResponse
 from twext.web2.http_headers import MimeType
 from twext.web2.stream import MemoryStream
+from txdav.xml import element as davxml
 
-from twisted.internet.defer import succeed, inlineCallbacks, returnValue,\
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue, \
     DeferredList
 
-from twistedcaldav import timezonexml, xmlutil
+from twistedcaldav import xmlutil
 from twistedcaldav.client.geturl import getURL
 from twistedcaldav.config import config
-from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.extensions import DAVResource,\
+from twistedcaldav.extensions import DAVResource, \
     DAVResourceWithoutChildrenMixin
 from twistedcaldav.ical import tzexpandlocal
 from twistedcaldav.resource import ReadOnlyNoCopyResourceMixIn
-from twistedcaldav.timezones import TimezoneException, TimezoneCache, readVTZ,\
+from twistedcaldav.timezones import TimezoneException, TimezoneCache, readVTZ, \
     addVTZ
-from twistedcaldav.xmlutil import addSubElement, readXMLString
+from twistedcaldav.xmlutil import addSubElement
 
 from pycalendar.calendar import PyCalendar
 from pycalendar.datetime import PyCalendarDateTime
@@ -58,6 +55,7 @@
 
 import hashlib
 import itertools
+import json
 import os
 
 log = Logger()
@@ -69,7 +67,6 @@
     Extends L{DAVResource} to provide timezone service functionality.
     """
 
-
     def __init__(self, parent):
         """
         @param parent: the parent resource of this one.
@@ -80,6 +77,8 @@
 
         self.parent = parent
         self.expandcache = {}
+        self.primary = True
+        self.info_source = None
 
         if config.TimezoneService.Mode == "primary":
             log.info("Using primary Timezone Service")
@@ -89,7 +88,8 @@
             self._initSecondaryService()
         else:
             raise ValueError("Invalid TimezoneService mode: %s" % (config.TimezoneService.Mode,))
-            
+
+
     def _initPrimaryService(self):
         tzpath = TimezoneCache.getDBPath()
         xmlfile = os.path.join(tzpath, "timezones.xml")
@@ -98,9 +98,11 @@
             self.timezones.createNewDatabase()
         else:
             self.timezones.readDatabase()
+        self.info_source = "Primary"
 
+
     def _initSecondaryService(self):
-        
+
         # Must have writeable paths
         tzpath = TimezoneCache.getDBPath()
         xmlfile = config.TimezoneService.XMLInfoPath
@@ -111,24 +113,32 @@
             self.timezones.readDatabase()
         except:
             pass
+        self.info_source = "Secondary"
+        self.primary = False
 
+
     def onStartup(self):
         return self.timezones.onStartup()
 
+
     def deadProperties(self):
         if not hasattr(self, "_dead_properties"):
             self._dead_properties = NonePropertyStore(self)
         return self._dead_properties
 
+
     def etag(self):
         return succeed(None)
 
+
     def checkPreconditions(self, request):
         return None
 
+
     def checkPrivileges(self, request, privileges, recurse=False, principal=None, inherited_aces=None):
         return succeed(None)
 
+
     def defaultAccessControlList(self):
         return davxml.ACL(
             # DAV:Read for all principals (includes anonymous)
@@ -141,21 +151,27 @@
             ),
         )
 
+
     def contentType(self):
-        return MimeType.fromString("text/html; charset=utf-8");
+        return MimeType.fromString("text/html; charset=utf-8")
 
+
     def resourceType(self):
         return None
 
+
     def isCollection(self):
         return False
 
+
     def isCalendarCollection(self):
         return False
 
+
     def isPseudoCalendarCollection(self):
         return False
 
+
     def render(self, request):
         output = """<html>
 <head>
@@ -176,10 +192,11 @@
         """
         The timezone service POST method.
         """
-        
+
         # GET and POST do the same thing
         return self.http_POST(request)
 
+
     def http_POST(self, request):
         """
         The timezone service POST method.
@@ -187,109 +204,99 @@
 
         # Check authentication and access controls
         def _gotResult(_):
-            
+
             if not request.args:
                 # Do normal GET behavior
                 return self.render(request)
-    
+
             action = request.args.get("action", ("",))
             if len(action) != 1:
-                raise HTTPError(StatusResponse(
+                raise HTTPError(JSONResponse(
                     responsecode.BAD_REQUEST,
-                    "Invalid action query parameter",
+                    {
+                        "error": "invalid-action",
+                        "description": "Invalid action query parameter",
+                    },
                 ))
             action = action[0]
-                
+
             action = {
                 "capabilities"  : self.doCapabilities,
                 "list"          : self.doList,
                 "get"           : self.doGet,
                 "expand"        : self.doExpand,
             }.get(action, None)
-            
+
             if action is None:
-                raise HTTPError(StatusResponse(
+                raise HTTPError(JSONResponse(
                     responsecode.BAD_REQUEST,
-                    "Unknown action query parameter",
+                    {
+                        "error": "invalid-action",
+                        "description": "Unknown action query parameter",
+                    },
                 ))
-    
+
             return action(request)
-            
+
         d = self.authorize(request, (davxml.Read(),))
         d.addCallback(_gotResult)
         return d
 
+
     def doCapabilities(self, request):
         """
         Return a list of all timezones known to the server.
         """
-        
-        result = timezonexml.Capabilities(
-            
-            timezonexml.Operation(
-                timezonexml.Action.fromString("capabilities"),
-                timezonexml.Description.fromString("Get capabilities of the server"),
-            ),
-            
-            timezonexml.Operation(
-                timezonexml.Action.fromString("list"),
-                timezonexml.Description.fromString("List timezones on the server"),
-                timezonexml.AcceptParameter(
-                    timezonexml.Name.fromString("changedsince"),
-                    timezonexml.Required.fromString("false"),
-                    timezonexml.Multi.fromString("false"),
-                ),
-            ),
-            
-            timezonexml.Operation(
-                timezonexml.Action.fromString("get"),
-                timezonexml.Description.fromString("Get timezones from the server"),
-                timezonexml.AcceptParameter(
-                    timezonexml.Name.fromString("format"),
-                    timezonexml.Required.fromString("false"),
-                    timezonexml.Multi.fromString("false"),
-                    timezonexml.Value.fromString("text/calendar"),
-                    timezonexml.Value.fromString("text/plain"),
-                ),
-                timezonexml.AcceptParameter(
-                    timezonexml.Name.fromString("tzid"),
-                    timezonexml.Required.fromString("true"),
-                    timezonexml.Multi.fromString("false"),
-                ),
-            ),
-            
-            timezonexml.Operation(
-                timezonexml.Action.fromString("expand"),
-                timezonexml.Description.fromString("Expand timezones from the server"),
-                timezonexml.AcceptParameter(
-                    timezonexml.Name.fromString("tzid"),
-                    timezonexml.Required.fromString("true"),
-                    timezonexml.Multi.fromString("false"),
-                ),
-                timezonexml.AcceptParameter(
-                    timezonexml.Name.fromString("start"),
-                    timezonexml.Required.fromString("false"),
-                    timezonexml.Multi.fromString("false"),
-                ),
-                timezonexml.AcceptParameter(
-                    timezonexml.Name.fromString("end"),
-                    timezonexml.Required.fromString("false"),
-                    timezonexml.Multi.fromString("false"),
-                ),
-            ),
-        )
-        return XMLResponse(responsecode.OK, result)
 
+        result = {
+            "info" : {
+                "primary-source" if self.primary else "secondary_source": self.info_source,
+                "contacts" : [],
+            },
+            "actions" : [
+                {
+                    "name": "capabilities",
+                    "parameters": [],
+                },
+                {
+                    "name": "list",
+                    "parameters": [
+                        {"name": "changedsince", "required": False, "multi": False, },
+                    ],
+                },
+                {
+                    "name": "get",
+                    "parameters": [
+                        {"name": "format", "required": False, "multi": False, "values": ["text/calendar", "text/plain", ], },
+                        {"name": "tzid", "required": True, "multi": False, },
+                    ],
+                },
+                {
+                    "name": "expand",
+                    "parameters": [
+                        {"name": "tzid", "required": True, "multi": False, },
+                        {"name": "start", "required": False, "multi": False, },
+                        {"name": "end", "required": False, "multi": False, },
+                    ],
+                },
+            ]
+        }
+        return JSONResponse(responsecode.OK, result)
+
+
     def doList(self, request):
         """
         Return a list of all timezones known to the server.
         """
-        
+
         changedsince = request.args.get("changedsince", ())
         if len(changedsince) > 1:
-            raise HTTPError(StatusResponse(
+            raise HTTPError(JSONResponse(
                 responsecode.BAD_REQUEST,
-                "Invalid changedsince query parameter",
+                {
+                    "error": "invalid-changedsince",
+                    "description": "Invalid changedsince query parameter",
+                },
             ))
         if len(changedsince) == 1:
             # Validate a date-time stamp
@@ -297,58 +304,67 @@
             try:
                 dt = PyCalendarDateTime.parseText(changedsince)
             except ValueError:
-                raise HTTPError(StatusResponse(
+                raise HTTPError(JSONResponse(
                     responsecode.BAD_REQUEST,
-                    "Invalid changedsince query parameter value",
+                    {
+                        "error": "invalid-changedsince",
+                        "description": "Invalid changedsince query parameter",
+                    },
                 ))
             if not dt.utc():
-                raise HTTPError(StatusResponse(
+                raise HTTPError(JSONResponse(
                     responsecode.BAD_REQUEST,
                     "Invalid changedsince query parameter value",
                 ))
-                
 
         timezones = []
         for tz in self.timezones.listTimezones(changedsince):
-            timezones.append(
-                timezonexml.Summary(
-                    timezonexml.Tzid.fromString(tz.tzid),
-                    timezonexml.LastModified.fromString(tz.dtstamp),
-                    *tuple([timezonexml.Alias.fromString(alias) for alias in tz.aliases])
-                )
-            )
-        result = timezonexml.TimezoneList(
-            timezonexml.Dtstamp.fromString(self.timezones.dtstamp),
-            *timezones
-        )
-        return XMLResponse(responsecode.OK, result)
+            timezones.append({
+                "tzid": tz.tzid,
+                "last-modified": tz.dtstamp,
+                "aliases": tz.aliases,
+            })
+        result = {
+            "dtstamp": self.timezones.dtstamp,
+            "timezones": timezones,
+        }
+        return JSONResponse(responsecode.OK, result)
 
+
     def doGet(self, request):
         """
         Return the specified timezone data.
         """
-        
+
         tzids = request.args.get("tzid", ())
         if len(tzids) != 1:
-            raise HTTPError(StatusResponse(
+            raise HTTPError(JSONResponse(
                 responsecode.BAD_REQUEST,
-                "Invalid tzid query parameter",
+                {
+                    "error": "invalid-tzid",
+                    "description": "Invalid tzid query parameter",
+                },
             ))
 
         format = request.args.get("format", ("text/calendar",))
         if len(format) != 1 or format[0] not in ("text/calendar", "text/plain",):
-            raise HTTPError(StatusResponse(
+            raise HTTPError(JSONResponse(
                 responsecode.BAD_REQUEST,
-                "Invalid format query parameter",
+                {
+                    "error": "invalid-format",
+                    "description": "Invalid format query parameter",
+                },
             ))
         format = format[0]
 
         calendar = self.timezones.getTimezone(tzids[0])
         if calendar is None:
-            raise HTTPError(ErrorResponse(
+            raise HTTPError(JSONResponse(
                 responsecode.NOT_FOUND,
-                (calendarserver_namespace, "invalid-tzid"),
-                "Tzid could not be found",
+                {
+                    "error": "missing-tzid",
+                    "description": "Tzid could not be found",
+                }
             ))
 
         tzdata = calendar.getText()
@@ -358,6 +374,7 @@
         response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (format,)))
         return response
 
+
     def doExpand(self, request):
         """
         Expand a timezone within specified start/end dates.
@@ -365,9 +382,12 @@
 
         tzids = request.args.get("tzid", ())
         if len(tzids) != 1:
-            raise HTTPError(StatusResponse(
+            raise HTTPError(JSONResponse(
                 responsecode.BAD_REQUEST,
-                "Invalid tzid query parameter",
+                {
+                    "error": "invalid-tzid",
+                    "description": "Invalid tzid query parameter",
+                },
             ))
 
         try:
@@ -381,10 +401,12 @@
                 start.setDay(1)
                 start.setMonth(1)
         except ValueError:
-            raise HTTPError(ErrorResponse(
+            raise HTTPError(JSONResponse(
                 responsecode.BAD_REQUEST,
-                (calendarserver_namespace, "valid-start-date"),
-                "Invalid start query parameter",
+                {
+                    "error": "invalid-start",
+                    "description": "Invalid start query parameter",
+                }
             ))
 
         try:
@@ -401,59 +423,59 @@
             if end <= start:
                 raise ValueError()
         except ValueError:
-            raise HTTPError(ErrorResponse(
+            raise HTTPError(JSONResponse(
                 responsecode.BAD_REQUEST,
-                (calendarserver_namespace, "valid-end-date"),
-                "Invalid end query parameter",
+                {
+                    "error": "invalid-end",
+                    "description": "Invalid end query parameter",
+                }
             ))
 
-        results = []
-        
         tzid = tzids[0]
         tzdata = self.timezones.getTimezone(tzid)
         if tzdata is None:
-            raise HTTPError(ErrorResponse(
+            raise HTTPError(JSONResponse(
                 responsecode.NOT_FOUND,
-                (calendarserver_namespace, "invalid-tzid"),
-                "Tzid could not be found",
+                {
+                    "error": "missing-tzid",
+                    "description": "Tzid could not be found",
+                }
             ))
-            
+
         # Now do the expansion (but use a cache to avoid re-calculating TZs)
         observances = self.expandcache.get((tzid, start, end), None)
         if observances is None:
             observances = tzexpandlocal(tzdata, start, end)
             self.expandcache[(tzid, start, end)] = observances
 
-        # Turn into XML
-        results.append(timezonexml.Tzdata(
-            timezonexml.Tzid.fromString(tzid),
-            *[
-                timezonexml.Observance(
-                    timezonexml.Name(name),
-                    timezonexml.Onset(onset),
-                    timezonexml.UTCOffsetFrom(utc_offset_from),
-                    timezonexml.UTCOffsetTo(utc_offset_to),
-                ) for onset, utc_offset_from, utc_offset_to, name in observances
-            ]
-        ))
-        
-        result = timezonexml.Timezones(
-            timezonexml.Dtstamp.fromString(self.timezones.dtstamp),
-            *results
-        )
-        return XMLResponse(responsecode.OK, result)
+        # Turn into JSON
+        result = {
+            "dtstamp": self.timezones.dtstamp,
+            "observances": [
+                {
+                    "name": name,
+                    "onset": onset.getXMLText(),
+                    "utc-offset-from": utc_offset_from,
+                    "utc-offset-to": utc_offset_to,
+                } for onset, utc_offset_from, utc_offset_to, name in observances
+            ],
+        }
+        return JSONResponse(responsecode.OK, result)
 
+
+
 class TimezoneInfo(object):
     """
     Maintains information from an on-disk store of timezone files.
     """
-    
+
     def __init__(self, tzid, aliases, dtstamp, md5):
         self.tzid = tzid
         self.aliases = aliases
         self.dtstamp = dtstamp
         self.md5 = md5
-    
+
+
     @classmethod
     def readXML(cls, node):
         """
@@ -466,7 +488,8 @@
         aliases = tuple([alias_node.text for alias_node in node.findall("alias")])
         md5 = node.findtext("md5")
         return cls(tzid, aliases, dtstamp, md5)
-    
+
+
     def generateXML(self, parent):
         """
         Generate the XML element for this timezone info.
@@ -478,11 +501,13 @@
             xmlutil.addSubElement(node, "alias", alias)
         xmlutil.addSubElement(node, "md5", self.md5)
 
+
+
 class CommonTimezoneDatabase(object):
     """
     Maintains the database of timezones read from an XML file.
     """
-    
+
     def __init__(self, basepath, xmlfile):
         self.basepath = basepath
         self.xmlfile = xmlfile
@@ -490,9 +515,11 @@
         self.timezones = {}
         self.aliases = {}
 
+
     def onStartup(self):
         return succeed(None)
 
+
     def readDatabase(self):
         """
         Read in XML data.
@@ -507,22 +534,24 @@
                     for alias in tz.aliases:
                         self.aliases[alias] = tz.tzid
 
+
     def listTimezones(self, changedsince):
         """
         List timezones (not aliases) possibly changed since a particular dtstamp.
         """
-        
-        for tzid, tzinfo in sorted(self.timezones.items(), key=lambda x:x[0]):
+
+        for tzid, tzinfo in sorted(self.timezones.items(), key=lambda x: x[0]):
             # Ignore those that are aliases
             if tzid in self.aliases:
                 continue
-            
+
             # Detect timestamp changes
             if changedsince and tzinfo.dtstamp <= changedsince:
                 continue
-            
+
             yield tzinfo
 
+
     def getTimezone(self, tzid):
         """
         Generate a PyCalendar containing the requested timezone.
@@ -533,7 +562,7 @@
             vtz = readVTZ(tzid)
             calendar.addComponent(vtz.getComponents()[0].duplicate())
         except TimezoneException:
-            
+
             # Check if an alias exists and create data for that
             if tzid in self.aliases:
                 try:
@@ -552,32 +581,37 @@
 
         return calendar
 
+
     def _dumpTZs(self):
-        
+
         _ignore, root = xmlutil.newElementTreeWithRoot("timezones")
         addSubElement(root, "dtstamp", self.dtstamp)
-        for _ignore,v in sorted(self.timezones.items(), key=lambda x:x[0]):
+        for _ignore, v in sorted(self.timezones.items(), key=lambda x: x[0]):
             v.generateXML(root)
         xmlutil.writeXML(self.xmlfile, root)
-    
+
+
     def _buildAliases(self):
         """
         Rebuild aliases mappings from current tzinfo.
         """
-        
+
         self.aliases = {}
         for tzinfo in self.timezones.values():
             for alias in tzinfo.aliases:
                 self.aliases[alias] = tzinfo.tzid
 
+
+
 class PrimaryTimezoneDatabase(CommonTimezoneDatabase):
     """
     Maintains the database of timezones read from an XML file.
     """
-    
+
     def __init__(self, basepath, xmlfile):
         super(PrimaryTimezoneDatabase, self).__init__(basepath, xmlfile)
 
+
     def createNewDatabase(self):
         """
         Create a new DB xml file from scratch by scanning zoneinfo.
@@ -587,6 +621,7 @@
         self._scanTZs("")
         self._dumpTZs()
 
+
     def _scanTZs(self, path, checkIfChanged=False):
         # Read in all timezone files first
         for item in os.listdir(os.path.join(self.basepath, path)):
@@ -601,7 +636,7 @@
                 except IOError:
                     log.error("Unable to read timezone file: %s" % (fullPath,))
                     continue
-                
+
                 if checkIfChanged:
                     oldtz = self.timezones.get(tzid)
                     if oldtz != None and oldtz.md5 == md5:
@@ -609,14 +644,14 @@
                     self.changeCount += 1
                     self.changed.add(tzid)
                 self.timezones[tzid] = TimezoneInfo(tzid, (), self.dtstamp, md5)
-        
+
         # Try links (aliases) file
         try:
             aliases = open(os.path.join(self.basepath, "links.txt")).read()
         except IOError, e:
             log.error("Unable to read links.txt file: %s" % (str(e),))
             aliases = ""
-        
+
         try:
             for alias in aliases.splitlines():
                 alias_from, alias_to = alias.split()
@@ -630,8 +665,8 @@
                     log.error("Missing alias from '%s' to '%s'" % (alias_from, alias_to,))
         except ValueError:
             log.error("Unable to parse links.txt file: %s" % (str(e),))
-            
-    
+
+
     def updateDatabase(self):
         """
         Update existing DB info by comparing md5's.
@@ -643,11 +678,13 @@
         if self.changeCount:
             self._dumpTZs()
 
+
+
 class SecondaryTimezoneDatabase(CommonTimezoneDatabase):
     """
     Caches a database of timezones from another timezone service.
     """
-    
+
     def __init__(self, basepath, xmlfile, uri):
         super(SecondaryTimezoneDatabase, self).__init__(basepath, xmlfile)
         self.uri = uri
@@ -658,22 +695,24 @@
 
         if not os.path.exists(self.basepath):
             os.makedirs(self.basepath)
-            
+
         # Paths need to be writeable
         if not os.access(basepath, os.W_OK):
             raise ValueError("Secondary Timezone Service needs writeable zoneinfo path at: %s" % (basepath,))
         if os.path.exists(xmlfile) and not os.access(xmlfile, os.W_OK):
             raise ValueError("Secondary Timezone Service needs writeable xmlfile path at: %s" % (xmlfile,))
 
+
     def onStartup(self):
         return self.syncWithServer()
 
+
     @inlineCallbacks
     def syncWithServer(self):
         """
         Sync local data with that from the server we are replicating.
         """
-        
+
         log.debug("Sync'ing with secondary server")
         result = (yield self._getTimezoneListFromServer())
         if result is None:
@@ -681,18 +720,18 @@
             log.debug("No changes on secondary server")
             returnValue(None)
         newdtstamp, newtimezones = result
-        
+
         # Compare timezone infos
-        
+
         # New ones on the server
         newtzids = set(newtimezones.keys()) - set(self.timezones.keys())
-        
+
         # Check for changes
         changedtzids = set()
         for tzid in set(newtimezones.keys()) & set(self.timezones.keys()):
             if self.timezones[tzid].dtstamp < newtimezones[tzid].dtstamp:
                 changedtzids.add(tzid)
-        
+
         log.debug("Fetching %d new, %d changed timezones on secondary server" % (len(newtzids), len(changedtzids),))
 
         # Now apply changes - do requests in parallel for speedier fetching
@@ -708,15 +747,16 @@
         self._buildAliases()
 
         log.debug("Sync with secondary server complete")
-        
+
         returnValue((len(newtzids), len(changedtzids),))
-        
+
+
     @inlineCallbacks
     def _discoverServer(self):
         """
         Make sure we know the timezone service path
         """
-        
+
         if self.uri is None:
             if config.TimezoneService.SecondaryService.Host:
                 self.uri = "https://%s/.well-known/timezone" % (config.TimezoneService.SecondaryService.Host,)
@@ -724,7 +764,7 @@
                 self.uri = config.TimezoneService.SecondaryService.URI
         elif not self.uri.startswith("https:") and not self.uri.startswith("http:"):
             self.uri = "https://%s/.well-known/timezone" % (self.uri,)
-            
+
         testURI = "%s?action=capabilities" % (self.uri,)
         log.debug("Discovering secondary server: %s" % (testURI,))
         response = (yield getURL(testURI))
@@ -732,29 +772,30 @@
             log.error("Unable to discover secondary server: %s" % (testURI,))
             self.discovered = False
             returnValue(False)
-        
+
         # Cache the redirect target
         if hasattr(response, "location"):
-            self.uri = response.location 
+            self.uri = response.location
             log.debug("Redirected secondary server to: %s" % (self.uri,))
 
         # TODO: Ignoring the data from capabilities for now
 
         self.discovered = True
         returnValue(True)
-    
+
+
     @inlineCallbacks
     def _getTimezoneListFromServer(self):
         """
         Retrieve the timezone list from the specified server
         """
-        
+
         # Make sure we have the server
         if not self.discovered:
             result = (yield self._discoverServer())
             if not result:
                 returnValue(None)
-        
+
         # List all from the server
         url = "%s?action=list" % (self.uri,)
         if self.dtstamp:
@@ -763,26 +804,30 @@
         response = (yield getURL(url))
         if response is None or response.code / 100 != 2:
             returnValue(None)
-        
+
         ct = response.headers.getRawHeaders("content-type", ("bogus/type",))[0]
         ct = ct.split(";", 1)
         ct = ct[0]
-        if ct not in ("application/xml", "text/xml",):
+        if ct not in ("application/json",):
             returnValue(None)
-        
-        etroot, _ignore = readXMLString(response.data, timezonexml.TimezoneList.sname())
-        dtstamp = etroot.findtext(timezonexml.Dtstamp.sname())
-        timezones = {}
-        for summary in etroot.findall(timezonexml.Summary.sname()):
-            tzid = summary.findtext(timezonexml.Tzid.sname())
-            lastmod = summary.findtext(timezonexml.LastModified.sname())
-            aliases = tuple([alias_node.text for alias_node in summary.findall(timezonexml.Alias.sname())])
-            timezones[tzid] = TimezoneInfo(tzid, aliases, lastmod, None)
 
+        try:
+            jroot = json.loads(response.data)
+            dtstamp = jroot["dtstamp"]
+            timezones = {}
+            for timezone in jroot["timezones"]:
+                tzid = timezone["tzid"]
+                lastmod = timezone["last-modified"]
+                aliases = timezone.get("aliases", ())
+                timezones[tzid] = TimezoneInfo(tzid, aliases, lastmod, None)
+        except ValueError, KeyError:
+            log.debug("Failed to parse JSON timezone list response: %s" % (response.data,))
+            returnValue(None)
         log.debug("Got %s timezones from secondary server" % (len(timezones),))
-        
+
         returnValue((dtstamp, timezones,))
 
+
     @inlineCallbacks
     def _getTimezoneFromServer(self, tzinfo):
         # List all from the server
@@ -791,14 +836,14 @@
         response = (yield getURL(url))
         if response is None or response.code / 100 != 2:
             returnValue(None)
-        
+
         ct = response.headers.getRawHeaders("content-type", ("bogus/type",))[0]
         ct = ct.split(";", 1)
         ct = ct[0]
         if ct not in ("text/calendar",):
             log.error("Invalid content-type '%s' for tzid : %s" % (ct, tzinfo.tzid,))
             returnValue(None)
-        
+
         ical = response.data
         try:
             calendar = PyCalendar.parseText(ical)
@@ -808,7 +853,7 @@
         ical = calendar.getText()
 
         tzinfo.md5 = hashlib.md5(ical).hexdigest()
-        
+
         try:
             tzpath = os.path.join(self.basepath, tzinfo.tzid) + ".ics"
             if not os.path.exists(os.path.dirname(tzpath)):
@@ -821,6 +866,7 @@
         else:
             self.timezones[tzinfo.tzid] = tzinfo
 
+
     def _removeTimezone(self, tzid):
         tzpath = os.path.join(self.basepath, tzid) + ".ics"
         try:
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20121207/398478ec/attachment-0001.html>


More information about the calendarserver-changes mailing list