[CalendarServer-changes] [5486] CalendarServer/trunk/twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Fri Apr 16 12:52:35 PDT 2010


Revision: 5486
          http://trac.macosforge.org/projects/calendarserver/changeset/5486
Author:   cdaboo at apple.com
Date:     2010-04-16 12:52:34 -0700 (Fri, 16 Apr 2010)
Log Message:
-----------
Re-work query and filter behavior to match the changes we did for CalDAV.

Modified Paths:
--------------
    CalendarServer/trunk/twistedcaldav/caldavxml.py
    CalendarServer/trunk/twistedcaldav/carddavxml.py
    CalendarServer/trunk/twistedcaldav/datafilters/filter.py
    CalendarServer/trunk/twistedcaldav/method/report_addressbook_multiget.py
    CalendarServer/trunk/twistedcaldav/method/report_addressbook_query.py
    CalendarServer/trunk/twistedcaldav/method/report_common.py
    CalendarServer/trunk/twistedcaldav/query/addressbookquery.py
    CalendarServer/trunk/twistedcaldav/query/queryfilter.py
    CalendarServer/trunk/twistedcaldav/resource.py

Added Paths:
-----------
    CalendarServer/trunk/twistedcaldav/datafilters/addressdata.py
    CalendarServer/trunk/twistedcaldav/query/addressbookqueryfilter.py

Modified: CalendarServer/trunk/twistedcaldav/caldavxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/caldavxml.py	2010-04-16 14:11:11 UTC (rev 5485)
+++ CalendarServer/trunk/twistedcaldav/caldavxml.py	2010-04-16 19:52:34 UTC (rev 5486)
@@ -33,9 +33,7 @@
 
 from twext.python.log import Logger
 
-from twistedcaldav.dateops import clipPeriod, timeRangesOverlap
 from twistedcaldav.ical import Component as iComponent
-from twistedcaldav.ical import Property as iProperty
 from twistedcaldav.ical import parse_date_or_datetime
 
 log = Logger()

Modified: CalendarServer/trunk/twistedcaldav/carddavxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/carddavxml.py	2010-04-16 14:11:11 UTC (rev 5485)
+++ CalendarServer/trunk/twistedcaldav/carddavxml.py	2010-04-16 19:52:34 UTC (rev 5486)
@@ -25,9 +25,6 @@
 See draft spec: 
 """
 
-from twistedcaldav.vcard import Property as iProperty
-from twistedcaldav.vcard import Component
-
 from twext.web2.dav import davxml
 
 ##
@@ -35,7 +32,6 @@
 ##
 
 carddav_namespace = "urn:ietf:params:xml:ns:carddav"
-addressbookserver_namespace = "http://addressbookserver.org/ns/"
 
 carddav_compliance = (
     "addressbook",
@@ -59,69 +55,6 @@
     """
     namespace = carddav_namespace
 
-class CardDAVFilterElement (CardDAVElement):
-    """
-    CardDAV filter element.
-    """
-    def __init__(self, *children, **attributes):
-
-        super(CardDAVFilterElement, self).__init__(*children, **attributes)
-
-        qualifier = None
-        filters = []
-
-        for child in self.children:
-            qname = child.qname()
-            
-            if qname in (
-                (carddav_namespace, "is-not-defined"),
-            ):
-                if qualifier is not None:
-                    raise ValueError("Only one of CardDAV:is-not-defined allowed")
-                qualifier = child
-
-            else:
-                filters.append(child)
-
-        if qualifier and (len(filters) != 0):
-            raise ValueError("No other tests allowed when CardDAV:is-not-defined is present")
-        
-        if self.qname() == (carddav_namespace, "prop-filter"):
-            propfilter_test = attributes.get("test", "anyof")
-            if propfilter_test not in ("anyof", "allof"):
-                raise ValueError("Test must be only one of anyof, allof")
-        else:
-            propfilter_test = "anyof"
-
-        self.propfilter_test = propfilter_test
-        self.qualifier   = qualifier
-        self.filters     = filters
-        self.filter_name = attributes["name"]
-        if isinstance(self.filter_name, unicode):
-            self.filter_name = self.filter_name.encode("utf-8")
-        self.defined     = not self.qualifier or (self.qualifier.qname() != (carddav_namespace, "is-not-defined"))
-
-    def match(self, item):
-        """
-        Returns True if the given address book item (either a property or parameter value)
-        matches this filter, False otherwise.
-        """
-        
-        # Always return True for the is-not-defined case as the result of this will
-        # be negated by the caller
-        if not self.defined: return True
-
-        if self.qualifier and not self.qualifier.match(item): return False
-
-        if len(self.filters) > 0:
-            allof = self.propfilter_test == "allof"
-            for filter in self.filters:
-                if allof != filter._match(item):
-                    return not allof
-            return allof
-        else:
-            return True
-
 class AddressBookHomeSet (CardDAVElement):
     """
     The address book collections URLs for this principal.
@@ -336,67 +269,21 @@
         
         return False
 
-    def elementFromResource(self, resource):
+    def address(self):
         """
