[Zope-CVS] CVS: Packages/WebService - SOAPTypes.py:1.1 SOAPMessage.py:1.3 Serializer.py:1.2 todo.txt:1.4

Brian Lloyd brian@digicool.com
Mon, 17 Dec 2001 10:00:41 -0500


Update of /cvs-repository/Packages/WebService
In directory cvs.zope.org:/tmp/cvs-serv23471

Modified Files:
	SOAPMessage.py Serializer.py todo.txt 
Added Files:
	SOAPTypes.py 
Log Message:
Implemented MIME multipart encoding / decoding, refactored SOAPMessage 
interface to make it easier to customize.


=== Added File Packages/WebService/SOAPTypes.py ===
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.

from types import IntType


class SOAPStruct:
    """A SOAPStruct provides a pythonic interface to structured values."""

    def __init__(self, name=None, namespaceURI=None):
        dict = self.__dict__
        dict['_list'] = []
        dict['_dict'] = {}

    _namespaceURI = None
    _name = None
    
    def addMember(self, name, value):
        dict = self.__dict__
        dict['_dict'][name] = value
        dict['_list'].append(value)
        dict[name] = value

    def __getattr__(self, name):
        value = self._dict.get(name, self)
        if value is self:
            raise AttributeError, name
        return value

    def __setattr__(self, name, value):
        self.addMember(name, value)

    def __getitem__(self, key):
        if isinstance(key, IntType):
            return self._list[key]
        return self._dict[key]

    def get(self, name, default=None):
        return self._dict.get(name, default)

    def keys(self):
        return self._dict.keys()

    def values(self):
        return self._list

    def __len__(self):
        return len(self._list)



class SOAPArray:
    """ """
    pass



=== Packages/WebService/SOAPMessage.py 1.2 => 1.3 ===
 # FOR A PARTICULAR PURPOSE.
 
+import mimetools, multifile
 from Serializer import Serializer, SerializationContext
 from Transports import HTTPTransport
 from MimeWriter import MimeWriter
@@ -16,10 +17,6 @@
 from Utility import DOM
 
 
-
-# SOAPBlock?
-# most doc-style used for rpc
-# 
 class SOAPMessage:
     """A SOAPMessage provides a higher level interface for working with 
        SOAP messages that handles most of the details of serialization."""
@@ -41,6 +38,10 @@
         """Return the content type of the serialized message body."""
         return getattr(self, 'content_type', 'text/xml')
 
+    def setContentType(self, content_type):
+        """Set the content type of the message."""
+        self.content_type = content_type
+
     def getMimeParts(self):
         """Return the MIME message parts associated with the SOAP message."""
         return self.mimeparts
@@ -53,14 +54,6 @@
                 return item
         return None
 
-    def addMimePart(self, name, content_type, data):
-        """Add a MIME part to the message. Note that adding MIME parts
-           causes the message to be sent as a mime/multipart message."""
-        part = MIMEPart(name, content_type, data)
-        self.mimeparts.append(part)
-        param = Parameter(name, part, None)
-        self.parameters.append(param)
-
     def addSoapHeader(self, name, namespace, value, type, actor=None,
                       mustUnderstand=0,):
         """Add a SOAP header with the given values to the message."""
@@ -117,6 +110,8 @@
     def addParameter(self, name, value, type, namespace=None):
         """Add a parameter to the SOAP request message. Parameters are
            serialized and sent in the order they are added."""
+        if isinstance(value, MIMEPart):
+            self.mimeparts.append(value)
         param = Parameter(name, value, type, namespace)
         self.parameters.append(param)
 
@@ -140,31 +135,14 @@
             return None
         return self.parameters[0].value
 
-    def beforeSerialize(self):
-        """This method is called before serialization of a SOAP message.
-           Subclasses can implement this to customize message processing."""
-        pass
-
-    def afterSerialize(self):
-        """This method is called after serialization of a SOAP message.
-           Subclasses can implement this to customize message processing."""
-        pass
-
-    def beforeDeserialize(self):
-        """This method is called before deserialization of a SOAP message.
-           Subclasses can implement this to customize message processing."""
-        pass
-
-    def afterDeserialize(self):
-        """This method is called after deserialization of a SOAP message.
-           Subclasses can implement this to customize message processing."""
-        pass
-
     def serialize(self):
-        """Encode the message data into a valid SOAP XML message, using the
-           standard SOAP encoding for rpc style messages."""
-        self.beforeSerialize()
+        """Encode the message data into a valid SOAP XML or MIME message."""
+        self.soapEncode()
+        if self.mimeparts:
+            self.mimeEncode()
 
