Rework the parsing of soap response.
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
|
||||
#include "example/common/book_xml.h"
|
||||
|
||||
static const std::string kResultName = "Result";
|
||||
|
||||
static void PrintSeparateLine() {
|
||||
std::cout << "--------------------------------";
|
||||
std::cout << "--------------------------------";
|
||||
@@ -16,7 +18,6 @@ BookClient::BookClient(const std::string& host, const std::string& port)
|
||||
: soap_client_(host, port), code_(0) {
|
||||
soap_client_.set_url("/book");
|
||||
soap_client_.set_service_ns({ "ser", "http://www.example.com/book/" });
|
||||
soap_client_.set_result_name("Result");
|
||||
|
||||
// Customize response XML format.
|
||||
soap_client_.set_format_raw(false);
|
||||
@@ -91,7 +92,6 @@ bool BookClient::Call0(const std::string& operation, std::string* result_str) {
|
||||
return Call(operation, {}, result_str);
|
||||
}
|
||||
|
||||
|
||||
bool BookClient::Call1(const std::string& operation,
|
||||
webcc::SoapParameter&& parameter,
|
||||
std::string* result_str) {
|
||||
@@ -104,7 +104,8 @@ bool BookClient::Call1(const std::string& operation,
|
||||
bool BookClient::Call(const std::string& operation,
|
||||
std::vector<webcc::SoapParameter>&& parameters,
|
||||
std::string* result_str) {
|
||||
if (!soap_client_.Request(operation, std::move(parameters), result_str)) {
|
||||
if (!soap_client_.Request(operation, std::move(parameters), kResultName,
|
||||
result_str)) {
|
||||
PrintError();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
static const std::string kResultName = "Result";
|
||||
|
||||
class CalcClient {
|
||||
public:
|
||||
CalcClient(const std::string& host, const std::string& port)
|
||||
@@ -15,7 +17,6 @@ class CalcClient {
|
||||
soap_client_.set_service_ns({
|
||||
"ser", "http://www.example.com/calculator/"
|
||||
});
|
||||
soap_client_.set_result_name("Result");
|
||||
|
||||
// Customize request XML format.
|
||||
soap_client_.set_format_raw(false);
|
||||
@@ -54,7 +55,8 @@ class CalcClient {
|
||||
};
|
||||
|
||||
std::string result_str;
|
||||
if (!soap_client_.Request(operation, std::move(parameters), &result_str)) {
|
||||
if (!soap_client_.Request(operation, std::move(parameters), kResultName,
|
||||
&result_str)) {
|
||||
PrintError();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
static const std::string kResultName = "Result";
|
||||
|
||||
class CalcClient {
|
||||
public:
|
||||
// NOTE: Parasoft's calculator service uses SOAP V1.1.
|
||||
@@ -16,7 +18,6 @@ class CalcClient {
|
||||
soap_client_.set_service_ns({
|
||||
"cal", "http://www.parasoft.com/wsdl/calculator/"
|
||||
});
|
||||
soap_client_.set_result_name("Result");
|
||||
|
||||
// Customize request XML format.
|
||||
soap_client_.set_format_raw(false);
|
||||
@@ -39,6 +40,11 @@ class CalcClient {
|
||||
return Calc("divide", "numerator", "denominator", x, y, result);
|
||||
}
|
||||
|
||||
// Only for testing purpose.
|
||||
bool Unknown(double x, double y, double* result) {
|
||||
return Calc("unknown", "x", "y", x, y, result);
|
||||
}
|
||||
|
||||
private:
|
||||
bool Calc(const std::string& operation,
|
||||
const std::string& x_name, const std::string& y_name,
|
||||
@@ -50,7 +56,8 @@ class CalcClient {
|
||||
};
|
||||
|
||||
std::string result_str;
|
||||
if (!soap_client_.Request(operation, std::move(parameters), &result_str)) {
|
||||
if (!soap_client_.Request(operation, std::move(parameters), kResultName,
|
||||
&result_str)) {
|
||||
PrintError();
|
||||
return false;
|
||||
}
|
||||
@@ -69,7 +76,12 @@ class CalcClient {
|
||||
if (soap_client_.timed_out()) {
|
||||
std::cout << " (timed out)";
|
||||
}
|
||||
|
||||
std::cout << std::endl;
|
||||
|
||||
if (soap_client_.fault()) {
|
||||
std::cout << *soap_client_.fault() << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
webcc::SoapClient soap_client_;
|
||||
@@ -102,5 +114,7 @@ int main() {
|
||||
printf("divide: %.1f\n", result);
|
||||
}
|
||||
|
||||
calc.Unknown(x, y, &result);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ const char* DescribeError(Error error) {
|
||||
return "Socket write error";
|
||||
case kHttpError:
|
||||
return "HTTP error";
|
||||
case kServerError:
|
||||
return "Server error";
|
||||
case kXmlError:
|
||||
return "XML error";
|
||||
default:
|
||||
|
||||
+14
-4
@@ -70,21 +70,31 @@ struct HttpStatus {
|
||||
kNotModified = 304,
|
||||
kBadRequest = 400,
|
||||
kNotFound = 404,
|
||||
InternalServerError = 500,
|
||||
kInternalServerError = 500,
|
||||
kNotImplemented = 501,
|
||||
kServiceUnavailable = 503,
|
||||
};
|
||||
};
|
||||
|
||||
// Error codes.
|
||||
// Client side error codes.
|
||||
enum Error {
|
||||
kNoError = 0,
|
||||
kNoError = 0, // i.e., OK
|
||||
|
||||
kHostResolveError,
|
||||
kEndpointConnectError,
|
||||
kHandshakeError,
|
||||
kHandshakeError, // HTTPS handshake
|
||||
kSocketReadError,
|
||||
kSocketWriteError,
|
||||
|
||||
// HTTP error.
|
||||
// E.g., failed to parse HTTP response (invalid content length, etc.).
|
||||
kHttpError,
|
||||
|
||||
// Server error.
|
||||
// E.g., HTTP status 500 + SOAP Fault element.
|
||||
kServerError,
|
||||
|
||||
// XML parsing error.
|
||||
kXmlError,
|
||||
};
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ const std::string& ToString(int status) {
|
||||
case HttpStatus::kNotFound:
|
||||
return NOT_FOUND;
|
||||
|
||||
case HttpStatus::InternalServerError:
|
||||
case HttpStatus::kInternalServerError:
|
||||
return INTERNAL_SERVER_ERROR;
|
||||
|
||||
case HttpStatus::kNotImplemented:
|
||||
|
||||
@@ -97,7 +97,7 @@ void SoapAsyncClient::ResponseHandler(SoapResponseHandler soap_response_handler,
|
||||
if (!soap_response.FromXml(http_response->content())) {
|
||||
soap_response_handler("", kXmlError, false);
|
||||
} else {
|
||||
soap_response_handler(soap_response.result_moved(), kNoError, false);
|
||||
//soap_response_handler(soap_response.result_moved(), kNoError, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+26
-5
@@ -3,6 +3,8 @@
|
||||
#include <cassert>
|
||||
#include <utility> // for move()
|
||||
|
||||
#include "boost/algorithm/string.hpp"
|
||||
|
||||
#include "webcc/soap_request.h"
|
||||
#include "webcc/soap_response.h"
|
||||
|
||||
@@ -24,12 +26,12 @@ SoapClient::SoapClient(const std::string& host, const std::string& port,
|
||||
|
||||
bool SoapClient::Request(const std::string& operation,
|
||||
std::vector<SoapParameter>&& parameters,
|
||||
std::string* result) {
|
||||
SoapResponse::Parser parser) {
|
||||
assert(service_ns_.IsValid());
|
||||
assert(!url_.empty() && !host_.empty());
|
||||
assert(!result_name_.empty());
|
||||
|
||||
error_ = kNoError;
|
||||
fault_.reset();
|
||||
|
||||
SoapRequest soap_request;
|
||||
|
||||
@@ -73,16 +75,35 @@ bool SoapClient::Request(const std::string& operation,
|
||||
}
|
||||
|
||||
SoapResponse soap_response;
|
||||
soap_response.set_result_name(result_name_);
|
||||
soap_response.set_operation(operation);
|
||||
soap_response.set_parser(parser);
|
||||
|
||||
if (!soap_response.FromXml(http_client_.response()->content())) {
|
||||
if (soap_response.fault()) {
|
||||
fault_ = soap_response.fault();
|
||||
error_ = kServerError;
|
||||
} else {
|
||||
error_ = kXmlError;
|
||||
return false;
|
||||
}
|
||||
|
||||
*result = soap_response.result_moved();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SoapClient::Request(const std::string& operation,
|
||||
std::vector<SoapParameter>&& parameters,
|
||||
const std::string& result_name,
|
||||
std::string* result) {
|
||||
auto parser = [result, &result_name](pugi::xml_node xnode) {
|
||||
if (boost::iequals(soap_xml::GetNameNoPrefix(xnode), result_name)) {
|
||||
soap_xml::GetText(xnode, result);
|
||||
}
|
||||
return false; // Stop next call.
|
||||
};
|
||||
|
||||
return Request(operation, std::move(parameters), parser);
|
||||
}
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
+19
-8
@@ -7,6 +7,7 @@
|
||||
#include "webcc/http_client.h"
|
||||
#include "webcc/soap_globals.h"
|
||||
#include "webcc/soap_parameter.h"
|
||||
#include "webcc/soap_response.h"
|
||||
|
||||
namespace webcc {
|
||||
|
||||
@@ -31,10 +32,6 @@ class SoapClient {
|
||||
service_ns_ = service_ns;
|
||||
}
|
||||
|
||||
void set_result_name(const std::string& result_name) {
|
||||
result_name_ = result_name;
|
||||
}
|
||||
|
||||
void set_format_raw(bool format_raw) {
|
||||
format_raw_ = format_raw;
|
||||
}
|
||||
@@ -45,14 +42,30 @@ class SoapClient {
|
||||
|
||||
bool Request(const std::string& operation,
|
||||
std::vector<SoapParameter>&& parameters,
|
||||
SoapResponse::Parser parser);
|
||||
|
||||
// Shortcut for responses with single result node.
|
||||
// The name of the single result node is specified by |result_name|.
|
||||
// The text of the result node will be set to |result|.
|
||||
bool Request(const std::string& operation,
|
||||
std::vector<SoapParameter>&& parameters,
|
||||
const std::string& result_name,
|
||||
std::string* result);
|
||||
|
||||
// HTTP status code (200, 500, etc.) in the response.
|
||||
int http_status() const {
|
||||
assert(http_client_.response());
|
||||
return http_client_.response()->status();
|
||||
}
|
||||
|
||||
bool timed_out() const {
|
||||
return http_client_.timed_out();
|
||||
}
|
||||
|
||||
Error error() const { return error_; }
|
||||
|
||||
std::shared_ptr<SoapFault> fault() const { return fault_; }
|
||||
|
||||
private:
|
||||
std::string host_;
|
||||
std::string port_; // Leave this empty to use default 80.
|
||||
@@ -65,10 +78,6 @@ class SoapClient {
|
||||
// Namespace for your web service.
|
||||
SoapNamespace service_ns_;
|
||||
|
||||
// Response result XML node name.
|
||||
// E.g., "Result".
|
||||
std::string result_name_;
|
||||
|
||||
// Format request XML without any indentation or line breaks.
|
||||
bool format_raw_;
|
||||
|
||||
@@ -79,6 +88,8 @@ class SoapClient {
|
||||
HttpClient http_client_;
|
||||
|
||||
Error error_;
|
||||
|
||||
std::shared_ptr<SoapFault> fault_;
|
||||
};
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "webcc/soap_globals.h"
|
||||
|
||||
#include <ostream>
|
||||
|
||||
namespace webcc {
|
||||
|
||||
const std::string kSoapAction = "SOAPAction";
|
||||
@@ -30,4 +32,13 @@ const SoapNamespace kSoapEnvNamespaceV12{
|
||||
"http://www.w3.org/2003/05/soap-envelope"
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const SoapFault& fault) {
|
||||
os << "Fault: {" << std::endl
|
||||
<< " faultcode: " << fault.faultcode << std::endl
|
||||
<< " faultstring: " << fault.faultstring << std::endl
|
||||
<< " detail: " << fault.detail << std::endl
|
||||
<< "}";
|
||||
return os;
|
||||
}
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef WEBCC_SOAP_GLOBALS_H_
|
||||
#define WEBCC_SOAP_GLOBALS_H_
|
||||
|
||||
#include <iosfwd>
|
||||
#include <string>
|
||||
|
||||
namespace webcc {
|
||||
@@ -35,6 +36,14 @@ struct SoapNamespace {
|
||||
extern const SoapNamespace kSoapEnvNamespaceV11;
|
||||
extern const SoapNamespace kSoapEnvNamespaceV12;
|
||||
|
||||
struct SoapFault {
|
||||
std::string faultcode;
|
||||
std::string faultstring;
|
||||
std::string detail;
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const SoapFault& fault);
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
#endif // WEBCC_SOAP_GLOBALS_H_
|
||||
|
||||
@@ -31,11 +31,11 @@ class SoapMessage {
|
||||
operation_ = operation;
|
||||
}
|
||||
|
||||
// Convert to SOAP request XML.
|
||||
// Convert to SOAP XML.
|
||||
void ToXml(bool format_raw, const std::string& indent,
|
||||
std::string* xml_string);
|
||||
|
||||
// Parse from SOAP request XML.
|
||||
// Parse from SOAP XML.
|
||||
bool FromXml(const std::string& xml_string);
|
||||
|
||||
protected:
|
||||
|
||||
+47
-11
@@ -7,6 +7,8 @@
|
||||
namespace webcc {
|
||||
|
||||
void SoapResponse::ToXmlBody(pugi::xml_node xbody) {
|
||||
assert(!result_name_.empty());
|
||||
|
||||
pugi::xml_node xop = soap_xml::AddChild(xbody, service_ns_.name,
|
||||
operation_ + "Response");
|
||||
soap_xml::AddNSAttr(xop, service_ns_.name, service_ns_.url);
|
||||
@@ -15,28 +17,62 @@ void SoapResponse::ToXmlBody(pugi::xml_node xbody) {
|
||||
result_name_);
|
||||
|
||||
// xresult.text().set() also works for PCDATA.
|
||||
// TODO: Add SetText() to soap_xml.h|cpp.
|
||||
xresult.append_child(is_cdata_ ? pugi::node_cdata : pugi::node_pcdata)
|
||||
.set_value(result_.c_str());
|
||||
}
|
||||
|
||||
bool SoapResponse::FromXmlBody(pugi::xml_node xbody) {
|
||||
assert(!result_name_.empty());
|
||||
// Check Fault element.
|
||||
|
||||
pugi::xml_node xresponse = xbody.first_child();
|
||||
if (xresponse) {
|
||||
soap_xml::SplitName(xresponse, &service_ns_.name, nullptr);
|
||||
service_ns_.url = soap_xml::GetNSAttr(xresponse, service_ns_.name);
|
||||
pugi::xml_node xfault = soap_xml::GetChildNoNS(xbody, "Fault");
|
||||
|
||||
pugi::xml_node xresult = soap_xml::GetChildNoNS(xresponse, result_name_);
|
||||
if (xresult) {
|
||||
// The value of the first child node of type PCDATA/CDATA.
|
||||
// xresult.text().get/as_string() also works.
|
||||
result_ = xresult.child_value();
|
||||
return true;
|
||||
// TODO: service_ns_.url
|
||||
|
||||
if (xfault) {
|
||||
fault_.reset(new SoapFault);
|
||||
|
||||
pugi::xml_node xfaultcode = soap_xml::GetChildNoNS(xfault, "faultcode");
|
||||
pugi::xml_node xfaultstring = soap_xml::GetChildNoNS(xfault, "faultstring");
|
||||
pugi::xml_node xdetail = soap_xml::GetChildNoNS(xfault, "detail");
|
||||
|
||||
if (xfaultcode) {
|
||||
fault_->faultcode = xfaultcode.text().as_string();
|
||||
}
|
||||
if (xfaultstring) {
|
||||
fault_->faultstring = xfaultstring.text().as_string();
|
||||
}
|
||||
if (xdetail) {
|
||||
fault_->detail = xdetail.text().as_string();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check Response element.
|
||||
|
||||
pugi::xml_node xresponse = soap_xml::GetChildNoNS(xbody,
|
||||
operation_ + "Response");
|
||||
|
||||
if (!xresponse) {
|
||||
return false;
|
||||
}
|
||||
|
||||
soap_xml::SplitName(xresponse, &service_ns_.name, nullptr);
|
||||
service_ns_.url = soap_xml::GetNSAttr(xresponse, service_ns_.name);
|
||||
|
||||
// Call result parser on each child of the response node.
|
||||
if (parser_) {
|
||||
pugi::xml_node xchild = xresponse.first_child();
|
||||
while (xchild) {
|
||||
if (!parser_(xchild)) {
|
||||
break;
|
||||
}
|
||||
xchild = xchild.next_sibling();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace webcc
|
||||
|
||||
+45
-7
@@ -1,10 +1,13 @@
|
||||
#ifndef WEBCC_SOAP_RESPONSE_H_
|
||||
#define WEBCC_SOAP_RESPONSE_H_
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility> // for move()
|
||||
|
||||
#include "webcc/soap_message.h"
|
||||
#include "webcc/soap_xml.h"
|
||||
|
||||
namespace webcc {
|
||||
|
||||
@@ -12,6 +15,39 @@ class SoapResponse : public SoapMessage {
|
||||
public:
|
||||
SoapResponse() : is_cdata_(false) {}
|
||||
|
||||
// Response result parser.
|
||||
// Called on each child of the response node.
|
||||
// Example:
|
||||
// - SOAP action: xxx
|
||||
// - Given SOAP response:
|
||||
// <soap:Body>
|
||||
// <n:xxxResponse xmlns:n="...">
|
||||
// <n:aaa>Blaah</aaa>
|
||||
// <n:bbb>Blaah</bbb>
|
||||
// <n:ccc>Blaah</ccc>
|
||||
// </n:xxxResponse>
|
||||
// <soap:Body>
|
||||
// The parser will be called in the following sequence:
|
||||
// - parser(xaaa); // return true
|
||||
// - parser(xbbb); // return true
|
||||
// - parser(xccc);
|
||||
// If any of the parser returns false, the call sequence will be stopped:
|
||||
// - parser(xaaa); // return false
|
||||
// <stopped>
|
||||
// Then you can get the expected result by parsing the node one by one.
|
||||
// When you implement the parser, you normally need to check the node name,
|
||||
// the following helper can extract the name without any namespace prefix:
|
||||
// webcc::soap_xml::GetNameNoPrefix(xnode);
|
||||
typedef std::function<bool(pugi::xml_node)> Parser;
|
||||
|
||||
void set_parser(Parser parser) {
|
||||
parser_ = parser;
|
||||
}
|
||||
|
||||
std::shared_ptr<SoapFault> fault() const {
|
||||
return fault_;
|
||||
}
|
||||
|
||||
// Could be "Price" for an operation/method like "GetXyzPrice".
|
||||
// Really depend on the service.
|
||||
// Most services use a general name "Result".
|
||||
@@ -19,35 +55,37 @@ class SoapResponse : public SoapMessage {
|
||||
result_name_ = result_name;
|
||||
}
|
||||
|
||||
// Server only.
|
||||
void set_result(const std::string& result, bool is_cdata) {
|
||||
result_ = result;
|
||||
is_cdata_ = is_cdata;
|
||||
}
|
||||
|
||||
// Server only.
|
||||
void set_result_moved(std::string&& result, bool is_cdata) {
|
||||
result_ = std::move(result);
|
||||
is_cdata_ = is_cdata;
|
||||
}
|
||||
|
||||
std::string result_moved() {
|
||||
return std::move(result_);
|
||||
}
|
||||
|
||||
protected:
|
||||
void ToXmlBody(pugi::xml_node xbody) override;
|
||||
|
||||
bool FromXmlBody(pugi::xml_node xbody) override;
|
||||
|
||||
private:
|
||||
// NOTE:
|
||||
// Multiple results might be necessary. But for most cases, single result
|
||||
// should be enough, because an API normally returns only one value.
|
||||
// Fault element if any.
|
||||
std::shared_ptr<SoapFault> fault_;
|
||||
|
||||
// Response result parser.
|
||||
Parser parser_;
|
||||
|
||||
// Result XML node name.
|
||||
// Used to parse the response XML from client side.
|
||||
// TODO
|
||||
std::string result_name_;
|
||||
|
||||
// Result value.
|
||||
// TODO
|
||||
std::string result_;
|
||||
|
||||
// CDATA result.
|
||||
|
||||
+15
-2
@@ -7,7 +7,7 @@ void SplitName(const pugi::xml_node& xnode, std::string* prefix,
|
||||
std::string* name) {
|
||||
std::string full_name = xnode.name();
|
||||
|
||||
size_t pos = full_name.find(':');
|
||||
std::size_t pos = full_name.find(':');
|
||||
|
||||
if (pos != std::string::npos) {
|
||||
if (prefix != nullptr) {
|
||||
@@ -38,6 +38,19 @@ std::string GetNameNoPrefix(const pugi::xml_node& xnode) {
|
||||
return name;
|
||||
}
|
||||
|
||||
// NOTE:
|
||||
// The following 3 ways all work for PCDATA and CDATA:
|
||||
// - xnode.text().get()
|
||||
// - xnode.text().as_string()
|
||||
// - xnode.child_value()
|
||||
std::string GetText(const pugi::xml_node& xnode) {
|
||||
return xnode.child_value();
|
||||
}
|
||||
|
||||
void GetText(const pugi::xml_node& xnode, std::string* text) {
|
||||
*text = xnode.child_value();
|
||||
}
|
||||
|
||||
pugi::xml_node AddChild(pugi::xml_node xnode,
|
||||
const std::string& ns, const std::string& name) {
|
||||
return xnode.append_child((ns + ":" + name).c_str());
|
||||
@@ -55,7 +68,7 @@ pugi::xml_node GetChildNoNS(const pugi::xml_node& xnode,
|
||||
std::string child_name = xchild.name();
|
||||
|
||||
// Remove NS prefix.
|
||||
size_t pos = child_name.find(':');
|
||||
std::size_t pos = child_name.find(':');
|
||||
if (pos != std::string::npos) {
|
||||
child_name = child_name.substr(pos + 1);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,15 @@ std::string GetPrefix(const pugi::xml_node& xnode);
|
||||
// Get the node name without namespace prefix.
|
||||
std::string GetNameNoPrefix(const pugi::xml_node& xnode);
|
||||
|
||||
// Get node text (applicable for both PCDATA and CDATA).
|
||||
// E.g., given the following node:
|
||||
// <Name>Chunting Gu</Name>
|
||||
// GetText returns "Chunting Gu".
|
||||
std::string GetText(const pugi::xml_node& xnode);
|
||||
|
||||
// Output parameter version GetText.
|
||||
void GetText(const pugi::xml_node& xnode, std::string* text);
|
||||
|
||||
// Add a child with the given name which is prefixed by a namespace.
|
||||
// E.g., AppendChild(xnode, "soapenv", "Envelope") will append a child with
|
||||
// name "soapenv:Envelope".
|
||||
|
||||
Reference in New Issue
Block a user