-        Return a new AddressData element comprised of the possibly filtered
-        address data from the specified resource. If no filter is being applied
-        read the data directly from the resource without parsing it. If a filter
-        is required, parse the vCard data and filter using this AddressData.
-        @param resource: the resource whose address data is to be returned.
-        @return: an L{AddressData} with the (filtered) address data.
+        Returns an address component derived from this element.
         """
-        # Check for filtering or not
-        if self.children:
-            filtered = self.getFromvCard(resource.vCard())
-            return AddressData.fromAddress(filtered)
-        else:
-            return resource.vCardXML()
+        for data in self.children:
+            if not isinstance(data, davxml.PCDATAElement):
+                return None
+            else:
+                # We guaranteed in __init__() that there is only one child...
+                break
 
-    def elementFromAddress(self, address):
-        """
-        Return a new AddressData element comprised of the possibly filtered
-        address.
-        @param address: the address that is to be filtered and returned.
-        @return: an L{AddressData} with the (filtered) address data.
-        """
-        
-        # Check for filtering or not
-        filtered = self.getFromvCard(address)
-        return AddressData.fromAddress(filtered)
+        return None
 
-    def getFromvCard(self, address):
+    def addressData(self):
         """
-        Returns an address object containing the data in the given vCard
-        which is specified by this AddressData.
-        """
-        if address.name() != "VCARD":
-            raise ValueError("Not a vCard: %r" % (address,))
-
-        # Empty element: get all data
-        if not self.children: return address
-
-        # property filtering
-        # copy requested properties
-        vcard = Component("VCARD")
-        allProps = True
-        for property in self.children:
-            if isinstance(property, Property):
-                allProps = False
-                for addressProperty in address.properties(property.attributes["name"]):
-                    vcard.addProperty(addressProperty)
-        
-        # add required properties
-        if allProps:
-            vcard = address
-        else:
-            for requiredProperty in ('N', 'FN', 'VERSION'):
-                if not vcard.hasProperty(requiredProperty):
-                    vcard.addProperty(address.getProperty(requiredProperty))
-
-        return vcard
-
-    def address(self):
-        """
         Returns an address component derived from this element.
         """
         for data in self.children:
@@ -406,7 +293,7 @@
                 # We guaranteed in __init__() that there is only one child...
                 break
 
-        return None # TODO: iComponent.fromString(str(data))
+        return str(data)
 
 
 class AllProperties (CardDAVEmptyElement):
@@ -453,49 +340,8 @@
 
     allowed_children = { (carddav_namespace, "prop-filter"): (0, None) }
     allowed_attributes = { "test": False }
-
-
-    def __init__(self, *children, **attributes):
-
-        super(Filter, self).__init__(*children, **attributes)
-
-        filter_test = attributes.get("test", "anyof")
-        if filter_test not in ("anyof", "allof"):
-            raise ValueError("Test must be only one of anyof, allof")
         
-        self.filter_test = filter_test
-
-    def match(self, vcard):
-        """
-        Returns True if the given address property matches this filter, False
-        otherwise. Empty element means always match.
-        """
- 
-        if len(self.children) > 0:
-            allof = self.filter_test == "allof"
-            for propfilter in self.children:
-                if allof != propfilter._match(vcard):
-                    return not allof
-            return allof
-        else:
-            return True
-
-    def valid(self):
-        """
-        Indicate whether this filter element's structure is valid wrt vCard
-        data object model.
-        
-        @return: True if valid, False otherwise
-        """
-        
-        # Test each property
-        for propfilter in self.children:
-            if not propfilter.valid():
-                return False
-        else:
-            return True
-        
-class PropertyFilter (CardDAVFilterElement):
+class PropertyFilter (CardDAVElement):
     """
     Limits a search to specific properties.
     (CardDAV-access-09, section 10.5.1)
