# HG changeset patch # User Sebastien Jodogne # Date 1435592854 -7200 # Node ID ad94a3583b079a164478356b8728b1eb5d93092f # Parent 7366a0bdda6a0b86fa1a2184fe6d76378c149357 Plugins can send answers as multipart messages diff -r 7366a0bdda6a -r ad94a3583b07 Core/HttpServer/HttpOutput.cpp --- a/Core/HttpServer/HttpOutput.cpp Mon Jun 29 14:43:08 2015 +0200 +++ b/Core/HttpServer/HttpOutput.cpp Mon Jun 29 17:47:34 2015 +0200 @@ -151,6 +151,11 @@ } } + if (state_ == State_WritingMultipart) + { + throw OrthancException(ErrorCode_InternalError); + } + if (state_ == State_WritingHeader) { // Send the HTTP header before writing the body @@ -270,4 +275,109 @@ { stateMachine_.SendBody(NULL, 0); } + + + void HttpOutput::StateMachine::StartMultipart(const std::string& subType, + const std::string& contentType) + { + if (subType != "mixed" && + subType != "related") + { + throw OrthancException(ErrorCode_ParameterOutOfRange); + } + + if (keepAlive_) + { + LOG(ERROR) << "Multipart answers are not implemented together with keep-alive connections"; + throw OrthancException(ErrorCode_NotImplemented); + } + + if (state_ != State_WritingHeader) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + if (status_ != HttpStatus_200_Ok) + { + SendBody(NULL, 0); + return; + } + + stream_.OnHttpStatusReceived(status_); + + std::string header = "HTTP/1.1 200 OK\r\n"; + + // Possibly add the cookies + for (std::list::const_iterator + it = headers_.begin(); it != headers_.end(); ++it) + { + if (!Toolbox::StartsWith(*it, "Set-Cookie: ")) + { + LOG(ERROR) << "The only headers that can be set in multipart answers are Set-Cookie (here: " << *it << " is set)"; + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + header += *it; + } + + multipartBoundary_ = Toolbox::GenerateUuid(); + multipartContentType_ = contentType; + header += "Content-Type: multipart/related; type=multipart/" + subType + "; boundary=" + multipartBoundary_ + "\r\n\r\n"; + + stream_.Send(true, header.c_str(), header.size()); + state_ = State_WritingMultipart; + } + + + void HttpOutput::StateMachine::SendMultipartItem(const void* item, size_t length) + { + std::string header = "--" + multipartBoundary_ + "\n"; + header += "Content-Type: " + multipartContentType_ + "\n"; + header += "Content-Length: " + boost::lexical_cast(length) + "\n"; + header += "MIME-Version: 1.0\n\n"; + + stream_.Send(false, header.c_str(), header.size()); + + if (length > 0) + { + stream_.Send(false, item, length); + } + + stream_.Send(false, "\n", 1); + } + + + void HttpOutput::StateMachine::CloseMultipart() + { + if (state_ != State_WritingMultipart) + { + throw OrthancException(ErrorCode_BadSequenceOfCalls); + } + + // The two lines below might throw an exception, if the client has + // closed the connection. Such an error is ignored. + try + { + std::string header = "--" + multipartBoundary_ + "--\n"; + stream_.Send(false, header.c_str(), header.size()); + } + catch (OrthancException&) + { + } + + state_ = State_Done; + } + + + void HttpOutput::SendMultipartItem(const std::string& item) + { + if (item.size() > 0) + { + stateMachine_.SendMultipartItem(item.c_str(), item.size()); + } + else + { + stateMachine_.SendMultipartItem(NULL, 0); + } + } } diff -r 7366a0bdda6a -r ad94a3583b07 Core/HttpServer/HttpOutput.h --- a/Core/HttpServer/HttpOutput.h Mon Jun 29 14:43:08 2015 +0200 +++ b/Core/HttpServer/HttpOutput.h Mon Jun 29 17:47:34 2015 +0200 @@ -38,6 +38,7 @@ #include "../Enumerations.h" #include "IHttpOutputStream.h" #include "HttpHandler.h" +#include "../Uuid.h" namespace Orthanc { @@ -48,14 +49,16 @@ class StateMachine : public boost::noncopyable { - private: + public: enum State { State_WritingHeader, State_WritingBody, + State_WritingMultipart, State_Done }; + private: IHttpOutputStream& stream_; State state_; @@ -66,6 +69,9 @@ bool keepAlive_; std::list headers_; + std::string multipartBoundary_; + std::string multipartContentType_; + public: StateMachine(IHttpOutputStream& stream, bool isKeepAlive); @@ -89,6 +95,18 @@ void ClearHeaders(); void SendBody(const void* buffer, size_t length); + + void StartMultipart(const std::string& subType, + const std::string& contentType); + + void SendMultipartItem(const void* item, size_t length); + + void CloseMultipart(); + + State GetState() const + { + return state_; + } }; StateMachine stateMachine_; @@ -140,5 +158,28 @@ void Redirect(const std::string& path); void SendUnauthorized(const std::string& realm); + + void StartMultipart(const std::string& subType, + const std::string& contentType) + { + stateMachine_.StartMultipart(subType, contentType); + } + + void SendMultipartItem(const std::string& item); + + void SendMultipartItem(const void* item, size_t size) + { + stateMachine_.SendMultipartItem(item, size); + } + + void CloseMultipart() + { + stateMachine_.CloseMultipart(); + } + + bool IsWritingMultipart() const + { + return stateMachine_.GetState() == StateMachine::State_WritingMultipart; + } }; } diff -r 7366a0bdda6a -r ad94a3583b07 Core/HttpServer/MongooseServer.cpp --- a/Core/HttpServer/MongooseServer.cpp Mon Jun 29 14:43:08 2015 +0200 +++ b/Core/HttpServer/MongooseServer.cpp Mon Jun 29 17:47:34 2015 +0200 @@ -82,7 +82,12 @@ { if (length > 0) { - mg_write(connection_, buffer, length); + int status = mg_write(connection_, buffer, length); + if (status != static_cast(length)) + { + // status == 0 when the connection has been closed, -1 on error + throw OrthancException(ErrorCode_NetworkProtocol); + } } } diff -r 7366a0bdda6a -r ad94a3583b07 Core/Toolbox.cpp --- a/Core/Toolbox.cpp Mon Jun 29 14:43:08 2015 +0200 +++ b/Core/Toolbox.cpp Mon Jun 29 17:47:34 2015 +0200 @@ -1233,5 +1233,20 @@ break; } } + + + bool Toolbox::StartsWith(const std::string& str, + const std::string& prefix) + { + if (str.size() < prefix.size()) + { + return false; + } + else + { + return str.compare(0, prefix.size(), prefix) == 0; + } + } + } diff -r 7366a0bdda6a -r ad94a3583b07 Core/Toolbox.h --- a/Core/Toolbox.h Mon Jun 29 14:43:08 2015 +0200 +++ b/Core/Toolbox.h Mon Jun 29 17:47:34 2015 +0200 @@ -160,5 +160,8 @@ void CopyJsonWithoutComments(Json::Value& target, const Json::Value& source); + + bool StartsWith(const std::string& str, + const std::string& prefix); } } diff -r 7366a0bdda6a -r ad94a3583b07 NEWS --- a/NEWS Mon Jun 29 14:43:08 2015 +0200 +++ b/NEWS Mon Jun 29 17:47:34 2015 +0200 @@ -1,9 +1,20 @@ Pending changes in the mainline =============================== + * The configuration can be splitted into several files stored inside the same folder * Custom setting of the local AET during C-Store SCU (both in Lua and in the REST API) + +Plugins +------- + * Plugins can retrieve the configuration file directly as a JSON string +* Plugins can send answers as multipart messages + +Fixes +----- + +* Fix compatibility issues for C-Find SCU to Siemens Syngo.Via modalities SCP * Fix issue 35 (Characters in PatientID string are not protected for C-Find) * Fix issue 37 (Hyphens trigger range query even if datatype does not support ranges) diff -r 7366a0bdda6a -r ad94a3583b07 Plugins/Engine/OrthancPlugins.cpp --- a/Plugins/Engine/OrthancPlugins.cpp Mon Jun 29 14:43:08 2015 +0200 +++ b/Plugins/Engine/OrthancPlugins.cpp Mon Jun 29 17:47:34 2015 +0200 @@ -426,6 +426,12 @@ &request); } + if (error == 0 && + output.IsWritingMultipart()) + { + output.CloseMultipart(); + } + if (error < 0) { LOG(ERROR) << "Plugin callback failed with error code " << error; @@ -1232,6 +1238,26 @@ return true; } + case _OrthancPluginService_StartMultipartAnswer: + { + const _OrthancPluginStartMultipartAnswer& p = + *reinterpret_cast(parameters); + HttpOutput* output = reinterpret_cast(p.output); + output->StartMultipart(p.subType, p.contentType); + return true; + } + + case _OrthancPluginService_SendMultipartItem: + { + // An exception might be raised in this function if the + // connection was closed by the HTTP client. + const _OrthancPluginAnswerBuffer& p = + *reinterpret_cast(parameters); + HttpOutput* output = reinterpret_cast(p.output); + output->SendMultipartItem(p.answer, p.answerSize); + return true; + } + default: return false; } diff -r 7366a0bdda6a -r ad94a3583b07 Plugins/Include/orthanc/OrthancCPlugin.h --- a/Plugins/Include/orthanc/OrthancCPlugin.h Mon Jun 29 14:43:08 2015 +0200 +++ b/Plugins/Include/orthanc/OrthancCPlugin.h Mon Jun 29 17:47:34 2015 +0200 @@ -273,6 +273,8 @@ _OrthancPluginService_SendMethodNotAllowed = 2005, _OrthancPluginService_SetCookie = 2006, _OrthancPluginService_SetHttpHeader = 2007, + _OrthancPluginService_StartMultipartAnswer = 2008, + _OrthancPluginService_SendMultipartItem = 2009, /* Access to the Orthanc database and API */ _OrthancPluginService_GetDicomForInstance = 3000, @@ -2153,6 +2155,66 @@ + typedef struct + { + OrthancPluginRestOutput* output; + const char* subType; + const char* contentType; + } _OrthancPluginStartMultipartAnswer; + + /** + * @brief Start an HTTP multipart answer. + * + * Initiates a HTTP multipart answer, as the result of a REST request. + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param subType The sub-type of the multipart answer ("mixed" or "related"). + * @param contentType The MIME type of the items in the multipart answer. + * @return 0 if success, other value if error. + * @see OrthancPluginSendMultipartItem() + **/ + ORTHANC_PLUGIN_INLINE int32_t OrthancPluginStartMultipartAnswer( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const char* subType, + const char* contentType) + { + _OrthancPluginStartMultipartAnswer params; + params.output = output; + params.subType = subType; + params.contentType = contentType; + return context->InvokeService(context, _OrthancPluginService_StartMultipartAnswer, ¶ms); + } + + + /** + * @brief Send an item as a part of some HTTP multipart answer. + * + * This function sends an item as a part of some HTTP multipart + * answer that was initiated by OrthancPluginStartMultipartAnswer(). + * + * @param context The Orthanc plugin context, as received by OrthancPluginInitialize(). + * @param output The HTTP connection to the client application. + * @param answer Pointer to the memory buffer containing the item. + * @param answerSize Number of bytes of the item. + * @return 0 if success, other value if error (this notably happens + * if the connection is closed by the client). + **/ + ORTHANC_PLUGIN_INLINE int32_t OrthancPluginSendMultipartItem( + OrthancPluginContext* context, + OrthancPluginRestOutput* output, + const char* answer, + uint32_t answerSize) + { + _OrthancPluginAnswerBuffer params; + params.output = output; + params.answer = answer; + params.answerSize = answerSize; + params.mimeType = NULL; + return context->InvokeService(context, _OrthancPluginService_SendMultipartItem, ¶ms); + } + #ifdef __cplusplus } #endif diff -r 7366a0bdda6a -r ad94a3583b07 Resources/OrthancPlugin.doxygen --- a/Resources/OrthancPlugin.doxygen Mon Jun 29 14:43:08 2015 +0200 +++ b/Resources/OrthancPlugin.doxygen Mon Jun 29 17:47:34 2015 +0200 @@ -655,9 +655,9 @@ # directories like "/usr/src/myproject". Separate the files or directories # with spaces. -INPUT = @CMAKE_SOURCE_DIR@/Plugins/Include/OrthancCPlugin.h \ - @CMAKE_SOURCE_DIR@/Plugins/Include/OrthancCDatabasePlugin.h \ - @CMAKE_SOURCE_DIR@/Plugins/Include/OrthancCppDatabasePlugin.h +INPUT = @CMAKE_SOURCE_DIR@/Plugins/Include/orthanc/OrthancCPlugin.h \ + @CMAKE_SOURCE_DIR@/Plugins/Include/orthanc/OrthancCDatabasePlugin.h \ + @CMAKE_SOURCE_DIR@/Plugins/Include/orthanc/OrthancCppDatabasePlugin.h # This tag can be used to specify the character encoding of the source files # that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is diff -r 7366a0bdda6a -r ad94a3583b07 UnitTestsSources/UnitTestsMain.cpp --- a/UnitTestsSources/UnitTestsMain.cpp Mon Jun 29 14:43:08 2015 +0200 +++ b/UnitTestsSources/UnitTestsMain.cpp Mon Jun 29 17:47:34 2015 +0200 @@ -807,6 +807,18 @@ } +TEST(Toolbox, StartsWith) +{ + ASSERT_TRUE(Toolbox::StartsWith("hello world", "")); + ASSERT_TRUE(Toolbox::StartsWith("hello world", "hello")); + ASSERT_TRUE(Toolbox::StartsWith("hello world", "h")); + ASSERT_FALSE(Toolbox::StartsWith("hello world", "H")); + ASSERT_FALSE(Toolbox::StartsWith("h", "hello")); + ASSERT_TRUE(Toolbox::StartsWith("h", "h")); + ASSERT_FALSE(Toolbox::StartsWith("", "h")); +} + + int main(int argc, char **argv) { // Initialize Google's logging library.