+    def soapEncode(self):
+        """Encode the SOAP XML message."""
         serializer = self.serializer
         context = SerializationContext(serializer)
         context.writer = SOAPWriter(self.version)
@@ -183,7 +161,7 @@
         if self.headers:
             writer.startHeader()
             for item in self.headers:
-                if self.mimeparts and isinstance(item.value, MIMEPart):
+                if isinstance(item.value, MIMEPart):
                     writer.startElement(item.name, item.namespace)
                     writer.writeAttr('href', 'cid:%s@message' % item.name)
                     writer.endElement()
@@ -255,73 +233,143 @@
 
         writer.endBody()
         writer.endEnvelope()
+        self.body = writer.toString()
 
-        output = writer.toString()
-
-        # If the message contains MIME parts, create a multipart message.
-        if self.mimeparts:
-            stream = StringIO()
-            writer = MimeWriter(stream)
-            writer.startmultipartbody('related',
-                plist=[('type', 'text/xml'),
-                       ('start', '<soap-envelope@message>')
-                       ]
-                )
-            soap = writer.nextpart()
-            soap.addheader('Content-Transfer-Encoding', '8bit')
-            soap.addheader('Content-ID', '<soap-envelope@message>')
-            body = soap.startbody('text/xml')
-            body.write(output)
-
-            for mimepart in self.mimeparts:
-                part = writer.nextpart()
-                part.addheader('Content-ID', '<%s@message>' % mimepart.name)
-                if mimepart.content_type[:4] == 'text':
-                    part.addheader('Content-Transfer-Encoding', '8bit')
-                    body = part.startbody(mimepart.content_type)
-                    body.write(mimepart.data)
-                else:
-                    part.addheader('Content-Transfer-Encoding', 'base64')
-                    body = part.startbody(mimepart.content_type)
-                    body.write(base64.encodestring(mimepart.data))
-            writer.lastpart()
-            output = stream.getvalue()
-            output = output.split('--', 1)[-1]
 
+    def mimeEncode(self):
+        """Encode the message (and attachments) as MIME multipart."""
+        stream = StringIO()
+        writer = MimeWriter(stream)
+        writer.startmultipartbody('related',
+            plist=[('type', 'text/xml'),
+                   ('start', '<soap-envelope@message>')
+                   ]
+            )
+        soap = writer.nextpart()
+        soap.addheader('Content-Transfer-Encoding', '8bit')
+        soap.addheader('Content-ID', '<soap-envelope@message>')
+        body = soap.startbody('text/xml')
+        body.write(self.body)
+
+        for mimepart in self.mimeparts:
+            part = writer.nextpart()
+            part.addheader('Content-ID', '<%s@message>' % mimepart.name)
+            if mimepart.content_type[:4] == 'text':
+                part.addheader('Content-Transfer-Encoding', '8bit')
+                body = part.startbody(mimepart.content_type)
+                body.write(mimepart.data)
+            else:
+                part.addheader('Content-Transfer-Encoding', 'base64')
+                body = part.startbody(mimepart.content_type)
+                body.write(base64.encodestring(mimepart.data))
+
+        writer.lastpart()
+        output = stream.getvalue()
+        output = output.split('--', 1)[-1]
         self.body = output
-        self.afterSerialize()
 
 
     def deserialize(self):
-        """Decode the response SOAP message."""
-        serializer = self.serializer
-        context = SerializationContext(serializer)
+        """Deserialize an XML or MIME multipart SOAP message. This method
+           may be overridden in a subclass to customize message processing."""
+        if self.getContentType().lower().startswith('multipart'):
+            self.mimeDecode()
+        self.soapDecode()
+
+    def mimeDecode(self):
+        """Decode a MIME multipart/related message, storing MIME data."""
+
+        # Note that the Content-Type must have been set so that we can
+        # determine the boundary information for a multipart message.
+        boundary = None
+        soapbody = None
+        for param in self.getContentType().split(';'):
+            param = param.strip()
+            if param.startswith('boundary='):
+                boundary = param[9:]
+                if boundary[0] in ('"', "'"):
+                    boundary = boundary[1:-1]
+                break
+        if boundary is None:
+            raise ValueError('Missing MIME boundary')
+
+        # We assume that the first text/* part of the multipart message is
+        # the SOAP XML message. Other MIME parts are stored as MIMEParts
+        # with names derived from their Content-ID or Content-Location.
+        mfile = multifile.MultiFile(StringIO(self.body))
+        mfile.push(boundary)
+        while mfile.next():
+            mimepart = mimetools.Message(mfile)
+            content_type = mimepart.gettype()
+
+            output = StringIO()
+            mimetools.decode(mfile, output, mimepart.getencoding())
+            output.seek(0)
+
+            if content_type.startswith('text') and soapbody is None:
+                self.body = output.getvalue()
+                soapbody = 1
+            else:
+                content_loc = mimepart.getheader('Content-Location', None)
+                content_id = mimepart.getheader('Content-ID', None)
+                partname = content_id or content_loc
+                if not partname:
+                    raise ValueError('Missing Content-ID or Content-Location')
+                if partname.startswith('<'):
+                    partname = partname[1:-1]
+                if not content_id:
+                    partname = partname.split('/')[-1]
+                mpart = MIMEPart(partname, content_type, output, mimepart)
+                self.mimeparts.append(mpart)
 