@@ -512,26 +358,7 @@
         "test": False,
     }
 
-    def _match(self, vcard):
-        # At least one property must match (or is-not-defined is set)
-        for property in vcard.properties():
-            if property.name() == self.filter_name and self.match(property): break
-        else:
-            return not self.defined
-        return self.defined
-
-    def valid(self):
-        """
-        Indicate whether this filter element's structure is valid wrt vCard
-        data object model.
-        
-        @return:      True if valid, False otherwise
-        """
-        
-        # No tests
-        return True
-
-class ParameterFilter (CardDAVFilterElement):
+class ParameterFilter (CardDAVElement):
     """
     Limits a search to specific parameters.
     (CardDAV, section 10.5.2)
@@ -544,17 +371,6 @@
     }
     allowed_attributes = { "name": True }
 
-    def _match(self, property):
-
-        # At least one parameter must match (or is-not-defined is set)
-        result = not self.defined
-        for parameterName in property.params().keys():
-            if parameterName == self.filter_name and self.match(property.params()[parameterName]):
-                result = self.defined
-                break
-
-        return result
-
 class Limit (davxml.WebDAVElement):
     """
     Client supplied limit for reports.
@@ -580,13 +396,6 @@
     """
     name = "is-not-defined"
 
-    def match(self, component):
-        # Oddly, this needs always to return True so that it appears there is
-        # a match - but we then "negate" the result if is-not-defined is set.
-        # Actually this method should never be called as we special case the
-        # is-not-defined option.
-        return True
-
 class TextMatch (CardDAVTextElement):
     """
     Specifies a substring match on a property or parameter value.
@@ -610,81 +419,6 @@
         "match-type": False
     }
 
