1 #!/usr/bin/env python 2 3 """ 4 SOAP support using libxml2dom. Support for the archaic SOAP namespaces is also 5 provided. 6 7 See: http://www.w3.org/TR/2007/REC-soap12-part0-20070427/ 8 9 Copyright (C) 2007 Paul Boddie <paul@boddie.org.uk> 10 11 This program is free software; you can redistribute it and/or modify it under 12 the terms of the GNU Lesser General Public License as published by the Free 13 Software Foundation; either version 3 of the License, or (at your option) any 14 later version. 15 16 This program is distributed in the hope that it will be useful, but WITHOUT 17 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 18 FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 19 details. 20 21 You should have received a copy of the GNU Lesser General Public License along 22 with this program. If not, see <http://www.gnu.org/licenses/>. 23 24 -------- 25 26 The sending and receiving of SOAP messages can be done using traditional HTTP 27 libraries. 28 29 See tests/soap_test.py for more details. 30 """ 31 32 import libxml2dom 33 from libxml2dom.macrolib import * 34 from libxml2dom.macrolib import \ 35 createDocument as Node_createDocument 36 37 # SOAP-related namespaces. 38 39 SOAP_ENVELOPE_NAMESPACE = "http://www.w3.org/2003/05/soap-envelope" 40 SOAP_ENCODING_NAMESPACE = "http://www.w3.org/2003/05/soap-encoding" 41 SOAP_RPC_NAMESPACE = "http://www.w3.org/2003/05/soap-rpc" 42 XS_NAMESPACE = "http://www.w3.org/2001/XMLSchema" 43 XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance" 44 45 # Archaic namespaces. 46 47 OLD_SOAP_ENVELOPE_NAMESPACE = "http://schemas.xmlsoap.org/soap/envelope/" 48 OLD_SOAP_ENCODING_NAMESPACE = "http://schemas.xmlsoap.org/soap/encoding/" 49 50 # Default namespace bindings for XPath. 51 52 default_ns = { 53 "env" : SOAP_ENVELOPE_NAMESPACE, 54 "enc" : SOAP_ENCODING_NAMESPACE, 55 "rpc" : SOAP_RPC_NAMESPACE, 56 "xs" : XS_NAMESPACE, 57 "xsi" : XSI_NAMESPACE, 58 "SOAP-ENV" : OLD_SOAP_ENVELOPE_NAMESPACE, 59 "SOAP-ENC" : OLD_SOAP_ENCODING_NAMESPACE 60 } 61 62 class SOAPImplementation(libxml2dom.Implementation): 63 64 "Contains a SOAP-specific implementation." 65 66 # Wrapping of documents. 67 68 def adoptDocument(self, node): 69 return SOAPDocument(node, self) 70 71 # Factory functions. 72 73 def get_node(self, _node, context_node): 74 75 """ 76 Get a libxml2dom node for the given low-level '_node' and libxml2dom 77 'context_node'. 78 """ 79 80 if Node_nodeType(_node) == context_node.ELEMENT_NODE: 81 82 # Make special envelope elements. 83 84 if Node_namespaceURI(_node) in (SOAP_ENVELOPE_NAMESPACE, OLD_SOAP_ENVELOPE_NAMESPACE): 85 if Node_localName(_node) == "Envelope": 86 return SOAPEnvelopeElement(_node, self, context_node.ownerDocument) 87 elif Node_localName(_node) == "Header": 88 return SOAPHeaderElement(_node, self, context_node.ownerDocument) 89 elif Node_localName(_node) == "Body": 90 return SOAPBodyElement(_node, self, context_node.ownerDocument) 91 elif Node_localName(_node) == "Fault": 92 return SOAPFaultElement(_node, self, context_node.ownerDocument) 93 elif Node_localName(_node) == "Code": 94 return SOAPCodeElement(_node, self, context_node.ownerDocument) 95 elif Node_localName(_node) == "Subcode": 96 return SOAPSubcodeElement(_node, self, context_node.ownerDocument) 97 elif Node_localName(_node) == "Value": 98 return SOAPValueElement(_node, self, context_node.ownerDocument) 99 elif Node_localName(_node) == "Text": 100 return SOAPTextElement(_node, self, context_node.ownerDocument) 101 102 # Detect the method element. 103 104 if Node_parentNode(_node) and Node_localName(Node_parentNode(_node)) == "Body" and \ 105 Node_namespaceURI(Node_parentNode(_node)) in (SOAP_ENVELOPE_NAMESPACE, OLD_SOAP_ENVELOPE_NAMESPACE): 106 107 return SOAPMethodElement(_node, self, context_node.ownerDocument) 108 109 # Otherwise, make generic SOAP elements. 110 111 return SOAPElement(_node, self, context_node.ownerDocument) 112 113 else: 114 return libxml2dom.Implementation.get_node(self, _node, context_node) 115 116 # Convenience functions. 117 118 def createSOAPMessage(self, namespaceURI, localName): 119 120 "Create a new SOAP message document (fragment)." 121 122 return SOAPDocument(Node_createDocument(namespaceURI, localName, None), self).documentElement 123 124 # Node classes. 125 126 class SOAPNode(libxml2dom.Node): 127 128 "Convenience modifications to nodes specific to libxml2dom.soap." 129 130 def xpath(self, expr, variables=None, namespaces=None): 131 132 """ 133 Evaluate the given 'expr' using the optional 'variables' and 134 'namespaces'. If not otherwise specified, the prefixes given in the 135 module global 'default_ns' will be bound as in that dictionary. 136 """ 137 138 ns = {} 139 ns.update(default_ns) 140 ns.update(namespaces or {}) 141 return libxml2dom.Node.xpath(self, expr, variables, ns) 142 143 # All nodes support convenience methods. 144 145 def convert(self, node): 146 return node.textContent.strip() 147 148 def _contents(self): 149 # NOTE: Should check whether this should be a leaf element. 150 if not self.xpath("*"): 151 return (self.localName, getattr(self.ownerDocument, "convert", self.convert)(self)) 152 else: 153 return (self.localName, SOAPContents(self)) 154 155 def __len__(self): 156 return 2 157 158 def __getitem__(self, i): 159 return self.contents[i] 160 161 def __eq__(self, other): 162 if hasattr(other, "contents"): 163 return self.contents == other.contents 164 else: 165 return self.contents == other 166 167 # Node construction methods. 168 169 def createSOAPElement(self, localName): 170 171 "Create an element with the appropriate namespace and prefix." 172 173 ref_element = self.ownerDocument.documentElement 174 prefix = ref_element.prefix 175 if prefix: 176 name = prefix + ":" + localName 177 else: 178 name = localName 179 return self.createElementNS(ref_element.namespaceURI, name) 180 181 contents = property(_contents) 182 183 class SOAPContents(object): 184 185 "A wrapper around another node in order to provide sequence-like access." 186 187 def __init__(self, node): 188 self.node = node 189 190 def __len__(self): 191 return len(self.node.xpath("*")) 192 193 def __getitem__(self, i): 194 return self.node.xpath("*")[i] 195 196 def __eq__(self, other): 197 for i, j in map(None, self, other): 198 if i != j: 199 return False 200 return True 201 202 class SOAPDocument(libxml2dom._Document, SOAPNode): 203 204 "A SOAP document fragment." 205 206 def _envelope(self): 207 return self.xpath("env:Envelope|SOAP-ENV:Envelope")[0] 208 209 envelope = property(_envelope) 210 211 # Convenience methods and properties. 212 213 def _fault(self): 214 return self.envelope.body.fault 215 216 def _method(self): 217 return self.envelope.body.method 218 219 fault = property(_fault) 220 method = property(_method) 221 222 class SOAPElement(SOAPNode): 223 224 "A SOAP element." 225 226 pass 227 228 class SOAPEnvelopeElement(SOAPNode): 229 230 "A SOAP envelope element." 231 232 def _body(self): 233 return self.xpath("env:Body|SOAP-ENV:Body")[0] 234 235 def _setBody(self, body): 236 self.appendChild(body) 237 238 def _delBody(self): 239 self.removeChild(self.body) 240 241 def createBody(self): 242 return self.createSOAPElement("Body") 243 244 body = property(_body, _setBody, _delBody) 245 246 class SOAPHeaderElement(SOAPNode): 247 248 "A SOAP header element." 249 250 pass 251 252 class SOAPBodyElement(SOAPNode): 253 254 "A SOAP body element." 255 256 def _fault(self): 257 return (self.xpath("env:Fault|SOAP-ENV:Fault") or [None])[0] 258 259 def _method(self): 260 if self.namespaceURI == SOAP_ENVELOPE_NAMESPACE: 261 return (self.xpath("*[@env:encodingStyle = '%s']" % SOAP_ENCODING_NAMESPACE) or [None])[0] 262 else: 263 return (self.xpath("*") or [None])[0] 264 265 # Node construction methods. 266 267 def createFault(self): 268 return self.createSOAPElement("Fault") 269 270 fault = property(_fault) 271 method = property(_method) 272 273 class SOAPMethodElement(SOAPNode): 274 275 "A SOAP method element." 276 277 def _methodName(self): 278 return self.localName 279 280 def _resultParameter(self): 281 return (self.xpath(".//rpc:result") or [None])[0] 282 283 def _resultParameterValue(self): 284 if self.resultParameter: 285 name = self.resultParameter.textContent.strip() 286 result = self.xpath(".//" + name, namespaces={self.prefix : self.namespaceURI}) 287 if result: 288 return result[0].textContent.strip() 289 else: 290 return None 291 else: 292 return None 293 294 def _parameterValues(self): 295 return [value.contents for value in self.xpath("*")] 296 297 methodName = property(_methodName) 298 resultParameter = property(_resultParameter) 299 resultParameterValue = property(_resultParameterValue) 300 parameterValues = property(_parameterValues) 301 302 class SOAPFaultElement(SOAPNode): 303 304 "A SOAP fault element." 305 306 def _code(self): 307 code = self.xpath("env:Code|SOAP-ENV:Code") 308 if code: 309 return code[0].value 310 else: 311 return None 312 313 def _subcode(self): 314 subcode = self.xpath("./env:Code/env:Subcode|./SOAP-ENV:Code/SOAP-ENV:Subcode") 315 if subcode: 316 return subcode[0].value 317 else: 318 return None 319 320 def _reason(self): 321 return (self.xpath("env:Reason|SOAP-ENV:Reason") or [None])[0] 322 323 def _detail(self): 324 return (self.xpath("env:Detail|SOAP-ENV:Detail") or [None])[0] 325 326 def createCode(self): 327 return self.createSOAPElement("Code") 328 329 code = property(_code) 330 subcode = property(_subcode) 331 reason = property(_reason) 332 detail = property(_detail) 333 334 class SOAPSubcodeElement(SOAPNode): 335 336 "A SOAP subcode element." 337 338 def _value(self): 339 value = self.xpath("env:Value|SOAP-ENV:Value") 340 if value: 341 return value[0].textContent.strip() 342 else: 343 return None 344 345 def _setValue(self, value): 346 nodes = self.xpath("env:Value|SOAP-ENV:Value") 347 v = self.createValue() 348 if nodes: 349 self.replaceChild(v, nodes[0]) 350 else: 351 self.appendChild(v) 352 v.value = value 353 354 def createValue(self, value=None): 355 code_value = self.createSOAPElement("Value") 356 if value is not None: 357 code_value.value = code 358 return code_value 359 360 value = property(_value, _setValue) 361 362 class SOAPCodeElement(SOAPSubcodeElement): 363 364 "A SOAP code element." 365 366 def _subcode(self): 367 return (self.xpath("env:Subcode|SOAP-ENV:Subcode") or [None])[0] 368 369 def createSubcode(self): 370 return self.createSOAPElement("Subcode") 371 372 subcode = property(_subcode) 373 374 class SOAPValueElement(SOAPNode): 375 376 "A SOAP value element." 377 378 def _value(self): 379 return self.textContent 380 381 def _setValue(self, value): 382 for node in self.childNodes: 383 self.removeChild(node) 384 text = self.ownerDocument.createTextNode(value) 385 self.appendChild(text) 386 387 value = property(_value, _setValue) 388 389 class SOAPTextElement(SOAPValueElement): 390 391 "A SOAP text element." 392 393 def _lang(self): 394 return self.getAttributeNS(libxml2dom.XML_NAMESPACE, "lang") 395 396 def _setLang(self, value): 397 self.setAttributeNS(libxml2dom.XML_NAMESPACE, "xml:lang", value) 398 399 lang = property(_lang, _setLang) 400 401 # Utility functions. 402 403 createDocument = libxml2dom.createDocument 404 createDocumentType = libxml2dom.createDocumentType 405 406 def createSOAPMessage(namespaceURI, localName): 407 return default_impl.createSOAPMessage(namespaceURI, localName) 408 409 def parse(stream_or_string, html=0, htmlencoding=None, unfinished=0, impl=None): 410 return libxml2dom.parse(stream_or_string, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 411 412 def parseFile(filename, html=0, htmlencoding=None, unfinished=0, impl=None): 413 return libxml2dom.parseFile(filename, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 414 415 def parseString(s, html=0, htmlencoding=None, unfinished=0, impl=None): 416 return libxml2dom.parseString(s, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 417 418 def parseURI(uri, html=0, htmlencoding=None, unfinished=0, impl=None): 419 return libxml2dom.parseURI(uri, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 420 421 # Single instance of the implementation. 422 423 default_impl = SOAPImplementation() 424 425 # vim: tabstop=4 expandtab shiftwidth=4