[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