-    def __init__(self, *children, **attributes):
-        super(TextMatch, self).__init__(*children, **attributes)
-
-        if "collation" in attributes:
-            self.collation = attributes["collation"]
-        else:
-            self.collation = "i;unicode-casemap"
-
-        if "negate-condition" in attributes:
-            self.negate = attributes["negate-condition"]
-            if self.negate not in ("yes", "no"):
-                self.negate = "no"
-            self.negate = {"yes": True, "no": False}[self.negate]
-        else:
-            self.negate = False
-
-        if "match-type" in attributes:
-            self.match_type = attributes["match-type"]
-            if self.match_type not in (
-                "equals",
-                "contains",
-                "starts-with",
-                "ends-with",
-            ):
-                self.match_type = "contains"
-        else:
-            self.match_type = "contains"
-
-    def _match(self, item):
-        """
-        Match the text for the item.
-        If the item is a property, then match the property value,
-        otherwise it may be a list of parameter values - try to match anyone of those
-        """
-        if item is None: return False
-
-        if isinstance(item, iProperty):
-            values = [item.value()]
-        else:
-            values = item
-
-        test = unicode(str(self), "utf-8").lower()
-
-        def _textCompare(s):
-            s = s.lower()
-            
-            #print("test=%r, s=%r, matchType=%r" % (test, s, self.match_type))
-            
-            if self.match_type == "equals":
-                return s == test
-            elif self.match_type == "contains":
-                return s.find(test) != -1 
-            elif self.match_type == "starts-with":
-                return s.startswith(test)
-            elif self.match_type == "ends-with":
-                return s.endswith(test)
-            else:
-                return False
-
-        for value in values:
-            # NB Its possible that we have a text list value which appears as a Python list,
-            # so we need to check for that an iterate over the list.
-            if isinstance(value, list):
-                for subvalue in value:
-                    if _textCompare(unicode(subvalue)):
-                        return not self.negate
-            else:
-                if _textCompare(unicode(value)):
-                    return not self.negate
-        
-        return self.negate
-
-    def match(self, item):
-        return self._match(item)
-
 class AddressBookMultiGet (CardDAVElement):
     """
     CardDAV report used to retrieve specific vCard items via their URIs.

Added: CalendarServer/trunk/twistedcaldav/datafilters/addressdata.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/datafilters/addressdata.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/datafilters/addressdata.py	2010-04-16 19:52:34 UTC (rev 5486)
@@ -0,0 +1,96 @@
+##
+# Copyright (c) 2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+from twistedcaldav.carddavxml import AllProperties
+from twistedcaldav.datafilters.filter import AddressFilter
+from twistedcaldav.vcard import Component
+
+__all__ = [
+    "AddressDataFilter",
+]
+
+class AddressDataFilter(AddressFilter):
+    """
+    Filter using the CARDDAV:address-data element specification
+    """
+
+    def __init__(self, addressdata):
+        """
+        
+        @param addressdata: the XML element describing how to filter
+        @type addressdata: L{AddressData}
+        """
+        
+        self.addressdata = addressdata
+    
+    def filter(self, vcard):
+        """
+        Filter the supplied vCard (vobject) data using the request information.
+
+        @param vcard: vCard object
+        @type vcard: L{Component} or C{str}
+        
+        @return: L{Component} for the filtered vcard data
+        """
+        
+        # Empty element: get all data
+        if not self.addressdata.children:
+            return vcard
+
+        # Make sure input is valid
+        vcard = self.validAddress(vcard)
+
+        # Filter data based on any provided CARDAV:prop element, or use all current data
+        if self.addressdata.properties:
+            vcard = self.propFilter(self.addressdata.properties, vcard)
+        
+        return vcard
+
+    def propFilter(self, properties, vcard):
+        """
+        Returns a vCard component object filtered according to the properties.
+        """
+
+        result = Component("VCARD")
+
+        xml_properties = properties
+
+        # Empty element means do all properties and components
+        if xml_properties is None:
+            xml_properties = AllProperties()
+
+        if xml_properties is not None:
+            if xml_properties == AllProperties():
+                for vcard_property in vcard.properties():
+                    result.addProperty(vcard_property)
+            else:
+                for xml_property in xml_properties:
+                    name = xml_property.property_name
+                    for vcard_property in vcard.properties(name):
+                        result.addProperty(vcard_property)
+                        
+                # add required properties
+                for requiredProperty in ('N', 'FN', 'VERSION'):
+                    if not result.hasProperty(requiredProperty):
+                        result.addProperty(vcard.getProperty(requiredProperty))
+
+        return result
+
+    def merge(self, vcardnew, vcardold):
+        """
+        Address-data merging does not happen
+        """
+        raise NotImplementedError

Modified: CalendarServer/trunk/twistedcaldav/datafilters/filter.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/datafilters/filter.py	2010-04-16 14:11:11 UTC (rev 5485)
+++ CalendarServer/trunk/twistedcaldav/datafilters/filter.py	2010-04-16 19:52:34 UTC (rev 5486)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
+# Copyright (c) 2009-2010 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -13,10 +13,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
-from twistedcaldav.ical import Component
 
+from twistedcaldav.ical import Component as iComponent
+from twistedcaldav.vcard import Component as vComponent
+
 __all__ = [
     "CalendarFilter",
+    "AddressFilter",
 ]
 
 class CalendarFilter(object):
@@ -55,7 +58,7 @@
         # If we were passed a string, parse it out as a Component
         if isinstance(ical, str):
             try:
-                ical = Component.fromString(ical)
+                ical = iComponent.fromString(ical)
             except ValueError:
                 raise ValueError("Not a calendar: %r" % (ical,))
         
@@ -63,3 +66,48 @@
             raise ValueError("Not a calendar: %r" % (ical,))
         
         return ical
+
+class AddressFilter(object):
+    """
+    Abstract class that defines a vCard filter/merge object
+    """
+
+
+    def __init__(self):
+        pass
+    
+    def filter(self, vcard):
+        """
+        Filter the supplied vCard (vobject) data using the request information.
+
+        @param vcard: iCalendar object
+        @type vcard: L{Component}
+        
+        @return: L{Component} for the filtered vcard data
+        """
+        raise NotImplementedError
+    
+    def merge(self, vcardnew, vcardold):
+        """
+        Merge the old vcard (vobject) data into the new vcard data using the request information.
+        
+        @param vcardnew: new vcard object to merge data into
+        @type vcardnew: L{Component}
+        @param vcardold: old vcard data to merge data from
+        @type vcardold: L{Component}
+        """
+        raise NotImplementedError
+
+    def validAddress(self, vcard):
+
+        # If we were passed a string, parse it out as a Component
+        if isinstance(vcard, str):
+            try:
+                vcard = vComponent.fromString(vcard)
+            except ValueError:
+                raise ValueError("Not a vcard: %r" % (vcard,))
+        
+        if vcard is None or vcard.name() != "VCARD":
+            raise ValueError("Not a vcard: %r" % (vcard,))
+        
+        return vcard

Modified: CalendarServer/trunk/twistedcaldav/method/report_addressbook_multiget.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_addressbook_multiget.py	2010-04-16 14:11:11 UTC (rev 5485)
+++ CalendarServer/trunk/twistedcaldav/method/report_addressbook_multiget.py	2010-04-16 19:52:34 UTC (rev 5486)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2010 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -40,8 +40,6 @@
 
 log = Logger()
 
-max_number_of_addressbook_multigets = 5000
-
 @inlineCallbacks
 def report_urn_ietf_params_xml_ns_carddav_addressbook_multiget(self, request, multiget):
     """
