diff --git a/example/soap_book_client/book_client.cc b/example/soap_book_client/book_client.cc index 8871dfa..99bfb86 100644 --- a/example/soap_book_client/book_client.cc +++ b/example/soap_book_client/book_client.cc @@ -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&& 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; } diff --git a/example/soap_book_server/book_service.cc b/example/soap_book_server/book_service.cc index 491616a..b8f7dea 100644 --- a/example/soap_book_server/book_service.cc +++ b/example/soap_book_server/book_service.cc @@ -95,7 +95,7 @@ bool BookService::CreateBook(const webcc::SoapRequest& soap_request, std::string id = g_book_store.AddBook(book); std::string response_xml = NewResultXml(0, "ok", "book", "id", - id.c_str()); + id.c_str()); soap_response->set_result_moved(std::move(response_xml), true); diff --git a/example/soap_calc_client/main.cc b/example/soap_calc_client/main.cc index 8ef54d3..2603b76 100644 --- a/example/soap_calc_client/main.cc +++ b/example/soap_calc_client/main.cc @@ -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; } diff --git a/example/soap_calc_client_parasoft/main.cc b/example/soap_calc_client_parasoft/main.cc index 2d9de0a..6c38d66 100644 --- a/example/soap_calc_client_parasoft/main.cc +++ b/example/soap_calc_client_parasoft/main.cc @@ -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; } diff --git a/webcc/globals.cc b/webcc/globals.cc index 83120df..7c5a3ef 100644 --- a/webcc/globals.cc +++ b/webcc/globals.cc @@ -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: diff --git a/webcc/globals.h b/webcc/globals.h index ef24e62..e2a2f6b 100644 --- a/webcc/globals.h +++ b/webcc/globals.h @@ -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, }; diff --git a/webcc/http_response.cc b/webcc/http_response.cc index 7ea4912..ba8bd81 100644 --- a/webcc/http_response.cc +++ b/webcc/http_response.cc @@ -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: diff --git a/webcc/soap_async_client.cc b/webcc/soap_async_client.cc index 4710ff5..a142b7c 100644 --- a/webcc/soap_async_client.cc +++ b/webcc/soap_async_client.cc @@ -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); } } } diff --git a/webcc/soap_client.cc b/webcc/soap_client.cc index f0d8d6e..8f6d13e 100644 --- a/webcc/soap_client.cc +++ b/webcc/soap_client.cc @@ -3,6 +3,8 @@ #include #include // 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&& 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())) { - error_ = kXmlError; + if (soap_response.fault()) { + fault_ = soap_response.fault(); + error_ = kServerError; + } else { + error_ = kXmlError; + } + return false; } - *result = soap_response.result_moved(); - return true; } +bool SoapClient::Request(const std::string& operation, + std::vector&& 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 diff --git a/webcc/soap_client.h b/webcc/soap_client.h index 8aec73d..61ba05c 100644 --- a/webcc/soap_client.h +++ b/webcc/soap_client.h @@ -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&& 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&& 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 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 fault_; }; } // namespace webcc diff --git a/webcc/soap_globals.cc b/webcc/soap_globals.cc index b310b89..87791e0 100644 --- a/webcc/soap_globals.cc +++ b/webcc/soap_globals.cc @@ -1,5 +1,7 @@ #include "webcc/soap_globals.h" +#include + 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 diff --git a/webcc/soap_globals.h b/webcc/soap_globals.h index fc5e4a3..a0be23a 100644 --- a/webcc/soap_globals.h +++ b/webcc/soap_globals.h @@ -1,6 +1,7 @@ #ifndef WEBCC_SOAP_GLOBALS_H_ #define WEBCC_SOAP_GLOBALS_H_ +#include #include 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_ diff --git a/webcc/soap_message.h b/webcc/soap_message.h index a2fc235..360240f 100644 --- a/webcc/soap_message.h +++ b/webcc/soap_message.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: diff --git a/webcc/soap_response.cc b/webcc/soap_response.cc index 108e5df..fb0dece 100644 --- a/webcc/soap_response.cc +++ b/webcc/soap_response.cc @@ -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 xfault = soap_xml::GetChildNoNS(xbody, "Fault"); + + // 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); - 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 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; + // 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 false; + return true; } } // namespace webcc diff --git a/webcc/soap_response.h b/webcc/soap_response.h index ff61f29..07b8ad1 100644 --- a/webcc/soap_response.h +++ b/webcc/soap_response.h @@ -1,10 +1,13 @@ #ifndef WEBCC_SOAP_RESPONSE_H_ #define WEBCC_SOAP_RESPONSE_H_ +#include +#include #include #include // 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: + // + // + // Blaah + // Blaah + // Blaah + // + // + // 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 + // + // 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 Parser; + + void set_parser(Parser parser) { + parser_ = parser; + } + + std::shared_ptr 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 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. diff --git a/webcc/soap_xml.cc b/webcc/soap_xml.cc index fb5afc3..d1a940b 100644 --- a/webcc/soap_xml.cc +++ b/webcc/soap_xml.cc @@ -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); } diff --git a/webcc/soap_xml.h b/webcc/soap_xml.h index 13b8537..3118ed0 100644 --- a/webcc/soap_xml.h +++ b/webcc/soap_xml.h @@ -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: +// Chunting Gu +// 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".