-        if self.body is None:
-            raise InvalidMessage(
-                'Cannot deserialize without a message body.'
-                )
+    def soapDecode(self):
+        """Decode the SOAP XML message body, storing parameter data."""
 
+        # Setup the serialization context we'll use to decode this message.
+        serializer = self.serializer
+        context = SerializationContext(serializer)
         context.reader = SOAPReader(self.body)
         reader = self.reader = context.reader
         self.version = reader.version
 
-        # xxx - handle mime response here.
-
+        # Register any MIME attachments with the serialization context so
+        # that references to them can be resolved during deserialization.
+        for item in self.mimeparts:
+            context.external[item.name] = item
 
         envelope = reader.getEnvelope()
 
-        self.checkEncodingStyle(envelope)
+        # Store any SOAP headers that appear in the message. Because schema
+        # rarely address headers, we can't be sure that we'll know how to
+        # deserialize them all. If we can't deserialize a header, we fall
+        # back to storing the DOM element rather than failing, on the theory
+        # that higher-level code will know how to deal with them.
+        soap_header = reader.getHeader()
+        if soap_header is not None:
+            encodingStyle = reader.getEncodingStyle(soap_header)
+            self.checkEncodingStyle(encodingStyle)
+            for element in reader.getHeaderElements():
+                name = element.localName
+                namespace = element.namespaceURI or None
+                type = DOM.getTypeRef(element)
+                try:    value = serializer.deserialize(element, context)
+                except: value = element
+                header = SOAPHeader(name, value, type, namespace)
+                header.actor = DOM.getAttr(element, 'actor', default=None)
+                header.mustUnderstand = (
+                    DOM.getAttr(element, 'mustUnderstand') == '1'
+                    )
+                self.headers.append(header)
 
-        # Check for a fault in the response message first.
+        # Save fault information if a fault appears in the message. Note
+        # that we just store the fault detail as a literal string, as we
+        # can't really know about custom fault encodings at this level.
         fault = reader.getFault()
         if fault is not None:
             faultcode = reader.getFaultCode()
             faultcodeNS = reader.getFaultCodeNS()
             faultstring = reader.getFaultString()
             faultactor = reader.getFaultActor()
-
-            # For now, save fault detail as an xml string.
             detail = reader.getFaultDetail()
             if detail is not None:
                 detail = reader.derefElement(detail)
@@ -331,28 +379,11 @@
                 )
             return
 
-        # Hmm - think about this!
-        headers = reader.getHeader()
-        if headers is not None:
-            self.checkEncodingStyle(headers)
-
-            for element in reader.getHeaderElements():
-                self.checkEncodingStyle(element)
-                name = element.localName
-                namespace = element.namespaceURI or None
-                type = DOM.getTypeRef(element)
-                try: value = serializer.deserialize(element, context)
-                except: value = element
-                header = SOAPHeader(name, value, type, namespace)
-                header.actor = DOM.getAttr(element, 'actor', default=None)
-                header.mustUnderstand = (
-                    DOM.getAttr(element, 'mustUnderstand') == '1'
-                    )
-                self.headers.append(header)
-
         body = reader.getBody()
+        encodingStyle = reader.getEncodingStyle(body)
+        self.checkEncodingStyle(encodingStyle)
 
-        self.checkEncodingStyle(body)
+        body_elements = reader.getBodyElements()
 
         if self.style == 'rpc':
             struct = reader.getRPCStruct()
@@ -362,7 +393,6 @@
             # Standard SOAP rpc style message. We use the serializer to
             # convert the param values using the standard SOAP encoding.
             for element in reader.getRPCParams():
-                self.checkEncodingStyle(element)
                 name = element.localName
                 namespace = element.namespaceURI or None
                 type = DOM.getTypeRef(element)
@@ -386,10 +416,10 @@
 
         return
 
