[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