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): 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 return self.createElementNS(ref_element.namespaceURI, name) 149 150 def makeSOAPElement(self, localName): 151 152 """ 153 Create and insert an element with the appropriate namespace and prefix. 154 """ 155 156 element = self.createSOAPElement(localName) 157 self.appendChild(element) 158 return element 159 160 contents = property(_contents) 161 162 class SOAPDocument(libxml2dom._Document, SOAPNode): 163 164 "A SOAP document fragment." 165 166 def _envelope(self): 167 return (self.xpath("env:Envelope|SOAP-ENV:Envelope") or [None])[0] 168 169 envelope = property(_envelope) 170 171 # Convenience methods and properties. 172 173 def _fault(self): 174 if self.envelope is not None: 175 return self.envelope.fault 176 else: 177 return None 178 179 def _method(self): 180 if self.envelope is not None: 181 return self.envelope.method 182 else: 183 return None 184 185 fault = property(_fault) 186 method = property(_method) 187 188 class SOAPEnvelopeElement(SOAPElement): 189 190 "A SOAP envelope element." 191 192 def _body(self): 193 return (self.xpath("env:Body|SOAP-ENV:Body") or [None])[0] 194 195 def _setBody(self, body): 196 self.appendChild(body) 197 198 def _delBody(self): 199 self.removeChild(self.body) 200 201 # Convenience methods and properties. 202 203 def _fault(self): 204 if self.body is not None: 205 return self.body.fault 206 else: 207 return None 208 209 def _method(self): 210 if self.body is not None: 211 return self.body.method 212 else: 213 return None 214 215 fault = property(_fault) 216 method = property(_method) 217 218 # Node construction methods. 219 220 def createBody(self): 221 return self.createSOAPElement("Body") 222 223 def makeBody(self): 224 element = self.createBody() 225 self.add_or_replace_element(element) 226 return element 227 228 body = property(_body, _setBody, _delBody) 229 230 class SOAPHeaderElement(SOAPElement): 231 232 "A SOAP header element." 233 234 pass 235 236 class SOAPBodyElement(SOAPElement): 237 238 "A SOAP body element." 239 240 def _fault(self): 241 return (self.xpath("env:Fault|SOAP-ENV:Fault") or [None])[0] 242 243 def _method(self): 244 if self.namespaceURI == SOAP_ENVELOPE_NAMESPACE: 245 return (self.xpath("*[@env:encodingStyle = '%s']" % SOAP_ENCODING_NAMESPACE) or [None])[0] 246 else: 247 return (self.xpath("*") or [None])[0] 248 249 # Node construction methods. 250 251 def createMethod(self, namespaceURI, name): 252 if self.method is not None: 253 self.removeChild(self.method) 254 element = self.createElementNS(namespaceURI, name) 255 element.setAttributeNS(SOAP_ENVELOPE_NAMESPACE, "env:encodingStyle", SOAP_ENCODING_NAMESPACE) 256 return element 257 258 def makeMethod(self, namespaceURI, name): 259 element = self.createMethod(namespaceURI, name) 260 self.appendChild(element) 261 return element 262 263 def createFault(self): 264 return self.createSOAPElement("Fault") 265 266 def makeFault(self): 267 element = self.createFault() 268 self.add_or_replace_element(element) 269 return element 270 271 fault = property(_fault) 272 method = property(_method) 273 274 class SOAPMethodElement(SOAPElement): 275 276 "A SOAP method element." 277 278 def _methodName(self): 279 return self.localName 280 281 def _resultParameter(self): 282 return (self.xpath(".//rpc:result") or [None])[0] 283 284 def _resultParameterValue(self): 285 if self.resultParameter: 286 name = self.resultParameter.textContent.strip() 287 result = self.xpath(".//" + name, namespaces={self.prefix : self.namespaceURI}) 288 if result: 289 return result[0].textContent.strip() 290 else: 291 return None 292 else: 293 return None 294 295 def _parameters(self): 296 return self.xpath("*") 297 298 def _parameterValues(self): 299 return self.values() 300 301 def __repr__(self): 302 return "<SOAPMethodElement: %r>" % self.parameters 303 304 methodName = property(_methodName) 305 resultParameter = property(_resultParameter) 306 resultParameterValue = property(_resultParameterValue) 307 parameterValues = property(_parameterValues) 308 parameters = property(_parameters) 309 310 class SOAPFaultElement(SOAPElement): 311 312 "A SOAP fault element." 313 314 def _code(self): 315 code = self.xpath("env:Code|SOAP-ENV:Code") 316 if code: 317 return code[0].value 318 else: 319 return None 320 321 def _subcode(self): 322 subcode = self.xpath("./env:Code/env:Subcode|./SOAP-ENV:Code/SOAP-ENV:Subcode") 323 if subcode: 324 return subcode[0].value 325 else: 326 return None 327 328 def _reason(self): 329 return (self.xpath("env:Reason|SOAP-ENV:Reason") or [None])[0] 330 331 def _detail(self): 332 return (self.xpath("env:Detail|SOAP-ENV:Detail") or [None])[0] 333 334 # Node construction methods. 335 336 def createCode(self): 337 return self.createSOAPElement("Code") 338 339 def makeCode(self): 340 element = self.createCode() 341 self.add_or_replace_element(element) 342 return element 343 344 code = property(_code) 345 subcode = property(_subcode) 346 reason = property(_reason) 347 detail = property(_detail) 348 349 class SOAPSubcodeElement(SOAPElement): 350 351 "A SOAP subcode element." 352 353 def _value(self): 354 value = self.xpath("env:Value|SOAP-ENV:Value") 355 if value: 356 return value[0].textContent.strip() 357 else: 358 return None 359 360 def _setValue(self, value): 361 nodes = self.xpath("env:Value|SOAP-ENV:Value") 362 v = self.createValue() 363 if nodes: 364 self.replaceChild(v, nodes[0]) 365 else: 366 self.appendChild(v) 367 v.value = value 368 369 # Node construction methods. 370 371 def createValue(self, value=None): 372 code_value = self.createSOAPElement("Value") 373 if value is not None: 374 code_value.value = code 375 return code_value 376 377 def makeValue(self, value=None): 378 code_value = self.createValue(value) 379 self.add_or_replace_element(code_value) 380 return code_value 381 382 value = property(_value, _setValue) 383 384 class SOAPCodeElement(SOAPSubcodeElement): 385 386 "A SOAP code element." 387 388 def _subcode(self): 389 return (self.xpath("env:Subcode|SOAP-ENV:Subcode") or [None])[0] 390 391 # Node construction methods. 392 393 def createSubcode(self): 394 return self.createSOAPElement("Subcode") 395 396 def makeSubcode(self): 397 element = self.createSubcode() 398 self.add_or_replace_element(element) 399 return element 400 401 subcode = property(_subcode) 402 403 class SOAPValueElement(SOAPElement): 404 405 "A SOAP value element." 406 407 def _value(self): 408 return self.textContent 409 410 def _setValue(self, value): 411 for node in self.childNodes: 412 self.removeChild(node) 413 text = self.ownerDocument.createTextNode(value) 414 self.appendChild(text) 415 416 value = property(_value, _setValue) 417 418 class SOAPTextElement(SOAPValueElement): 419 420 "A SOAP text element." 421 422 def _lang(self): 423 return self.getAttributeNS(libxml2dom.XML_NAMESPACE, "lang") 424 425 def _setLang(self, value): 426 self.setAttributeNS(libxml2dom.XML_NAMESPACE, "xml:lang", value) 427 428 lang = property(_lang, _setLang) 429 430 # Implementation-related functionality. 431 432 class SOAPImplementation(libxml2dom.Implementation): 433 434 "Contains a SOAP-specific implementation." 435 436 # Mapping of element names to wrappers. 437 438 _class_for_name = { 439 "Envelope" : SOAPEnvelopeElement, 440 "Header" : SOAPHeaderElement, 441 "Body" : SOAPBodyElement, 442 "Fault" : SOAPFaultElement, 443 "Code" : SOAPCodeElement, 444 "Subcode" : SOAPSubcodeElement, 445 "Value" : SOAPValueElement, 446 "Text" : SOAPTextElement 447 } 448 449 # Wrapping of documents. 450 451 def adoptDocument(self, node): 452 return SOAPDocument(node, self) 453 454 # Factory functions. 455 456 def get_node(self, _node, context_node): 457 458 """ 459 Get a libxml2dom node for the given low-level '_node' and libxml2dom 460 'context_node'. 461 """ 462 463 if Node_nodeType(_node) == context_node.ELEMENT_NODE: 464 465 # Make special envelope elements. 466 467 if Node_namespaceURI(_node) in (SOAP_ENVELOPE_NAMESPACE, OLD_SOAP_ENVELOPE_NAMESPACE): 468 cls = self._class_for_name[Node_localName(_node)] 469 return cls(_node, self, context_node.ownerDocument) 470 471 # Detect the method element. 472 473 if Node_parentNode(_node) and Node_localName(Node_parentNode(_node)) == "Body" and \ 474 Node_namespaceURI(Node_parentNode(_node)) in (SOAP_ENVELOPE_NAMESPACE, OLD_SOAP_ENVELOPE_NAMESPACE): 475 476 return SOAPMethodElement(_node, self, context_node.ownerDocument) 477 478 # Otherwise, make generic SOAP elements. 479 480 return SOAPElement(_node, self, context_node.ownerDocument) 481 482 else: 483 return libxml2dom.Implementation.get_node(self, _node, context_node) 484 485 # Convenience functions. 486 487 def createSOAPMessage(self): 488 489 "Create a new SOAP message document (fragment)." 490 491 return SOAPDocument(Node_createDocument(SOAP_ENVELOPE_NAMESPACE, "env:Envelope", None), self).documentElement 492 493 # Utility functions. 494 495 createDocument = libxml2dom.createDocument 496 createDocumentType = libxml2dom.createDocumentType 497 498 def createSOAPMessage(): 499 return default_impl.createSOAPMessage() 500 501 def parse(stream_or_string, html=0, htmlencoding=None, unfinished=0, impl=None): 502 return libxml2dom.parse(stream_or_string, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 503 504 def parseFile(filename, html=0, htmlencoding=None, unfinished=0, impl=None): 505 return libxml2dom.parseFile(filename, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 506 507 def parseString(s, html=0, htmlencoding=None, unfinished=0, impl=None): 508 return libxml2dom.parseString(s, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 509 510 def parseURI(uri, html=0, htmlencoding=None, unfinished=0, impl=None): 511 return libxml2dom.parseURI(uri, html=html, htmlencoding=htmlencoding, unfinished=unfinished, impl=(impl or default_impl)) 512 513 # Single instance of the implementation. 514 515 default_impl = SOAPImplementation() 516 517 # vim: tabstop=4 expandtab shiftwidth=4