[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