@@ -70,7 +68,7 @@
     request.extendedLogItems["rcount"] = len(resources)
     
     # Check size of results is within limit
-    if len(resources) > max_number_of_addressbook_multigets:
+    if len(resources) > config.MaxAddressBookMultigetHrefs:
         log.err("Too many results in multiget report: %d" % len(resources))
         raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, davxml.NumberOfMatchesWithinLimits()))
 
@@ -91,11 +89,6 @@
     else:
         raise AssertionError("We shouldn't be here")
 
-    # Check size of results is within limit
-    if len(resources) > config.MaxAddressBookMultigetHrefs:
-        log.err("Too many results in multiget report: %d" % len(resources))
-        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (dav_namespace, "number-of-matches-within-limits")))
-
     """
     Three possibilities exist:
         

Modified: CalendarServer/trunk/twistedcaldav/method/report_addressbook_query.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_addressbook_query.py	2010-04-16 14:11:11 UTC (rev 5485)
+++ CalendarServer/trunk/twistedcaldav/method/report_addressbook_query.py	2010-04-16 19:52:34 UTC (rev 5486)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2010 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -36,6 +36,7 @@
 from twistedcaldav.config import config
 from twistedcaldav.carddavxml import carddav_namespace, NResults
 from twistedcaldav.method import report_common
+from twistedcaldav.query import addressbookqueryfilter
 
 log = Logger()
 
@@ -57,7 +58,8 @@
 
     responses = []
 
-    filter = addressbook_query.filter
+    xmlfilter = addressbook_query.filter
+    filter = addressbookqueryfilter.Filter(xmlfilter)
     query  = addressbook_query.query
     limit = addressbook_query.limit
 
@@ -275,7 +277,6 @@
         yield report_common.applyToAddressBookCollections(self, request, request.uri, depth, doQuery, (davxml.Read(),))
     except NumberOfMatchesWithinLimits, e:
         self.log_info("Too many matching components in addressbook-query report. Limited to %d items" % e.maxLimit())
-        #raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, davxml.NumberOfMatchesWithinLimits()))
         responses.append(davxml.StatusResponse(
                         davxml.HRef.fromString(request.uri),
                         davxml.Status.fromResponseCode(responsecode.INSUFFICIENT_STORAGE_SPACE),

Modified: CalendarServer/trunk/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_common.py	2010-04-16 14:11:11 UTC (rev 5485)
+++ CalendarServer/trunk/twistedcaldav/method/report_common.py	2010-04-16 19:52:34 UTC (rev 5486)
@@ -56,9 +56,11 @@
 from twistedcaldav import caldavxml
 from twistedcaldav import carddavxml
 from twistedcaldav.caldavxml import caldav_namespace, CalendarData
+from twistedcaldav.carddavxml import AddressData
 from twistedcaldav.customxml import TwistedCalendarAccessProperty
 from twistedcaldav.datafilters.calendardata import CalendarDataFilter
 from twistedcaldav.datafilters.privateevents import PrivateEventFilter
+from twistedcaldav.datafilters.addressdata import AddressDataFilter
 from twistedcaldav.dateops import clipPeriod, normalizePeriodList, timeRangesOverlap
 from twistedcaldav.ical import Component, Property, iCalendarProductID
 from twistedcaldav.instance import InstanceList
@@ -123,7 +125,6 @@
     # First check the privilege on this resource
     if privileges:
         try:
-            print ("DeleteResource.applyToAddressBookCollections(1.5)")
             yield resource.checkPrivileges(request, privileges)
         except AccessDeniedError:
             returnValue( None )
@@ -135,12 +136,13 @@
         resources = [(resource, request_uri)]
     else:
         resources = []
-        yield resource.findCalendarCollections(depth, request, lambda x, y: resources.append((x, y)), privileges = privileges)
+        yield resource.findAddressBookCollections(depth, request, lambda x, y: resources.append((x, y)), privileges = privileges)
          
     for addrresource, uri in resources:
-        yield apply(addrresource, uri)
+        result = yield apply(addrresource, uri)
+        if not result:
+            break
 
-
 def responseForHref(request, responses, href, resource, calendar, timezone, propertiesForResource, propertyreq, isowner=True, vcard=None):
     """
     Create an appropriate property status response for the given resource.