-    def checkEncodingStyle(self, element):
-        encoding = DOM.getAttr(element, 'encodingStyle', None, default=None)
-        if encoding is not None and encoding.find(
-            DOM.GetSOAPEncUri(self.version)) < 0:
+
+    def checkEncodingStyle(self, encodingStyle):
+        soapEncoding = DOM.GetSOAPEncUri(self.version)
+        if encodingStyle and encodingStyle.find(soapEncoding) < 0:
             raise ValueError, 'Unknown encoding style: %s' % encoding
 
 
@@ -426,12 +456,12 @@
 
 class MIMEPart:
     """A MIMEPart object represents a MIME part of a SOAP message."""
-    def __init__(self, name, content_type, data, headers={}):
+    def __init__(self, name, content_type, data, headers=None):
         self.name = name
         self.content_type = content_type
-        self.headers = headers
-        if hasattr(data, 'read'):
-            data = data.read()
+        self.headers = headers or {}
+        if not hasattr(data, 'read'):
+            data = StringIO(data)
         self.data = data
 
 


=== Packages/WebService/Serializer.py 1.1 => 1.2 ===
     def __init__(self, serializer):
         self.serializer = serializer
+        self.external = {}
         self.write_ns = 0
-        self._refmap = {}
 
     NS_XSI = DOM.NS_XSI
+    _refmap = {}
     reader = None
     writer = None
     strict = 1
+    mime = {}
+
+    def derefExternal(self, element):
+        """Dereference an element, only if an ref points to external data."""
+        reference = DOM.getAttr(element, 'href', None, None)
+        if reference and not reference.startswith('#'):
+            if reference.startswith('cid:'):
+                value = self.external.get(reference[4:])
+                if value is not None:
+                    return value
+            local = reference.split('/')[-1]
+            value = self.external.get(local)
+            if value is not None:
+                return value
+            raise ValueError('Unresolvable external reference: %s' % reference)
+
+    def derefElement(self, element):
+        """Dereference an element, only if an ref points to another element."""
+        ref = DOM.getAttr(element, 'href', None, None)
+        if ref and ref.startswith('#'):
+            return self.reader.getElementByRef(reference)
+        return element
+
+    def resolve(self, reference):
+        """Resolve an href reference, possibly to external data."""
+        if reference.startswith('#'):
+            return self.reader.getElementByRef(reference)
+        elif reference.startswith('cid:'):
+            return self.mime.get(reference[4:])
+        return None
 
     def isMultiRef(self, value):
         """Return true if a value should be encoded as multi-reference."""
@@ -94,6 +125,11 @@
            that contains the SOAPReader instance for the message."""
         # check for array first?
 
+        # First, look for a reference to external data (e.g. MIME data).
+        derefed = context.derefExternal(element)
+        if derefed is not None:
+            return derefed
+
         # Look for an explicit type declaration on the element.
         typeref = DOM.getTypeRef(element)
         if typeref is not None:
@@ -168,10 +204,24 @@
         if element_is_null(element, context):
             return None
 
-        # Follow href reference if one appears on the element.
-        derefed = context.reader.derefElement(element)
+        object = context.derefExternal(element)
+        if object is not None:
+            return object
+
+        # If the element has an href reference, try to resolve it using 
+        # the serialization context.
+
+        ref = DOM.getAttr(element, 'href', None, None)
+        if ref is not None:
+            object = context.resolve(ref)
+            if hasattr(object, 'nodeType'):
+                element = object
+            elif object is None:
+                raise ValueError('Unresolvable reference: %s' % ref)
+            else:
+                return object
 
-        strval = DOM.getElementText(derefed)
+        strval = DOM.getElementText(element)
         value = self.load(strval, context)
         if context.strict:
             self.checkValue(value, context)
@@ -343,8 +393,15 @@
         if element_is_null(element, context):
             return None
 
-        # Follow href reference if one appears on the element.
-        element = context.reader.derefElement(element)
+        ref = DOM.getAttr(element, 'href', None, None)
+        if ref is not None:
+            object = context.resolve(ref)
+            if hasattr(object, 'nodeType'):
+                element = object
+            elif object is None:
+                raise ValueError('Unresolvable reference: %s' % ref)
+            else:
+                return object
 
         # Get array type information, offset info.
         atype = self.get_arraytype(element, context)
@@ -822,7 +879,7 @@
 def element_is_null(element, context):
     if DOM.getAttr(element, 'nil', default=None) is not None:
         return 1
-    derefed = context.reader.derefElement(element)
+    derefed = context.derefElement(element)
     if derefed is not element:
         if DOM.getAttr(derefed, 'nil', default=None) is not None:
             return 1


=== Packages/WebService/todo.txt 1.3 => 1.4 ===
 
-  - finish SOAPMessage docs
+  - finish SOAPMessage docs (document mime parts & params)
 
   - rethink Message params apis