diff --git a/example/rest_book_client/CMakeLists.txt b/example/rest_book_client/CMakeLists.txt index d381c00..f4416c9 100644 --- a/example/rest_book_client/CMakeLists.txt +++ b/example/rest_book_client/CMakeLists.txt @@ -6,5 +6,5 @@ file(GLOB SRCS *.cc *.h) add_executable(rest_book_client ${SRCS}) -target_link_libraries(rest_book_client webcc ${Boost_LIBRARIES}) +target_link_libraries(rest_book_client webcc jsoncpp ${Boost_LIBRARIES}) target_link_libraries(rest_book_client "${CMAKE_THREAD_LIBS_INIT}") diff --git a/example/rest_book_client/main.cc b/example/rest_book_client/main.cc index 565ce76..24b9132 100644 --- a/example/rest_book_client/main.cc +++ b/example/rest_book_client/main.cc @@ -1,87 +1,209 @@ #include +#include "boost/algorithm/string.hpp" +#include "json/json.h" // jsoncpp + #include "webcc/http_client.h" #include "webcc/http_request.h" #include "webcc/http_response.h" -class BookListClient { +//////////////////////////////////////////////////////////////////////////////// + +class BookClientBase { public: - BookListClient() { - host_ = "localhost"; - port_ = "8080"; + BookClientBase(const std::string& host, const std::string& port) + : host_(host), port_(port) { } - bool ListBooks() { + bool Request(const std::string& method, + const std::string& url, + const std::string& content, + webcc::HttpResponse* http_response) { webcc::HttpRequest http_request; - http_request.set_method(webcc::kHttpGet); - http_request.set_url("/books"); + http_request.set_method(method); + http_request.set_url(url); http_request.SetHost(host_, port_); - + if (!content.empty()) { // TODO + http_request.SetContent(content); + } http_request.Build(); - webcc::HttpResponse http_response; - webcc::HttpClient http_client; - webcc::Error error = http_client.SendRequest(http_request, &http_response); - - if (error != webcc::kNoError) { - return false; - } - - std::cout << "Book list: " << std::endl - << http_response.content() << std::endl; + webcc::Error error = http_client.SendRequest(http_request, http_response); - return true; + return error == webcc::kNoError; } -private: +protected: std::string host_; std::string port_; }; -class BookDetailClient { +//////////////////////////////////////////////////////////////////////////////// + +class BookListClient : public BookClientBase { public: - BookDetailClient() { - host_ = "localhost"; - port_ = "8080"; + BookListClient(const std::string& host, const std::string& port) + : BookClientBase(host, port) { } - bool GetBook(const std::string& id) { - webcc::HttpRequest http_request; + bool ListBooks() { + webcc::HttpResponse http_response; + if (!Request(webcc::kHttpGet, "/books", "", &http_response)) { + return false; + } - http_request.set_method(webcc::kHttpGet); - http_request.set_url("/books/" + id); - http_request.SetHost(host_, port_); + std::cout << http_response.content() << std::endl; - http_request.Build(); + return true; + } - webcc::HttpResponse http_response; + bool CreateBook(const std::string& id, + const std::string& title, + double price) { + Json::Value root(Json::objectValue); + root["id"] = id; + root["title"] = title; + root["price"] = price; - webcc::HttpClient http_client; - webcc::Error error = http_client.SendRequest(http_request, &http_response); + Json::StreamWriterBuilder builder; + std::string book_json = Json::writeString(builder, root); - if (error != webcc::kNoError) { + webcc::HttpResponse http_response; + if (!Request(webcc::kHttpPost, "/books", book_json, &http_response)) { return false; } - std::cout << "Book: " << id << std::endl - << http_response.content() << std::endl; + std::cout << http_response.status() << std::endl; return true; } +}; -private: - std::string host_; - std::string port_; +//////////////////////////////////////////////////////////////////////////////// + +class BookDetailClient : public BookClientBase { +public: + BookDetailClient(const std::string& host, const std::string& port) + : BookClientBase(host, port) { + } + + bool GetBook(const std::string& id) { + webcc::HttpResponse http_response; + if (!Request(webcc::kHttpGet, "/books/" + id, "", &http_response)) { + return false; + } + + std::cout << http_response.content() << std::endl; + + return true; + } }; -int main() { - BookListClient book_list_client; - book_list_client.ListBooks(); +//////////////////////////////////////////////////////////////////////////////// + +std::string g_host; +std::string g_port; + +void ListBooks() { + std::cout << "List books" << std::endl; + + BookListClient client(g_host, g_port); + + client.ListBooks(); +} + +void CreateBook(const std::string& id, + const std::string& title, + double price) { + std::cout << "Create book: " << id << " " << title << " " << price << std::endl; + + BookListClient client(g_host, g_port); + + client.CreateBook(id, title, price); +} + +void Help(const char* argv0) { + std::cout << "Usage: " << argv0 << " " << std::endl; + std::cout << " E.g.," << std::endl; + std::cout << " " << argv0 << " localhost 8080" << std::endl; +} + +std::string GetUserInput() { + char input[256]; + std::size_t length = 0; + do { + std::cout << ">> "; + std::cin.getline(input, 256); + length = strlen(input); + } while (length == 0); + + return input; +} + +int main(int argc, char* argv[]) { + if (argc != 3) { + Help(argv[0]); + return 1; + } + + g_host = argv[1]; + g_port = argv[2]; + + // Type commands to execute actions. + // Commands: list, create, update, remove + // Examples: + // >> list + // >> create 1 { "title": "1984", "price": 12.3 } + // >> detail 1 + // >> update 1 { "price": 32 } + // >> delete 1 + + while (true) { + std::string input = GetUserInput(); + boost::trim(input); + + std::string command; + std::size_t i = input.find(' '); + if (i == std::string::npos) { + command = input; + } else { + command = input.substr(0, i); + } + + if (command == "exit") { + break; + } + + if (command == "list") { + ListBooks(); + continue; + } + + ++i; + + std::size_t j = input.find(' ', i); + std::string id = input.substr(i, j - i); + i = j + 1; + + if (command == "create") { + std::string json = input.substr(i); + std::cout << "json:" << json << std::endl; + + Json::Value root; + Json::CharReaderBuilder builder; + std::string errs; + if (Json::parseFromStream(builder, std::stringstream(json), &root, &errs)) { + CreateBook(id, root["title"].asString(), root["price"].asDouble()); + } else { + std::cerr << errs << std::endl; + } + } + } - BookDetailClient book_detail_client; - book_detail_client.GetBook("1"); + //BookDetailClient book_detail_client(host, port); + //book_detail_client.GetBook("1"); return 0; } diff --git a/example/rest_book_server/book_services.cc b/example/rest_book_server/book_services.cc index cfb2a85..ac77c51 100644 --- a/example/rest_book_server/book_services.cc +++ b/example/rest_book_server/book_services.cc @@ -1,6 +1,7 @@ #include "book_services.h" #include +#include #include "boost/lexical_cast.hpp" #include "json/json.h" // jsoncpp @@ -28,16 +29,16 @@ public: } }; +std::ostream& operator<<(std::ostream& os, const Book& book) { + os << "{ " << book.id << ", " << book.title << ", " << book.price << " }"; + return os; +} + static const Book kNullBook{}; class BookStore { public: - BookStore() { - // Prepare test data. - books_.push_back({ "1", "Title1", 11.1 }); - books_.push_back({ "2", "Title2", 22.2 }); - books_.push_back({ "3", "Title3", 33.3 }); - } + BookStore() = default; const std::list& books() const { return books_; @@ -87,16 +88,39 @@ bool BookListService::Handle(const std::string& http_method, const std::string& request_content, std::string* response_content) { if (http_method == webcc::kHttpGet) { + // Return all books as a JSON array. + Json::Value root(Json::arrayValue); for (const Book& book : g_book_store.books()) { root.append(book.ToJson()); } - Json::StreamWriterBuilder wbuilder; - *response_content = Json::writeString(wbuilder, root); + Json::StreamWriterBuilder builder; + *response_content = Json::writeString(builder, root); + return true; } + if (http_method == webcc::kHttpPost) { + // Add a new book. + + Json::Value root; + Json::CharReaderBuilder builder; + std::stringstream stream(request_content); + std::string errs; + if (!Json::parseFromStream(builder, stream, &root, &errs)) { + std::cerr << errs << std::endl; + return false; + } + + Book book; + book.id = root["id"].asString(); + book.title = root["title"].asString(); + book.price = root["price"].asDouble(); + + return g_book_store.AddBook(book); + } + return false; } @@ -119,8 +143,8 @@ bool BookDetailService::Handle(const std::string& http_method, return false; } - Json::StreamWriterBuilder wbuilder; - *response_content = Json::writeString(wbuilder, book.ToJson()); + Json::StreamWriterBuilder builder; + *response_content = Json::writeString(builder, book.ToJson()); return true; diff --git a/src/webcc/http_message.h b/src/webcc/http_message.h index 5056ee5..db4e315 100644 --- a/src/webcc/http_message.h +++ b/src/webcc/http_message.h @@ -43,30 +43,20 @@ public: SetHeader(kContentType, content_type); } - void SetContentLength(std::size_t content_length) { - content_length_ = content_length; - SetHeader(kContentLength, std::to_string(content_length)); - } - - // Use move semantics to avoid copy. - void set_content(std::string&& content) { + void SetContent(std::string&& content) { content_ = std::move(content); + SetContentLength(content_.size()); } - void AppendContent(const char* data, std::size_t count) { - content_.append(data, count); - } - - void AppendContent(const std::string& data) { - content_.append(data); - } - - bool IsContentFull() const { - return IsContentLengthValid() && content_.length() >= content_length_; + void SetContent(const std::string& content) { + content_ = content; + SetContentLength(content_.size()); } - bool IsContentLengthValid() const { - return content_length_ != kInvalidLength; +private: + void SetContentLength(std::size_t content_length) { + content_length_ = content_length; + SetHeader(kContentLength, std::to_string(content_length)); } protected: diff --git a/src/webcc/http_parser.cc b/src/webcc/http_parser.cc index 8e62c0a..caa3538 100644 --- a/src/webcc/http_parser.cc +++ b/src/webcc/http_parser.cc @@ -9,6 +9,7 @@ namespace webcc { HttpParser::HttpParser(HttpMessage* message) : message_(message) + , content_length_(kInvalidLength) , start_line_parsed_(false) , content_length_parsed_(false) , header_parsed_(false) @@ -18,11 +19,11 @@ HttpParser::HttpParser(HttpMessage* message) Error HttpParser::Parse(const char* data, std::size_t len) { if (header_parsed_) { // Add the data to the content. - message_->AppendContent(data, len); + AppendContent(data, len); - if (message_->IsContentFull()) { + if (IsContentFull()) { // All content has been read. - finished_ = true; + Finish(); } return kNoError; @@ -66,22 +67,21 @@ Error HttpParser::Parse(const char* data, std::size_t len) { // Headers just ended. if (!content_length_parsed_) { - // No Content-Length, no content. - message_->SetContentLength(0); - finished_ = true; + // No Content-Length, no content. (TODO: Support chucked data) + Finish(); return kNoError; } else { // Invalid Content-Length in the request. - if (!message_->IsContentLengthValid()) { + if (content_length_ == kInvalidLength) { return kHttpContentLengthError; } } - message_->AppendContent(pending_data_.substr(off)); + AppendContent(pending_data_.substr(off)); - if (message_->IsContentFull()) { + if (IsContentFull()) { // All content has been read. - finished_ = true; + Finish(); } } else { // Save the unparsed piece for next parsing. @@ -110,11 +110,28 @@ void HttpParser::ParseContentLength(const std::string& line) { std::string value = line.substr(pos); try { - std::size_t length = boost::lexical_cast(value); - message_->SetContentLength(length); + content_length_ = boost::lexical_cast(value); } catch (boost::bad_lexical_cast&) { } } } +void HttpParser::Finish() { + message_->SetContent(content_); + finished_ = true; +} + +void HttpParser::AppendContent(const char* data, std::size_t count) { + content_.append(data, count); +} + +void HttpParser::AppendContent(const std::string& data) { + content_.append(data); +} + +bool HttpParser::IsContentFull() const { + return content_length_ != kInvalidLength && + content_length_ <= content_.length(); +} + } // namespace webcc diff --git a/src/webcc/http_parser.h b/src/webcc/http_parser.h index 9a846c2..b70bc97 100644 --- a/src/webcc/http_parser.h +++ b/src/webcc/http_parser.h @@ -13,6 +13,10 @@ class HttpParser { public: explicit HttpParser(HttpMessage* message); + ~HttpParser() = default; + HttpParser(const HttpParser&) = delete; + HttpParser& operator=(const HttpParser&) = delete; + bool finished() const { return finished_; } @@ -25,6 +29,13 @@ protected: void ParseContentLength(const std::string& line); + void Finish(); + + void AppendContent(const char* data, std::size_t count); + void AppendContent(const std::string& data); + + bool IsContentFull() const; + protected: // The result HTTP message. HttpMessage* message_; @@ -34,7 +45,9 @@ protected: // Data waiting to be parsed. std::string pending_data_; - // Parsing helper flags. + // Temporary data and helper flags for parsing. + std::size_t content_length_; + std::string content_; bool start_line_parsed_; bool content_length_parsed_; bool header_parsed_; diff --git a/src/webcc/http_response.cc b/src/webcc/http_response.cc index 3b33b52..71fd311 100644 --- a/src/webcc/http_response.cc +++ b/src/webcc/http_response.cc @@ -96,7 +96,7 @@ std::vector HttpResponse::ToBuffers() const { buffers.push_back(boost::asio::buffer(misc_strings::CRLF)); // Content (optional) - if (IsContentLengthValid()) { + if (!content_.empty()) { buffers.push_back(boost::asio::buffer(content_)); } diff --git a/src/webcc/http_session.cc b/src/webcc/http_session.cc index 7756d8e..7be746c 100644 --- a/src/webcc/http_session.cc +++ b/src/webcc/http_session.cc @@ -35,8 +35,7 @@ void HttpSession::SetResponseContent(const std::string& content_type, std::size_t content_length, std::string&& content) { response_.SetContentType(content_type); - response_.SetContentLength(content.length()); - response_.set_content(std::move(content)); + response_.SetContent(std::move(content)); } void HttpSession::SendResponse() { diff --git a/src/webcc/soap_client.cc b/src/webcc/soap_client.cc index ee51def..6f45f6a 100644 --- a/src/webcc/soap_client.cc +++ b/src/webcc/soap_client.cc @@ -40,11 +40,9 @@ Error SoapClient::Call(const std::string& operation, http_request.set_method(kHttpPost); http_request.set_url(url_); http_request.SetContentType(kTextXmlUtf8); - http_request.SetContentLength(http_content.size()); + http_request.SetContent(std::move(http_content)); http_request.SetHost(host_, port_); http_request.SetHeader(kSoapAction, operation); - http_request.set_content(std::move(http_content)); - http_request.Build(); HttpResponse http_response;