Mercurial > hg > orthanc
changeset 207:7f74209ea0f8
RestApi
author | Sebastien Jodogne <s.jodogne@gmail.com> |
---|---|
date | Wed, 28 Nov 2012 16:23:11 +0100 |
parents | 4453a010d0db |
children | de640de989b8 |
files | Core/HttpServer/HttpHandler.cpp Core/HttpServer/HttpHandler.h Core/HttpServer/HttpOutput.cpp Core/HttpServer/HttpOutput.h Core/HttpServer/MongooseServer.cpp Core/Toolbox.cpp NEWS OrthancServer/OrthancRestApi.cpp OrthancServer/OrthancRestApi.h UnitTests/ServerIndex.cpp UnitTests/main.cpp |
diffstat | 11 files changed, 784 insertions(+), 18 deletions(-) [+] |
line wrap: on
line diff
--- a/Core/HttpServer/HttpHandler.cpp Wed Nov 28 12:03:18 2012 +0100 +++ b/Core/HttpServer/HttpHandler.cpp Wed Nov 28 16:23:11 2012 +0100 @@ -76,12 +76,12 @@ - std::string HttpHandler::GetArgument(const Arguments& arguments, + std::string HttpHandler::GetArgument(const Arguments& getArguments, const std::string& name, const std::string& defaultValue) { - Arguments::const_iterator it = arguments.find(name); - if (it == arguments.end()) + Arguments::const_iterator it = getArguments.find(name); + if (it == getArguments.end()) { return defaultValue; }
--- a/Core/HttpServer/HttpHandler.h Wed Nov 28 12:03:18 2012 +0100 +++ b/Core/HttpServer/HttpHandler.h Wed Nov 28 16:23:11 2012 +0100 @@ -55,13 +55,13 @@ const std::string& method, const UriComponents& uri, const Arguments& headers, - const Arguments& arguments, + const Arguments& getArguments, const std::string& postData) = 0; static void ParseGetQuery(HttpHandler::Arguments& result, const char* query); - static std::string GetArgument(const Arguments& arguments, + static std::string GetArgument(const Arguments& getArguments, const std::string& name, const std::string& defaultValue); };
--- a/Core/HttpServer/HttpOutput.cpp Wed Nov 28 12:03:18 2012 +0100 +++ b/Core/HttpServer/HttpOutput.cpp Wed Nov 28 16:23:11 2012 +0100 @@ -80,6 +80,13 @@ } + void HttpOutput::SendCustomOkHeader(const std::string& customHeader) + { + std::string s = "HTTP/1.1 200 OK\r\n" + customHeader + "\r\n"; + Send(&s[0], s.size()); + } + + void HttpOutput::SendMethodNotAllowedError(const std::string& allowed) { std::string s =
--- a/Core/HttpServer/HttpOutput.h Wed Nov 28 12:03:18 2012 +0100 +++ b/Core/HttpServer/HttpOutput.h Wed Nov 28 16:23:11 2012 +0100 @@ -44,11 +44,6 @@ private: void SendHeaderInternal(Orthanc_HttpStatus status); - void SendOkHeader(const char* contentType, - bool hasContentLength, - uint64_t contentLength, - const char* contentFilename); - public: virtual ~HttpOutput() { @@ -58,6 +53,13 @@ void SendOkHeader(const std::string& contentType); + void SendOkHeader(const char* contentType, + bool hasContentLength, + uint64_t contentLength, + const char* contentFilename); + + void SendCustomOkHeader(const std::string& customHeader); + void SendString(const std::string& s); void SendMethodNotAllowedError(const std::string& allowed);
--- a/Core/HttpServer/MongooseServer.cpp Wed Nov 28 12:03:18 2012 +0100 +++ b/Core/HttpServer/MongooseServer.cpp Wed Nov 28 16:23:11 2012 +0100 @@ -489,7 +489,8 @@ { HttpHandler::ParseGetQuery(arguments, request->query_string); } - else if (!strcmp(request->request_method, "POST")) + else if (!strcmp(request->request_method, "POST") || + !strcmp(request->request_method, "PUT")) { HttpHandler::Arguments::const_iterator ct = headers.find("content-type"); if (ct == headers.end())
--- a/Core/Toolbox.cpp Wed Nov 28 12:03:18 2012 +0100 +++ b/Core/Toolbox.cpp Wed Nov 28 16:23:11 2012 +0100 @@ -288,6 +288,15 @@ { components.push_back(std::string(&uri[start], end - start)); } + + for (size_t i = 0; i < components.size(); i++) + { + if (components[i].size() == 0) + { + // Empty component, as in: "/coucou//e" + throw OrthancException(ErrorCode_UriSyntax); + } + } }
--- a/NEWS Wed Nov 28 12:03:18 2012 +0100 +++ b/NEWS Wed Nov 28 16:23:11 2012 +0100 @@ -11,8 +11,7 @@ ------------- * "CompletedSeries" event in the changes API -* Flush to disk thread - +* Thread to continuously flush DB to disk (robustness against reboots) Version 0.2.3 (2012/10/26)
--- a/OrthancServer/OrthancRestApi.cpp Wed Nov 28 12:03:18 2012 +0100 +++ b/OrthancServer/OrthancRestApi.cpp Wed Nov 28 16:23:11 2012 +0100 @@ -397,7 +397,7 @@ const std::string& method, const UriComponents& uri, const Arguments& headers, - const Arguments& arguments, + const Arguments& getArguments, const std::string& postData) { if (uri.size() == 0) @@ -718,13 +718,13 @@ { const static unsigned int MAX_RESULTS = 100; - //std::string filter = GetArgument(arguments, "filter", ""); + //std::string filter = GetArgument(getArguments, "filter", ""); int64_t since; unsigned int limit; try { - since = boost::lexical_cast<int64_t>(GetArgument(arguments, "since", "0")); - limit = boost::lexical_cast<unsigned int>(GetArgument(arguments, "limit", "0")); + since = boost::lexical_cast<int64_t>(GetArgument(getArguments, "since", "0")); + limit = boost::lexical_cast<unsigned int>(GetArgument(getArguments, "limit", "0")); } catch (boost::bad_lexical_cast) {
--- a/OrthancServer/OrthancRestApi.h Wed Nov 28 12:03:18 2012 +0100 +++ b/OrthancServer/OrthancRestApi.h Wed Nov 28 16:23:11 2012 +0100 @@ -93,7 +93,7 @@ const std::string& method, const UriComponents& uri, const Arguments& headers, - const Arguments& arguments, + const Arguments& getArguments, const std::string& postData); }; }
--- a/UnitTests/ServerIndex.cpp Wed Nov 28 12:03:18 2012 +0100 +++ b/UnitTests/ServerIndex.cpp Wed Nov 28 16:23:11 2012 +0100 @@ -258,3 +258,750 @@ index.DeleteResource(a[6]); ASSERT_EQ("", listener.ancestorId_); // No more ancestor } + + + +#include "../Core/Toolbox.h" +#include "../Core/HttpServer/HttpOutput.h" +#include "../Core/HttpServer/HttpHandler.h" + +namespace Orthanc +{ + class RestApiPath + { + private: + UriComponents uri_; + bool hasTrailing_; + std::vector<std::string> components_; + + public: + typedef std::map<std::string, std::string> Components; + + RestApiPath(const std::string& uri) + { + Toolbox::SplitUriComponents(uri_, uri); + + if (uri_.size() == 0) + { + return; + } + + if (uri_.back() == "*") + { + hasTrailing_ = true; + uri_.pop_back(); + } + else + { + hasTrailing_ = false; + } + + components_.resize(uri_.size()); + for (size_t i = 0; i < uri_.size(); i++) + { + size_t s = uri_[i].size(); + assert(s > 0); + + if (uri_[i][0] == '{' && + uri_[i][s - 1] == '}') + { + components_[i] = uri_[i].substr(1, s - 2); + uri_[i] = ""; + } + else + { + components_[i] = ""; + } + } + } + + // This version is slower + bool Match(Components& components, + UriComponents& trailing, + const std::string& uriRaw) const + { + UriComponents uri; + Toolbox::SplitUriComponents(uri, uriRaw); + return Match(components, trailing, uri); + } + + bool Match(Components& components, + UriComponents& trailing, + const UriComponents& uri) const + { + if (uri.size() < uri_.size()) + { + return false; + } + + if (!hasTrailing_ && uri.size() > uri_.size()) + { + return false; + } + + components.clear(); + trailing.clear(); + + assert(uri_.size() <= uri.size()); + for (size_t i = 0; i < uri_.size(); i++) + { + if (components_[i].size() == 0) + { + // This URI component is not a free parameter + if (uri_[i] != uri[i]) + { + return false; + } + } + else + { + // This URI component is a free parameter + components[components_[i]] = uri[i]; + } + } + + if (hasTrailing_) + { + trailing.assign(uri.begin() + uri_.size(), uri.end()); + } + + return true; + } + + bool Match(const UriComponents& uri) const + { + Components components; + UriComponents trailing; + return Match(components, trailing, uri); + } + }; + + + class HttpFileSender + { + private: + std::string contentType_; + std::string filename_; + + void SendHeader(HttpOutput& output) + { + std::string header; + header += "Content-Length: " + boost::lexical_cast<std::string>(GetFileSize()) + "\r\n"; + + if (contentType_.size() > 0) + { + header += "Content-Type: " + contentType_ + "\r\n"; + } + + if (filename_.size() > 0) + { + header += "Content-Disposition: attachment; filename=\"" + filename_ + "\"\r\n"; + } + + output.SendCustomOkHeader(header); + } + + protected: + virtual uint64_t GetFileSize() = 0; + + virtual bool SendData(HttpOutput& output) = 0; + + public: + virtual ~HttpFileSender() + { + } + + void ResetContentType() + { + contentType_.clear(); + } + + void SetContentType(const std::string& contentType) + { + contentType_ = contentType; + } + + const std::string& GetContentType() const + { + return contentType_; + } + + void ResetFilename() + { + contentType_.clear(); + } + + void SetFilename(const std::string& filename) + { + filename_ = filename; + } + + const std::string& GetFilename() const + { + return filename_; + } + + void Send(HttpOutput& output) + { + SendHeader(output); + + if (!SendData(output)) + { + output.SendHeader(Orthanc_HttpStatus_500_InternalServerError); + } + } + }; + + + class FilesystemHttpSender : public HttpFileSender + { + private: + std::string path_; + + protected: + virtual uint64_t GetFileSize() + { + return Toolbox::GetFileSize(path_); + } + + virtual bool SendData(HttpOutput& output) + { + FILE* fp = fopen(path_.c_str(), "rb"); + if (!fp) + { + return false; + } + + std::vector<uint8_t> buffer(1024 * 1024); // Chunks of 1MB + + for (;;) + { + size_t nbytes = fread(&buffer[0], 1, buffer.size(), fp); + if (nbytes == 0) + { + break; + } + else + { + output.Send(&buffer[0], nbytes); + } + } + + fclose(fp); + + return true; + } + + public: + FilesystemHttpSender(const char* path) : path_(path) + { + boost::filesystem::path p(path); + SetFilename(p.filename().string()); + SetContentType(Toolbox::AutodetectMimeType(p.filename().string())); + } + }; + + + class RestApiOutput + { + private: + HttpOutput& output_; + + public: + RestApiOutput(HttpOutput& output) : output_(output) + { + } + + void AnswerFile(HttpFileSender& sender) + { + sender.Send(output_); + } + + void AnswerJson(const Json::Value& value) + { + Json::StyledWriter writer; + std::string s = writer.write(value); + output_.AnswerBufferWithContentType(s, "application/json"); + } + + void AnswerBuffer(const std::string& buffer, + const std::string& contentType) + { + output_.AnswerBufferWithContentType(buffer, contentType); + } + + void Redirect(const char* path) + { + output_.Redirect(path); + } + }; + + + class RestApiSharedCall + { + protected: + RestApiOutput* output_; + IDynamicObject* context_; + const HttpHandler::Arguments* httpHeaders_; + const RestApiPath::Components* uriComponents_; + const UriComponents* trailing_; + + public: + RestApiOutput& GetOutput() + { + return *output_; + } + + IDynamicObject* GetContext() + { + return context_; + } + + const HttpHandler::Arguments& GetHttpHeaders() const + { + return *httpHeaders_; + } + + const RestApiPath::Components& GetUriComponents() const + { + return *uriComponents_; + } + + const UriComponents& GetTrailing() const + { + return *trailing_; + } + + std::string GetUriComponent(const std::string& name, + const std::string& defaultValue) + { + return HttpHandler::GetArgument(*uriComponents_, name, defaultValue); + } + }; + + + class RestApiPutCall : public RestApiSharedCall + { + friend class RestApi; + + private: + const std::string* data_; + + public: + const std::string& GetData() + { + return *data_; + } + }; + + + class RestApiPostCall : public RestApiSharedCall + { + friend class RestApi; + + private: + const std::string* data_; + + public: + const std::string& GetData() + { + return *data_; + } + }; + + + + class RestApiDeleteCall : public RestApiSharedCall + { + friend class RestApi; + }; + + + + + class RestApiGetCall : public RestApiSharedCall + { + friend class RestApi; + + private: + const HttpHandler::Arguments* getArguments_; + + public: + std::string GetArgument(const std::string& name, + const std::string& defaultValue) + { + return HttpHandler::GetArgument(*getArguments_, name, defaultValue); + } + }; + + + + class RestApi : public HttpHandler + { + public: + typedef void (*GetHandler) (RestApiGetCall& call); + + typedef void (*DeleteHandler) (RestApiDeleteCall& call); + + typedef void (*PutHandler) (RestApiPutCall& call); + + typedef void (*PostHandler) (RestApiPostCall& call); + + private: + typedef std::list< std::pair<RestApiPath*, GetHandler> > GetHandlers; + typedef std::list< std::pair<RestApiPath*, PutHandler> > PutHandlers; + typedef std::list< std::pair<RestApiPath*, PostHandler> > PostHandlers; + typedef std::list< std::pair<RestApiPath*, DeleteHandler> > DeleteHandlers; + + // TODO MUTEX BETWEEN CONTEXTS !!! + std::auto_ptr<IDynamicObject> context_; + + GetHandlers getHandlers_; + PutHandlers putHandlers_; + PostHandlers postHandlers_; + DeleteHandlers deleteHandlers_; + + bool IsGetAccepted(const UriComponents& uri) + { + for (GetHandlers::const_iterator it = getHandlers_.begin(); + it != getHandlers_.end(); it++) + { + if (it->first->Match(uri)) + { + return true; + } + } + + return false; + } + + bool IsPutAccepted(const UriComponents& uri) + { + for (PutHandlers::const_iterator it = putHandlers_.begin(); + it != putHandlers_.end(); it++) + { + if (it->first->Match(uri)) + { + return true; + } + } + + return false; + } + + bool IsPostAccepted(const UriComponents& uri) + { + for (PostHandlers::const_iterator it = postHandlers_.begin(); + it != postHandlers_.end(); it++) + { + if (it->first->Match(uri)) + { + return true; + } + } + + return false; + } + + bool IsDeleteAccepted(const UriComponents& uri) + { + for (DeleteHandlers::const_iterator it = deleteHandlers_.begin(); + it != deleteHandlers_.end(); it++) + { + if (it->first->Match(uri)) + { + return true; + } + } + + return false; + } + + void AddMethod(std::string& target, + const std::string& method) const + { + if (target.size() > 0) + target += "," + method; + else + target = method; + } + + std::string GetAcceptedMethods(const UriComponents& uri) + { + std::string s; + + if (IsGetAccepted(uri)) + AddMethod(s, "GET"); + + if (IsPutAccepted(uri)) + AddMethod(s, "PUT"); + + if (IsPostAccepted(uri)) + AddMethod(s, "POST"); + + if (IsDeleteAccepted(uri)) + AddMethod(s, "DELETE"); + + return s; + } + + public: + RestApi() + { + } + + void SetContext(IDynamicObject* context) // This takes the ownership + { + context_.reset(context); + } + + ~RestApi() + { + for (GetHandlers::iterator it = getHandlers_.begin(); + it != getHandlers_.end(); it++) + { + delete it->first; + } + + for (PutHandlers::iterator it = putHandlers_.begin(); + it != putHandlers_.end(); it++) + { + delete it->first; + } + + for (PostHandlers::iterator it = postHandlers_.begin(); + it != postHandlers_.end(); it++) + { + delete it->first; + } + + for (DeleteHandlers::iterator it = deleteHandlers_.begin(); + it != deleteHandlers_.end(); it++) + { + delete it->first; + } + } + + virtual bool IsServedUri(const UriComponents& uri) + { + return (IsGetAccepted(uri) || + IsPutAccepted(uri) || + IsPostAccepted(uri) || + IsDeleteAccepted(uri)); + } + + virtual void Handle(HttpOutput& output, + const std::string& method, + const UriComponents& uri, + const Arguments& headers, + const Arguments& getArguments, + const std::string& postData) + { + bool ok = false; + RestApiOutput restOutput(output); + RestApiPath::Components components; + UriComponents trailing; + + if (method == "GET") + { + for (GetHandlers::const_iterator it = getHandlers_.begin(); + it != getHandlers_.end(); it++) + { + if (it->first->Match(components, trailing, uri)) + { + ok = true; + RestApiGetCall call; + call.output_ = &restOutput; + call.context_ = context_.get(); + call.httpHeaders_ = &headers; + call.uriComponents_ = &components; + call.trailing_ = &trailing; + + call.getArguments_ = &getArguments; + it->second(call); + } + } + } + else if (method == "PUT") + { + for (PutHandlers::const_iterator it = putHandlers_.begin(); + it != putHandlers_.end(); it++) + { + if (it->first->Match(components, trailing, uri)) + { + ok = true; + RestApiPutCall call; + call.output_ = &restOutput; + call.context_ = context_.get(); + call.httpHeaders_ = &headers; + call.uriComponents_ = &components; + call.trailing_ = &trailing; + + call.data_ = &postData; + it->second(call); + } + } + } + else if (method == "POST") + { + for (PostHandlers::const_iterator it = postHandlers_.begin(); + it != postHandlers_.end(); it++) + { + if (it->first->Match(components, trailing, uri)) + { + ok = true; + RestApiPostCall call; + call.output_ = &restOutput; + call.context_ = context_.get(); + call.httpHeaders_ = &headers; + call.uriComponents_ = &components; + call.trailing_ = &trailing; + + call.data_ = &postData; + it->second(call); + } + } + } + else if (method == "DELETE") + { + for (DeleteHandlers::const_iterator it = deleteHandlers_.begin(); + it != deleteHandlers_.end(); it++) + { + if (it->first->Match(components, trailing, uri)) + { + ok = true; + RestApiDeleteCall call; + call.output_ = &restOutput; + call.context_ = context_.get(); + call.httpHeaders_ = &headers; + call.uriComponents_ = &components; + call.trailing_ = &trailing; + it->second(call); + } + } + } + + if (!ok) + { + output.SendMethodNotAllowedError(GetAcceptedMethods(uri)); + } + } + + void Register(const std::string& path, + GetHandler handler) + { + getHandlers_.push_back(std::make_pair(new RestApiPath(path), handler)); + } + + + void Register(const std::string& path, + PutHandler handler) + { + putHandlers_.push_back(std::make_pair(new RestApiPath(path), handler)); + } + + + void Register(const std::string& path, + PostHandler handler) + { + postHandlers_.push_back(std::make_pair(new RestApiPath(path), handler)); + } + + + void Register(const std::string& path, + DeleteHandler handler) + { + deleteHandlers_.push_back(std::make_pair(new RestApiPath(path), handler)); + } + + }; + +} + + +TEST(RestApi, RestApiPath) +{ + RestApiPath::Components args; + UriComponents trail; + + { + RestApiPath uri("/coucou/{abc}/d/*"); + ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d/e/f/g")); + ASSERT_EQ(1u, args.size()); + ASSERT_EQ(3u, trail.size()); + ASSERT_EQ("moi", args["abc"]); + ASSERT_EQ("e", trail[0]); + ASSERT_EQ("f", trail[1]); + ASSERT_EQ("g", trail[2]); + + ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi/f")); + ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d/")); + ASSERT_FALSE(uri.Match(args, trail, "/a/moi/d")); + ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi")); + } + + { + RestApiPath uri("/coucou/{abc}/d"); + ASSERT_FALSE(uri.Match(args, trail, "/coucou/moi/d/e/f/g")); + ASSERT_TRUE(uri.Match(args, trail, "/coucou/moi/d")); + ASSERT_EQ(1u, args.size()); + ASSERT_EQ(0u, trail.size()); + ASSERT_EQ("moi", args["abc"]); + } + + { + RestApiPath uri("/*"); + ASSERT_TRUE(uri.Match(args, trail, "/a/b/c")); + ASSERT_EQ(0u, args.size()); + ASSERT_EQ(3u, trail.size()); + ASSERT_EQ("a", trail[0]); + ASSERT_EQ("b", trail[1]); + ASSERT_EQ("c", trail[2]); + } +} + + + + +#include "../Core/HttpServer/MongooseServer.h" + +struct Tutu : public IDynamicObject +{ + static void Toto(RestApiGetCall& call) + { + printf("DONE\n"); + Json::Value a = Json::objectValue; + a["Tutu"] = "Toto"; + a["Youpie"] = call.GetArgument("coucou", "nope"); + a["Toto"] = call.GetUriComponent("test", "nope"); + call.GetOutput().AnswerJson(a); + } +}; + + + +TEST(RestApi, Tutu) +{ + MongooseServer httpServer; + httpServer.SetPortNumber(8042); + httpServer.Start(); + + RestApi* api = new RestApi; + httpServer.RegisterHandler(api); + api->Register("/coucou/{test}/a/*", Tutu::Toto); + + httpServer.Start(); + /*LOG(WARNING) << "REST has started"; + Toolbox::ServerBarrier();*/ +} + + +/** + + output.AnswerBufferWithContentType(s, "application/json"); + output.AnswerFile(storage_, fileUuid, contentType, filename.c_str()); + output.Redirect("app/explorer.html"); + output.SendHeader(Orthanc_HttpStatus_415_UnsupportedMediaType); + output.SendMethodNotAllowedError("GET"); + +**/
--- a/UnitTests/main.cpp Wed Nov 28 12:03:18 2012 +0100 +++ b/UnitTests/main.cpp Wed Nov 28 16:23:11 2012 +0100 @@ -188,6 +188,7 @@ ASSERT_THROW(Toolbox::SplitUriComponents(c, ""), OrthancException); ASSERT_THROW(Toolbox::SplitUriComponents(c, "a"), OrthancException); + ASSERT_THROW(Toolbox::SplitUriComponents(c, "/coucou//coucou"), OrthancException); }