@@ -352,12 +354,10 @@
             continue
     
         if isinstance(property, carddavxml.AddressData):
-            if vcard:
-                propvalue = property.elementFromAddress(vcard)
-            else:
-                propvalue = property.elementFromResource(resource)
-            if propvalue is None:
-                raise ValueError("Invalid CardDAV:address-data for request: %r" % (property,))
+            if vcard is None:
+                vcard = (yield resource.vCard())
+            filtered = AddressDataFilter(property).filter(vcard)
+            propvalue = AddressData().fromAddress(filtered)
             properties_by_status[responsecode.OK].append(propvalue)
             continue
     

Modified: CalendarServer/trunk/twistedcaldav/query/addressbookquery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/query/addressbookquery.py	2010-04-16 14:11:11 UTC (rev 5485)
+++ CalendarServer/trunk/twistedcaldav/query/addressbookquery.py	2010-04-16 19:52:34 UTC (rev 5486)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2006-2009 Apple Inc. All rights reserved.
+# Copyright (c) 2006-2010 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -26,8 +26,7 @@
     "sqladdressbookquery",
 ]
 
-from twistedcaldav.query import sqlgenerator
-from twistedcaldav.query import expression
+from twistedcaldav.query import expression, sqlgenerator
 from twistedcaldav import carddavxml
 
 # SQL Index column (field) names

