[Zope3-checkins]
SVN: Zope3/branches/isarsprint-dav-work/src/zope/app/dav/
Complete PROPPATCH implementation for opaque properties. Note
that registered
Martijn Pieters
mj at zopatista.com
Wed Oct 13 05:30:34 EDT 2004
Log message for revision 28053:
Complete PROPPATCH implementation for opaque properties. Note that registered
DAV namespaces are not yet supported!
Changed:
U Zope3/branches/isarsprint-dav-work/src/zope/app/dav/opaquenamespaces.py
U Zope3/branches/isarsprint-dav-work/src/zope/app/dav/proppatch.py
U Zope3/branches/isarsprint-dav-work/src/zope/app/dav/tests/test_propfind.py
U Zope3/branches/isarsprint-dav-work/src/zope/app/dav/tests/test_proppatch.py
U Zope3/branches/isarsprint-dav-work/src/zope/app/dav/tests/unitfixtures.py
-=-
Modified: Zope3/branches/isarsprint-dav-work/src/zope/app/dav/opaquenamespaces.py
===================================================================
--- Zope3/branches/isarsprint-dav-work/src/zope/app/dav/opaquenamespaces.py 2004-10-13 09:16:44 UTC (rev 28052)
+++ Zope3/branches/isarsprint-dav-work/src/zope/app/dav/opaquenamespaces.py 2004-10-13 09:30:32 UTC (rev 28053)
@@ -94,3 +94,122 @@
el = propel.ownerDocument.importNode(value.documentElement, True)
el.setAttribute('xmlns', nsprefix)
propel.appendChild(el)
+
+ def setProperty(self, propel):
+ ns = propel.namespaceURI
+ props = self.setdefault(ns, OOBTree())
+ propel = makeDOMStandalone(propel)
+ props[propel.nodeName] = propel.toxml('utf-8')
+
+
+def makeDOMStandalone(element):
+ """Make a DOM Element Node standalone
+
+ The DOM tree starting at element is copied to a new DOM tree where:
+
+ - Any prefix used for the element namespace is removed from the element
+ and all attributes and decendant nodes.
+ - Any other namespaces used on in the DOM tree is explcitly declared on
+ the root element.
+
+ So, if the root element to be transformed is defined with a prefix, that
+ prefix is removed from the whole tree:
+
+ >>> dom = minidom.parseString('''<?xml version="1.0"?>
+ ... <foo xmlns:bar="http://bar.com">
+ ... <bar:spam><bar:eggs /></bar:spam>
+ ... </foo>''')
+ >>> element = dom.documentElement.getElementsByTagName('bar:spam')[0]
+ >>> standalone = makeDOMStandalone(element)
+ >>> standalone.toxml()
+ u'<spam><eggs/></spam>'
+
+ Prefixes are of course also removed from attributes:
+
+ >>> element.setAttributeNS(element.namespaceURI, 'bar:vikings',
+ ... 'singing')
+ >>> standalone = makeDOMStandalone(element)
+ >>> standalone.toxml()
+ u'<spam vikings="singing"><eggs/></spam>'
+
+ Any other namespace used will be preserved, with the prefix definitions
+ for these renamed and moved to the root element:
+
+ >>> dom = minidom.parseString('''<?xml version="1.0"?>
+ ... <foo xmlns:bar="http://bar.com" xmlns:mp="uri://montypython">
+ ... <bar:spam>
+ ... <bar:eggs mp:song="vikings" />
+ ... <mp:holygrail xmlns:c="uri://castle">
+ ... <c:camelot place="silly" />
+ ... </mp:holygrail>
+ ... <lancelot xmlns="uri://montypython" />
+ ... </bar:spam>
+ ... </foo>''')
+ >>> element = dom.documentElement.getElementsByTagName('bar:spam')[0]
+ >>> standalone = makeDOMStandalone(element)
+ >>> print standalone.toxml()
+ <spam xmlns:p0="uri://montypython" xmlns:p1="uri://castle">
+ <eggs p0:song="vikings"/>
+ <p0:holygrail>
+ <p1:camelot place="silly"/>
+ </p0:holygrail>
+ <p0:lancelot/>
+ </spam>
+ """
+
+ return DOMTransformer(element).makeStandalone()
+
+
+def _numberGenerator(i = 0):
+ while True:
+ yield i
+ i += 1
+
+
+class DOMTransformer(object):
+ def __init__(self, el):
+ self.source = el
+ self.ns = el.namespaceURI
+ self.prefix = el.prefix
+ self.doc = minidom.getDOMImplementation().createDocument(
+ self.ns, el.localName, None)
+ self.dest = self.doc.documentElement
+ self.prefixes = {}
+ self._seq = _numberGenerator()
+
+ def seq(self): return self._seq.next()
+ seq = property(seq)
+
+ def _prefixForURI(self, uri):
+ if not uri or uri == self.ns:
+ return ''
+ if not self.prefixes.has_key(uri):
+ self.prefixes[uri] = 'p%d' % self.seq
+ return self.prefixes[uri] + ':'
+
+ def makeStandalone(self):
+ self._copyElement(self.source, self.dest)
+ for ns, prefix in self.prefixes.items():
+ self.dest.setAttribute('xmlns:%s' % prefix, ns)
+ return self.dest
+
+ def _copyElement(self, source, dest):
+ for i in range(source.attributes.length):
+ attr = source.attributes.item(i)
+ if attr.prefix == 'xmlns' or attr.nodeName == 'xmlns':
+ continue
+ ns = attr.prefix and attr.namespaceURI or source.namespaceURI
+ qname = attr.localName
+ if ns != dest.namespaceURI:
+ qname = '%s%s' % (self._prefixForURI(ns), qname)
+ dest.setAttributeNS(ns, qname, attr.value)
+
+ for node in source.childNodes:
+ if node.nodeType == node.ELEMENT_NODE:
+ ns = node.namespaceURI
+ qname = '%s%s' % (self._prefixForURI(ns), node.localName)
+ copy = self.doc.createElementNS(ns, qname)
+ self._copyElement(node, copy)
+ else:
+ copy = self.doc.importNode(node, True)
+ dest.appendChild(copy)
Modified: Zope3/branches/isarsprint-dav-work/src/zope/app/dav/proppatch.py
===================================================================
--- Zope3/branches/isarsprint-dav-work/src/zope/app/dav/proppatch.py 2004-10-13 09:16:44 UTC (rev 28052)
+++ Zope3/branches/isarsprint-dav-work/src/zope/app/dav/proppatch.py 2004-10-13 09:30:32 UTC (rev 28053)
@@ -18,8 +18,13 @@
from xml.dom import minidom
from zope.app import zapi
+from zope.schema import getFieldNamesInOrder
from zope.app.container.interfaces import IReadContainer
+from zope.publisher.http import status_reasons
+from interfaces import IDAVNamespace
+from opaquenamespaces import IDAVOpaqueNamespaces
+
class PROPPATCH(object):
"""PROPPATCH handler for all objects"""
@@ -35,7 +40,18 @@
self.content_type = ct.lower()
self.content_type_params = None
self.default_ns = 'DAV:'
+ self.oprops = IDAVOpaqueNamespaces(self.context, None)
+ _avail_props = {}
+ # List all *registered* DAV interface namespaces and their properties
+ for ns, iface in zapi.getUtilitiesFor(IDAVNamespace):
+ _avail_props[ns] = getFieldNamesInOrder(iface)
+ # List all opaque DAV namespaces and the properties we know of
+ if self.oprops:
+ for ns, oprops in self.oprops.items():
+ _avail_props[ns] = list(oprops.keys())
+ self.avail_props = _avail_props
+
def PROPPATCH(self):
if self.content_type not in ['text/xml', 'application/xml']:
self.request.response.setStatus(400)
@@ -56,7 +72,107 @@
ms.lastChild.appendChild(resp.createElement('href'))
ms.lastChild.lastChild.appendChild(resp.createTextNode(resource_url))
+ updateel = xmldoc.getElementsByTagNameNS(self.default_ns,
+ 'propertyupdate')
+ if not updateel:
+ self.request.response.setStatus(422)
+ return ''
+ updates = [node for node in updateel[0].childNodes
+ if node.nodeType == node.ELEMENT_NODE and
+ node.localName in ('set', 'remove')]
+ if not updates:
+ self.request.response.setStatus(422)
+ return ''
+ self._handlePropertyUpdate(resp, updates)
+
body = resp.toxml().encode('utf-8')
self.request.response.setBody(body)
self.request.response.setStatus(207)
return body
+
+ def _handlePropertyUpdate(self, resp, updates):
+ _propresults = {}
+ for update in updates:
+ prop = update.getElementsByTagNameNS(self.default_ns, 'prop')
+ if not prop:
+ continue
+ for node in prop[0].childNodes:
+ if not node.nodeType == node.ELEMENT_NODE:
+ continue
+ if update.localName == 'set':
+ status = self._handleSet(node)
+ else:
+ status = self._handleRemove(node)
+ results = _propresults.setdefault(status, {})
+ props = results.setdefault(node.namespaceURI, [])
+ if node.localName not in props:
+ props.append(node.localName)
+
+ if _propresults.keys() != [200]:
+ # At least some props failed, abort transaction
+ get_transaction().abort()
+ # Move 200 succeeded props to the 424 status
+ if _propresults.has_key(200):
+ failed = _propresults.setdefault(424, {})
+ for ns, props in _propresults[200].items():
+ failed_props = failed.setdefault(ns, [])
+ failed_props.extend(props)
+ del _propresults[200]
+
+ # Create the response document
+ re = resp.lastChild.lastChild
+ for status, results in _propresults.items():
+ re.appendChild(resp.createElement('propstat'))
+ prop = resp.createElement('prop')
+ re.lastChild.appendChild(prop)
+ count = 0
+ for ns in results.keys():
+ attr_name = 'a%s' % count
+ if ns is not None and ns != self.default_ns:
+ count += 1
+ prop.setAttribute('xmlns:%s' % attr_name, ns)
+ for p in results.get(ns, []):
+ el = resp.createElement(p)
+ prop.appendChild(el)
+ if ns is not None and ns != self.default_ns:
+ el.setAttribute('xmlns', attr_name)
+ reason = status_reasons.get(status, '')
+ re.lastChild.appendChild(resp.createElement('status'))
+ re.lastChild.lastChild.appendChild(
+ resp.createTextNode('HTTP/1.1 %d %s' % (status, reason)))
+
+ def _handleSet(self, prop):
+ ns = prop.namespaceURI
+ iface = zapi.queryUtility(IDAVNamespace, ns)
+ if not iface:
+ # opaque DAV properties
+ if self.oprops is not None:
+ self.oprops.setProperty(prop)
+ # Register the new available property, because we need to be
+ # able to remove it again in the same request!
+ props = self.avail_props.setdefault(ns, [])
+ if prop.localName not in props:
+ props.append(prop.localName)
+ return 200
+ return 403
+
+ # XXX: Deal with registered ns interfaces here
+ return 403
+
+ def _handleRemove(self, prop):
+ ns = prop.namespaceURI
+ if not prop.localName in self.avail_props.get(ns, []):
+ return 200
+ iface = zapi.queryUtility(IDAVNamespace, ns)
+ if not iface:
+ # opaque DAV properties
+ if self.oprops is None:
+ return 200
+ if self.oprops.get(ns, {}).get(prop.localName):
+ del self.oprops[ns][prop.localName]
+ if not list(self.oprops[ns].keys()):
+ del self.oprops[ns]
+ return 200
+
+ # XXX: Deal with registered ns interfaces here
+ return 403
Modified: Zope3/branches/isarsprint-dav-work/src/zope/app/dav/tests/test_propfind.py
===================================================================
--- Zope3/branches/isarsprint-dav-work/src/zope/app/dav/tests/test_propfind.py 2004-10-13 09:16:44 UTC (rev 28052)
+++ Zope3/branches/isarsprint-dav-work/src/zope/app/dav/tests/test_propfind.py 2004-10-13 09:30:32 UTC (rev 28053)
@@ -22,6 +22,8 @@
from zope.interface import Interface, implements, directlyProvides
from zope.publisher.interfaces.http import IHTTPRequest
+from zope.publisher.http import status_reasons
+
from zope.pagetemplate.tests.util import normalize_xml
from zope.schema import getFieldNamesInOrder
from zope.schema.interfaces import IText, ITextLine, IDatetime, ISequence
Modified: Zope3/branches/isarsprint-dav-work/src/zope/app/dav/tests/test_proppatch.py
===================================================================
--- Zope3/branches/isarsprint-dav-work/src/zope/app/dav/tests/test_proppatch.py 2004-10-13 09:16:44 UTC (rev 28052)
+++ Zope3/branches/isarsprint-dav-work/src/zope/app/dav/tests/test_proppatch.py 2004-10-13 09:30:32 UTC (rev 28053)
@@ -22,6 +22,9 @@
from zope.interface import Interface, implements, directlyProvides
from zope.publisher.interfaces.http import IHTTPRequest
+from zope.publisher.http import status_reasons
+from zope.pagetemplate.tests.util import normalize_xml
+from ZODB.tests.util import DB
from zope.app import zapi
from zope.app.tests import ztapi
@@ -41,27 +44,32 @@
from zope.app.dav import proppatch
from zope.app.dav.interfaces import IDAVSchema
from zope.app.dav.interfaces import IDAVNamespace
+from zope.app.dav.opaquenamespaces import DAVOpaqueNamespacesAdapter
+from zope.app.dav.opaquenamespaces import IDAVOpaqueNamespaces
def _createRequest(body=None, headers=None, skip_headers=None,
namespaces=(('Z', 'http://www.w3.com/standards/z39.50/'),),
set=('<Z:authors>\n<Z:Author>Jim Whitehead</Z:Author>\n',
'<Z:Author>Roy Fielding</Z:Author>\n</Z:authors>'),
- remove=('<D:prop><Z:Copyright-Owner/></D:prop>\n')):
+ remove=('<Z:Copyright-Owner/>'), extra=''):
+ if headers is None:
+ headers = {'Content-type':'text/xml'}
if body is None:
- setProps = removeProps = ''
+ nsAttrs = setProps = removeProps = ''
if set:
setProps = '<set><prop>\n%s\n</prop></set>\n' % (''.join(set))
if remove:
removeProps = '<remove><prop>\n%s\n</prop></remove>\n' % (
''.join(remove))
+ for prefix, ns in namespaces:
+ nsAttrs += ' xmlns:%s="%s"' % (prefix, ns)
body = '''<?xml version="1.0" ?>
- <propertyupdate xmlns="DAV:"
- xmlns:Z="http://www.w3.com/standards/z39.50/">
+ <propertyupdate xmlns="DAV:"%s>
%s
</propertyupdate>
- ''' % (setProps + removeProps)
+ ''' % (nsAttrs, setProps + removeProps + extra)
_environ = {'CONTENT_TYPE': 'text/xml',
'CONTENT_LENGTH': str(len(body))}
@@ -100,12 +108,22 @@
ztapi.provideAdapter(IAnnotatable, IAnnotations, AttributeAnnotations)
ztapi.provideAdapter(IAnnotatable, IZopeDublinCore,
ZDCAnnotatableAdapter)
+ ztapi.provideAdapter(IAnnotatable, IDAVOpaqueNamespaces,
+ DAVOpaqueNamespacesAdapter)
utils = zapi.getGlobalService('Utilities')
directlyProvides(IDAVSchema, IDAVNamespace)
utils.provideUtility(IDAVNamespace, IDAVSchema, 'DAV:')
directlyProvides(IZopeDublinCore, IDAVNamespace)
utils.provideUtility(IDAVNamespace, IZopeDublinCore,
'http://www.purl.org/dc/1.1')
+ self.db = DB()
+ self.conn = self.db.open()
+ root = self.conn.root()
+ root['Application'] = self.rootFolder
+ get_transaction().commit()
+
+ def tearDown(self):
+ self.db.close()
def test_contenttype1(self):
file = self.file
@@ -154,7 +172,143 @@
# Check HTTP Response
self.assertEqual(request.response.getStatus(), 207)
self.assertEqual(ppatch.content_type, 'text/xml')
+
+ def test_noupdates(self):
+ file = self.file
+ request = _createRequest(namespaces=(), set=(), remove=())
+ ppatch = proppatch.PROPPATCH(file, request)
+ ppatch.PROPPATCH()
+ # Check HTTP Response
+ self.assertEqual(request.response.getStatus(), 422)
+
+ def _checkProppatch(self, obj, ns=(), set=(), rm=(), extra='', expect=''):
+ request = _createRequest(namespaces=ns, set=set, remove=rm,
+ extra=extra)
+ resource_url = str(zapi.getView(obj, 'absolute_url', request))
+ expect = '''<?xml version="1.0" ?>
+ <multistatus xmlns="DAV:"><response>
+ <href>%%(resource_url)s</href>
+ %s
+ </response></multistatus>
+ ''' % expect
+ expect = expect % {'resource_url': resource_url}
+ ppatch = proppatch.PROPPATCH(obj, request)
+ ppatch.PROPPATCH()
+ # Check HTTP Response
+ self.assertEqual(request.response.getStatus(), 207)
+ s1 = normalize_xml(request.response._body)
+ s2 = normalize_xml(expect)
+ self.assertEqual(s1, s2)
+
+ def _makePropstat(self, ns, properties, status=200):
+ nsattrs = ''
+ count = 0
+ for uri in ns:
+ nsattrs += ' xmlns:a%d="%s"' % (count, uri)
+ count += 1
+ reason = status_reasons[status]
+ return '''<propstat>
+ <prop%s>%s</prop>
+ <status>HTTP/1.1 %d %s</status>
+ </propstat>''' % (nsattrs, properties, status, reason)
+ def _assertOPropsEqual(self, obj, expect):
+ oprops = IDAVOpaqueNamespaces(obj)
+ namespacesA = list(oprops.keys())
+ namespacesA.sort()
+ namespacesB = expect.keys()
+ namespacesB.sort()
+ self.assertEqual(namespacesA, namespacesB,
+ 'available opaque namespaces were %s, '
+ 'expected %s' % (namespacesA, namespacesB))
+
+ for ns in namespacesA:
+ propnamesA = list(oprops[ns].keys())
+ propnamesA.sort()
+ propnamesB = expect[ns].keys()
+ propnamesB.sort()
+ self.assertEqual(propnamesA, propnamesB,
+ 'props for opaque namespaces %s were %s, '
+ 'expected %s' % (ns, propnamesA, propnamesB))
+ for prop in propnamesA:
+ valueA = oprops[ns][prop]
+ valueB = expect[ns][prop]
+ self.assertEqual(valueA, valueB,
+ 'opaque prop %s:%s was %s, '
+ 'expected %s' % (ns, prop, valueA, valueB))
+
+ def test_removenonexisting(self):
+ expect = self._makePropstat(('uri://foo',), '<bar xmlns="a0"/>')
+ self._checkProppatch(self.zpt, ns=(('foo', 'uri://foo'),),
+ rm=('<foo:bar />'), expect=expect)
+
+ def test_opaque_set_simple(self):
+ expect = self._makePropstat(('uri://foo',), '<bar xmlns="a0"/>')
+ self._checkProppatch(self.zpt, ns=(('foo', 'uri://foo'),),
+ set=('<foo:bar>spam</foo:bar>'), expect=expect)
+ self._assertOPropsEqual(self.zpt,
+ {u'uri://foo': {u'bar': '<bar>spam</bar>'}})
+
+ def test_opaque_remove_simple(self):
+ oprops = IDAVOpaqueNamespaces(self.zpt)
+ oprops['uri://foo'] = {'bar': '<bar>eggs</bar>'}
+ expect = self._makePropstat(('uri://foo',), '<bar xmlns="a0"/>')
+ self._checkProppatch(self.zpt, ns=(('foo', 'uri://foo'),),
+ rm=('<foo:bar>spam</foo:bar>'), expect=expect)
+ self._assertOPropsEqual(self.zpt, {})
+
+ def test_opaque_add_and_replace(self):
+ oprops = IDAVOpaqueNamespaces(self.zpt)
+ oprops['uri://foo'] = {'bar': '<bar>eggs</bar>'}
+ expect = self._makePropstat(
+ ('uri://castle', 'uri://foo'),
+ '<camelot xmlns="a0"/><bar xmlns="a1"/>')
+ self._checkProppatch(self.zpt,
+ ns=(('foo', 'uri://foo'), ('c', 'uri://castle')),
+ set=('<foo:bar>spam</foo:bar>',
+ '<c:camelot place="silly" xmlns:k="uri://knights">'
+ ' <k:roundtable/>'
+ '</c:camelot>'),
+ expect=expect)
+ self._assertOPropsEqual(self.zpt, {
+ u'uri://foo': {u'bar': '<bar>spam</bar>'},
+ u'uri://castle': {u'camelot':
+ '<camelot place="silly" xmlns:p0="uri://knights">'
+ ' <p0:roundtable/></camelot>'}})
+
+ def test_opaque_set_and_remove(self):
+ expect = self._makePropstat(
+ ('uri://foo',), '<bar xmlns="a0"/>')
+ self._checkProppatch(self.zpt, ns=(('foo', 'uri://foo'),),
+ set=('<foo:bar>eggs</foo:bar>',), rm=('<foo:bar/>',),
+ expect=expect)
+ self._assertOPropsEqual(self.zpt, {})
+
+ def test_opaque_complex(self):
+ # PROPPATCH allows us to set, remove and set the same property, ordered
+ expect = self._makePropstat(
+ ('uri://foo',), '<bar xmlns="a0"/>')
+ self._checkProppatch(self.zpt, ns=(('foo', 'uri://foo'),),
+ set=('<foo:bar>spam</foo:bar>',), rm=('<foo:bar/>',),
+ extra='<set><prop><foo:bar>spam</foo:bar></prop></set>',
+ expect=expect)
+ self._assertOPropsEqual(self.zpt,
+ {u'uri://foo': {u'bar': '<bar>spam</bar>'}})
+
+ def test_proppatch_failure(self):
+ # XXX: This relies on the fact that only opaque properties can be set
+ # for now. As soon as registered interfaces support is implemented,
+ # this test will need to be rewritten.
+ expect = self._makePropstat(
+ ('uri://foo',), '<bar xmlns="a0"/>', 424)
+ expect += self._makePropstat(
+ ('http://www.purl.org/dc/1.1',), '<title xmlns="a0"/>', 403)
+ self._checkProppatch(self.zpt,
+ ns=(('foo', 'uri://foo'), ('DC', 'http://www.purl.org/dc/1.1')),
+ set=('<foo:bar>spam</foo:bar>', '<DC:title>Test</DC:title>'),
+ expect=expect)
+ self._assertOPropsEqual(self.zpt, {})
+
def test_suite():
return unittest.TestSuite((
Modified: Zope3/branches/isarsprint-dav-work/src/zope/app/dav/tests/unitfixtures.py
===================================================================
--- Zope3/branches/isarsprint-dav-work/src/zope/app/dav/tests/unitfixtures.py 2004-10-13 09:16:44 UTC (rev 28052)
+++ Zope3/branches/isarsprint-dav-work/src/zope/app/dav/tests/unitfixtures.py 2004-10-13 09:30:32 UTC (rev 28053)
@@ -17,6 +17,7 @@
"""
__docformat__ = 'restructuredtext'
+from persistent import Persistent
from zope.interface import implements
from zope.app.filerepresentation.interfaces import IWriteFile
@@ -25,7 +26,7 @@
import zope.app.location
-class Folder(zope.app.location.Location):
+class Folder(zope.app.location.Location, Persistent):
implements(IReadContainer)
@@ -45,7 +46,7 @@
Folder('sub1', level=self.level+1, parent=self)))
return tuple(result)
-class File(zope.app.location.Location):
+class File(zope.app.location.Location, Persistent):
implements(IWriteFile)
@@ -58,7 +59,7 @@
def write(self, data):
self.data = data
-class FooZPT(zope.app.location.Location):
+class FooZPT(zope.app.location.Location, Persistent):
implements(IAnnotatable)
More information about the Zope3-Checkins
mailing list