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, 2008 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/test_soap.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 from libxml2dom.values import ContentValue, SequenceValue 37 38 # SOAP-related namespaces. 39 40 SOAP_ENVELOPE_NAMESPACE = "http://www.w3.org/2003/05/soap-envelope" 41 SOAP_ENCODING_NAMESPACE = "http://www.w3.org/2003/05/soap-encoding" 42 SOAP_RPC_NAMESPACE = "http://www.w3.org/2003/05/soap-rpc" 43 XS_NAMESPACE = "http://www.w3.org/2001/XMLSchema" 44 XSI_NAMESPACE = "http://www.w3.org/2001/XMLSchema-instance" 45 46 # Archaic namespaces. 47 48 OLD_SOAP_ENVELOPE_NAMESPACE = "http://schemas.xmlsoap.org/soap/envelope/" 49 OLD_SOAP_ENCODING_NAMESPACE = "http://schemas.xmlsoap.org/soap/encoding/" 50 51 # Default namespace bindings for XPath. 52 53 default_ns = { 54 "env" : SOAP_ENVELOPE_NAMESPACE, 55 "enc" : SOAP_ENCODING_NAMESPACE, 56 "rpc" : SOAP_RPC_NAMESPACE, 57 "xs" : XS_NAMESPACE, 58 "xsi" : XSI_NAMESPACE, 59 "SOAP-ENV" : OLD_SOAP_ENVELOPE_NAMESPACE, 60 "SOAP-ENC" : OLD_SOAP_ENCODING_NAMESPACE 61 } 62 63 # Node classes. 64 65 class SOAPNode(libxml2dom.Node): 66 67 "Convenience modifications to nodes specific to libxml2dom.soap." 68 69 def xpath(self, expr, variables=None, namespaces=None): 70 71 """ 72 Evaluate the given 'expr' using the optional 'variables' and 73 'namespaces'. If not otherwise specified, the prefixes given in the 74 module global 'default_ns' will be bound as in that dictionary. 75 """ 76 77 ns = {} 78 ns.update(default_ns) 79 ns.update(namespaces or {}) 80 return libxml2dom.Node.xpath(self, expr, variables, ns) 81 82 def add_or_replace_element(self, new_element): 83 84 """ 85 Add or replace the given 'new_element', using its localName to find any 86 element to be replaced. 87 """ 88 89 elements = self.xpath(new_element.localName) 90 if elements: 91 self.replaceChild(new_element, elements[0]) 92 else: 93 self.appendChild(new_element) 94 95 class SOAPElement(ContentValue, SequenceValue, SOAPNode): 96 97 "A SOAP element." 98 99 def convert(self, node): 100 return node.textContent.strip() 101 102 def values(self): 103 return [v.contents for v in self.xpath("*")] 104 105 def _contents(self): 106 # NOTE: Should check whether this should be a leaf element. 107 if not self.xpath("*"): 108 return (self.localName, getattr(self.ownerDocument, "convert", self.convert)(self)) 109 else: 110 return (self.localName, self) 111 112 def __len__(self): 113 if not self.xpath("*"): 114 return 2 115 else: 116 return SequenceValue.__len__(self) 117 118 def __eq__(self, other): 119 if not self.xpath("*"): 120 return ContentValue.__eq__(self, other) 121 else: 122 return SequenceValue.__eq__(self, other) 123 124 def __ne__(self, other): 125 if not self.xpath("*"): 126 return ContentValue.__ne__(self, other) 127 else: 128 return SequenceValue.__ne__(self, other) 129 130 def __repr__(self): 131 if self.contents[1] is self: 132 return "<%s: %r>" % (self.__class__.__name__, self.values()) 133 else: 134 return "<%s: %r>" % (self.__class__.__name__, self.contents) 135 136 # Node construction methods. 137 138 def createSOAPElement(self, localName, insert=0): 139 140 "Create an element with the appropriate namespace and prefix." 141 142 ref_element = self.ownerDocument.documentElement 143 prefix = ref_element.prefix 144 if prefix: 145 name = prefix + ":" + localName 146 else: 147 name = localName 148 element = self.createElementNS(ref_element.namespaceURI, name) 149 if insert: 150 self.appendChild(element) 151 return element 152 153 contents = property(_contents) 154 155 class SOAPDocument(libxml2dom._Document, SOAPNode): 156 157 "A SOAP document fragment." 158 159 def _envelope(self): 160 return (self.xpath("env:Envelope|SOAP-ENV:Envelope") or [None])[0] 161 162 envelope = property(_envelope) 163 164 # Convenience methods and properties. 165 166 def _fault(self): 167 if self.envelope is not None: 168 return self.envelope.fault 169 else: 170 return None 171 172 def _method(self): 173 if self.envelope is not None: 174 return self.envelope.method 175 else: 176 return None 177 178 fault = property(_fault) 179 method = property(_method) 180 181 class SOAPEnvelopeElement(SOAPElement): 182 183 "A SOAP envelope element." 184 185 def _body(self): 186 return (self.xpath("env:Body|SOAP-ENV:Body") or [None])[0] 187 188 def _setBody(self, body): 189 self.appendChild(body) 190 191 def _delBody(self): 192 self.removeChild(self.body) 193 194 # Convenience methods and properties. 195 196 def _fault(self): 197 if self.body is not None: 198 return self.body.fault 199 else: 200 return None 201 202 def _method(self): 203 if self.body is not None: 204 return self.body.method 205 else: 206 return None 207 208 fault = property(_fault) 209 method = property(_method) 210 211 # Node construction methods. 212 213 def createBody(self, insert=0): 214 element = self.createSOAPElement("Body") 215 if insert: 216 self.add_or_replace_element(element) 217 return element 218 219 body = property(_body, _setBody, _delBody) 220 221 class SOAPHeaderElement(SOAPElement): 222 223 "A SOAP header element." 224 225 pass 226 227 class SOAPBodyElement(SOAPElement): 228 229 "A SOAP body element." 230 231 def _fault(self): 232 return (self.xpath("env:Fault|SOAP-ENV:Fault") or [None])[0] 233 234 def _method(self): 235 if self.namespaceURI == SOAP_ENVELOPE_NAMESPACE: 236 return (self.xpath("*[@env:encodingStyle = '%s']" % SOAP_ENCODING_NAMESPACE) or [None])[0] 237 else: 238 return (self.xpath("*") or [None])[0] 239 240 # Node construction methods. 241 242 def createMethod(self, namespaceURI, name, insert=0): 243 if self.method is not None: 244 self.removeChild(self.method) 245 element = self.createElementNS(namespaceURI, name) 246 element.setAttributeNS(SOAP_ENVELOPE_NAMESPACE, "env:encodingStyle", SOAP_ENCODING_NAMESPACE) 247 self.appendChild(element) 248 return element 249 250 def createFault(self, insert=0): 251 element = self.createSOAPElement("Fault") 252 if insert: 253 self.add_or_replace_element(element) 254 return element 255 256 fault = property(_fault) 257 method = property(_method) 258 259 class SOAPMethodElement(SOAPElement): 260 261 "A SOAP method element." 262 263 def _methodName(self): 264 return self.localName 265 266 def _resultParameter(self): 267 return (self.xpath(".//rpc:result") or [None])[0] 268 269 def _resultParameterValue(self): 270 if self.resultParameter: 271 name = self.resultParameter.textContent.strip() 272 result = self.xpath(".//" + name, namespaces={self.prefix : self.namespaceURI}) 273 if result: 274 return result[0].textContent.strip() 275 else: 276 return None 277 else: 278 return None 279 280 def _parameters(self): 281 return self.xpath("*") 282 283 def _parameterValues(self): 284 return self.values() 285 286 def __repr__(self): 287 return "<SOAPMethodElement: %r>" % self.parameters 288 289 methodName = property(_methodName) 290 resultParameter = property(_resultParameter) 291 resultParameterValue = property(_resultParameterValue) 292 parameterValues = property(_parameterValues) 293 parameters = property(_parameters) 294 295 class SOAPFaultElement(SOAPElement): 296 297 "A SOAP fault element." 298 299 def _code(self): 300 code = self.xpath("env:Code|SOAP-ENV:Code") 301 if code: 302 return code[0].value 303 else: 304 return None 305 306 def _subcode(self): 307 subcode = self.xpath("./env:Code/env:Subcode|./SOAP-ENV:Code/SOAP-ENV:Subcode") 308 if subcode: 309 return subcode[0].value 310 else: 311 return None 312 313 def _reason(self): 314 return (self.xpath("env:Reason|SOAP-ENV:Reason") or [None])[0] 315 316 def _detail(self): 317 return (self.xpath("env:Detail|SOAP-ENV:Detail") or [None])[0] 318 319 # Node construction methods. 320 321 def createCode(self, insert=0): 322 element = self.createSOAPElement("Code") 323 if insert: 324 self.add_or_replace_element(element) 325 return element 326 327 code = property(_code) 328 subcode = property(_subcode) 329 reason = property(_reason) 330 detail = property(_detail) 331 332 class SOAPSubcodeElement(SOAPElement): 333 334 "A SOAP subcode element." 335 336 def _value(self): 337 value = self.xpath("env:Value|SOAP-ENV:Value") 338 if value: 339 return value[0].textContent.strip() 340 else: 341 return None 342 343 def _setValue(self, value): 344 nodes = self.xpath("env:Value|SOAP-ENV:Value") 345 v = self.createValue() 346 if nodes: 347 self.replaceChild(v, nodes[0]) 348 else: 349 self.appendChild(v) 350 v.value = value 351 352 # Node construction methods. 353 354 def createValue(self, value=None, insert=0): 355 code_value = self.createSOAPElement("Value") 356 if value is not None: 357 code_value.value = code 358 if insert: 359 self.add_or_replace_element(code_value) 360 return code_value 361 362 value = property(_value, _setValue) 363 364 class SOAPCodeElement(SOAPSubcodeElement): 365 366 "A SOAP code element." 367 368 def _subcode(self): 369 return (self.xpath("env:Subcode|SOAP-ENV:Subcode") or [None])[0] 370 371 # Node construction methods. 372 373 def createSubcode(self, insert=0): 374 element = self.createSOAPElement("Subcode") 375 if insert: 376 self.add_or_replace_element(element) 377 return element 378 379 subcode = property(_subcode) 380 381 class SOAPValueElement(SOAPElement): 382 383 "A SOAP value element." 384 385 def _value(self): 386 return self.textContent 387 388 def _setValue(self, value): 389 for node in self.childNodes: 390 self.removeChild(node) 391 text = self.ownerDocument.createTextNode(value) 392 self.appendChild(text) 393 394 value = property(_value, _setValue) 395 396 class SOAPTextElement(SOAPValueElement): 397 398 "A SOAP text element." 399 400 def _lang(self): 401 return self.getAttributeNS(libxml2dom.XML_NAMESPACE, "lang") 402 403 def _setLang(self, value): 404 self.setAttributeNS(libxml2dom.XML_NAMESPACE, "xml:lang", value) 405 406 lang = property(_lang, _setLang) 407 408 # Implementation-related functionality. 409 410 class SOAPImplementation(libxml2dom.Implementation): 411 412 "Contains a SOAP-specific implementation." 413 414 # Mapping of element names to wrappers. 415 416 _class_for_name = { 417 "Envelope" : SOAPEnvelopeElement, 418 "Header" : SOAPHeaderElement, 419 "Body" : SOAPBodyElement, 420 "Fault" : SOAPFaultElement, 421 "Code" : SOAPCodeElement, 422 "Subcode" : SOAPSubcodeElement, 423 "Value" : SOAPValueElement, 424 "Text" : SOAPTextElement 425 } 426 427 # Wrapping of documents. 428 429 def adoptDocument(self, node): 430 return SOAPDocument(node, self) 431 432 # Factory functions. 433 434 def get_node(self, _node, context_node): 435 436 """ 437 Get a libxml2dom node for the given low-level '_node' and libxml2dom 438 'context_node'. 439 """ 440 441 if Node_nodeType(_node) == context_node.ELEMENT_NODE: 442 443 # Make special envelope elements. 444 445 if Node_namespaceURI(_node) in (SOAP_ENVELOPE_NAMESPACE, OLD_SOAP_ENVELOPE_NAMESPACE): 446 cls = self._class_for_name[Node_localName(_node)] 447 return cls(_node, self, context_node.ownerDocument) 448 449 # Detect the method element. 450 451 if Node_parentNode(_node) and Node_localName(Node_parentNode(_node)) == "Body" and \ 452 Node_namespaceURI(Node_parentNode(_node)) in (SOAP_ENVELOPE_NAMESPACE, OLD_SOAP_ENVELOPE_NAMESPACE): 453 454 return SOAPMethodElement(_node, self, context_node.ownerDocument) 455 456 # Otherwise, make generic SOAP elements. 457 458 return SOAPElement(_node, self, context_node.ownerDocument) 459 460 else: 461 return libxml2dom.Implementation.get_node(self, _node, context_node) 462 463 # Convenience functions. 464 465 def createSOAPMessage(self): 466 467 "Create a new SOAP message document (fragment)." 468 469 return SOAPDocument(Node_createDocument(SOAP_ENVELOPE_NAMESPACE, "env:Envelope", None), self).documentElement 470 471 # Utility functions. 472 473 createDocument = libxml2dom.createDocument 474 createDocumentType = libxml2dom.createDocumentType 475 476 def createSOAPMessage(): 477 return default_impl.createSOAPMessage() 478 479 def parse(stream_or_string, html=0, htmlencoding=None, unfinished=0, impl=None): 480 return libxml2dom.parse(stream_or_string, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 481 482 def parseFile(filename, html=0, htmlencoding=None, unfinished=0, impl=None): 483 return libxml2dom.parseFile(filename, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 484 485 def parseString(s, html=0, htmlencoding=None, unfinished=0, impl=None): 486 return libxml2dom.parseString(s, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 487 488 def parseURI(uri, html=0, htmlencoding=None, unfinished=0, impl=None): 489 return libxml2dom.parseURI(uri, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 490 491 # Single instance of the implementation. 492 493 default_impl = SOAPImplementation() 494 495 # vim: tabstop=4 expandtab shiftwidth=4