Added: CalendarServer/trunk/twistedcaldav/query/addressbookqueryfilter.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/query/addressbookqueryfilter.py	                        (rev 0)
+++ CalendarServer/trunk/twistedcaldav/query/addressbookqueryfilter.py	2010-04-16 19:52:34 UTC (rev 5486)
@@ -0,0 +1,290 @@
+##
+# Copyright (c) 2010 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Object model of CARDAV:filter element used in an addressbook-query.
+"""
+
+__all__ = [
+    "Filter",
+]
+
+from twext.python.log import Logger
+
+from twistedcaldav.carddavxml import carddav_namespace
+from twistedcaldav.vcard import Property
+
+log = Logger()
+
+class FilterBase(object):
+    """
+    Determines which matching components are returned.
+    """
+
+    def __init__(self, xml_element):
+        self.xmlelement = xml_element
+
+    def match(self, item, access=None):
+        raise NotImplementedError
+
+    def valid(self, level=0):
+        raise NotImplementedError
+
+class Filter(FilterBase):
+    """
+    Determines which matching components are returned.
+    """
+
+    def __init__(self, xml_element):
+
+        super(Filter, self).__init__(xml_element)
+
+        filter_test = xml_element.attributes.get("test", "anyof")
+        if filter_test not in ("anyof", "allof"):
+            raise ValueError("Test must be only one of anyof, allof")
+        
+        self.filter_test = filter_test
+
+        self.children = [PropertyFilter(child) for child in xml_element.children]
+
+    def match(self, vcard):
+        """
+        Returns True if the given address property matches this filter, False
+        otherwise. Empty element means always match.
+        """
+        
+        if len(self.children) > 0:
+            allof = self.filter_test == "allof"
+            for propfilter in self.children:
+                if allof != propfilter._match(vcard):
+                    return not allof
+            return allof
+        else:
+            return True
+
+    def valid(self):
+        """
+        Indicate whether this filter element's structure is valid wrt vCard
+        data object model.
+        
+        @return: True if valid, False otherwise
+        """
+        
+        # Test each property
+        for propfilter in self.children:
+            if not propfilter.valid():
+                return False
+        else:
+            return True
+
+class FilterChildBase(FilterBase):
+    """
+    CardDAV filter element.
+    """
+
+    def __init__(self, xml_element):
+
+        super(FilterChildBase, self).__init__(xml_element)
+
+        qualifier = None
+        filters = []
+
+        for child in xml_element.children:
+            qname = child.qname()
+            
+            if qname in (
+                (carddav_namespace, "is-not-defined"),
+            ):
+                if qualifier is not None:
+                    raise ValueError("Only one of CardDAV:is-not-defined allowed")
+                qualifier = IsNotDefined(child)
+
+            elif qname == (carddav_namespace, "text-match"):
+                filters.append(TextMatch(child))
+
+            elif qname == (carddav_namespace, "param-filter"):
+                filters.append(ParameterFilter(child))
+            else:
+                raise ValueError("Unknown child element: %s" % (qname,))
+
+        if qualifier and isinstance(qualifier, IsNotDefined) and (len(filters) != 0):
+            raise ValueError("No other tests allowed when CardDAV:is-not-defined is present")
+            
+        if xml_element.qname() == (carddav_namespace, "prop-filter"):
+            propfilter_test = xml_element.attributes.get("test", "anyof")
+            if propfilter_test not in ("anyof", "allof"):
+                raise ValueError("Test must be only one of anyof, allof")
+        else:
+            propfilter_test = "anyof"
+
+        self.propfilter_test = propfilter_test
+        self.qualifier = qualifier
+        self.filters = filters
+        self.filter_name = xml_element.attributes["name"]
+        if isinstance(self.filter_name, unicode):
+            self.filter_name = self.filter_name.encode("utf-8")
+        self.defined = not self.qualifier or not isinstance(qualifier, IsNotDefined)
+
+    def match(self, item):
+        """
+        Returns True if the given address book item (either a property or parameter value)
+        matches this filter, False otherwise.
+        """
+        
+        # Always return True for the is-not-defined case as the result of this will
+        # be negated by the caller
+        if not self.defined: return True
+
+        if self.qualifier and not self.qualifier.match(item): return False
+
+        if len(self.filters) > 0:
+            allof = self.propfilter_test == "allof"
+            for filter in self.filters:
+                if allof != filter._match(item):
+                    return not allof
+            return allof
+        else:
+            return True
+
+class PropertyFilter (FilterChildBase):
+    """
+    Limits a search to specific properties.
+    """
+
+    def _match(self, vcard):
+        # At least one property must match (or is-not-defined is set)
+        for property in vcard.properties():
+            if property.name() == self.filter_name and self.match(property): break
+        else:
+            return not self.defined
+        return self.defined
+
+    def valid(self):
+        """
+        Indicate whether this filter element's structure is valid wrt vCard
+        data object model.
+        
+        @return:      True if valid, False otherwise
+        """
+        
+        # No tests
+        return True
+
+class ParameterFilter (FilterChildBase):
+    """
+    Limits a search to specific parameters.
+    """
+
+    def _match(self, property):
+
+        # At least one parameter must match (or is-not-defined is set)
+        result = not self.defined
+        for parameterName in property.params().keys():
+            if parameterName == self.filter_name and self.match(property.params()[parameterName]):
+                result = self.defined
+                break
+
+        return result
+
+class IsNotDefined (FilterBase):
+    """
+    Specifies that the named iCalendar item does not exist.
+    """
+
+    def match(self, component, access=None):
+        # Oddly, this needs always to return True so that it appears there is
+        # a match - but we then "negate" the result if is-not-defined is set.
+        # Actually this method should never be called as we special case the
+        # is-not-defined option.
+        return True
+
+class TextMatch (FilterBase):
+    """
+    Specifies a substring match on a property or parameter value.
+    """
+    def __init__(self, xml_element):
+
+        super(TextMatch, self).__init__(xml_element)
+
+        self.text = str(xml_element)
+
+        if "collation" in xml_element.attributes:
+            self.collation = xml_element.attributes["collation"]
+        else:
+            self.collation = "i;unicode-casemap"
+
+        if "negate-condition" in xml_element.attributes:
+            self.negate = xml_element.attributes["negate-condition"]
+            if self.negate not in ("yes", "no"):
+                self.negate = "no"
+            self.negate = {"yes": True, "no": False}[self.negate]
+        else:
+            self.negate = False
+
+        if "match-type" in xml_element.attributes:
+            self.match_type = xml_element.attributes["match-type"]
+            if self.match_type not in (
+                "equals",
+                "contains",
+                "starts-with",
+                "ends-with",
+            ):
+                self.match_type = "contains"
+        else:
+            self.match_type = "contains"
+
+    def _match(self, item):
+        """
+        Match the text for the item.
+        If the item is a property, then match the property value,
+        otherwise it may be a list of parameter values - try to match anyone of those
+        """
+        if item is None: return False
+
+        if isinstance(item, Property):
+            values = [item.value()]
+        else:
+            values = item
+
+        test = unicode(self.text, "utf-8").lower()
+
+        def _textCompare(s):
+            # Currently ignores the collation and does caseless matching
+            s = s.lower()
+            
+            if self.match_type == "equals":
+                return s == test
+            elif self.match_type == "contains":
+                return s.find(test) != -1 
+            elif self.match_type == "starts-with":
+                return s.startswith(test)
+            elif self.match_type == "ends-with":
+                return s.endswith(test)
+            else:
+                return False
+
+        for value in values:
+            # NB Its possible that we have a text list value which appears as a Python list,
+            # so we need to check for that an iterate over the list.
+            if isinstance(value, list):
+                for subvalue in value:
+                    if _textCompare(unicode(subvalue)):
+                        return not self.negate
+            else:
+                if _textCompare(unicode(value)):
+                    return not self.negate
+        
+        return self.negate

Modified: CalendarServer/trunk/twistedcaldav/query/queryfilter.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/query/queryfilter.py	2010-04-16 14:11:11 UTC (rev 5485)
+++ CalendarServer/trunk/twistedcaldav/query/queryfilter.py	2010-04-16 19:52:34 UTC (rev 5486)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2009 Apple Inc. All rights reserved.
+# Copyright (c) 2009-2010 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -493,7 +493,7 @@
             negate = xml_element.attributes["negate-condition"]
             if negate == "yes":
                 self.negate = True
-            elif caseless == "no":
+            elif negate == "no":
                 self.negate = False
         else:
             self.negate = False

Modified: CalendarServer/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py	2010-04-16 14:11:11 UTC (rev 5485)
+++ CalendarServer/trunk/twistedcaldav/resource.py	2010-04-16 19:52:34 UTC (rev 5486)
@@ -674,9 +674,12 @@
         return self.isCalendarCollection()
 
     def findCalendarCollections(self, depth, request, callback, privileges=None):
-        """
-        See L{ICalDAVResource.findCalendarCollections}.
-        """
+        return self.findSpecialCollections(caldavxml.Calendar, depth, request, callback, privileges)
+
+    def findAddressBookCollections(self, depth, request, callback, privileges=None):
+        return self.findSpecialCollections(carddavxml.AddressBook, depth, request, callback, privileges)
+
+    def findSpecialCollections(self, type, depth, request, callback, privileges=None):
         assert depth in ("0", "1", "infinity"), "Invalid depth: %s" % (depth,)
 
         def checkPrivilegesError(failure):
@@ -693,11 +696,11 @@
             return ca
 
         def gotChild(child, childpath):
-            if child.isCalendarCollection():
+            if child.isSpecialCollection(type):
                 callback(child, childpath)
             elif child.isCollection():
                 if depth == "infinity":
-                    fc = child.findCalendarCollections(depth, request, callback, privileges)
+                    fc = child.findSpecialCollections(type, depth, request, callback, privileges)
                     fc.addCallback(lambda x: reactor.callLater(0, getChild))
                     return fc
 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20100416/f8253ae4/attachment-0001.html>


More information about the calendarserver-changes mailing list