changeset 1430:ad94a3583b07

Plugins can send answers as multipart messages
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 29 Jun 2015 17:47:34 +0200
parents 7366a0bdda6a
children 3e53edcd0120
files Core/HttpServer/HttpOutput.cpp Core/HttpServer/HttpOutput.h Core/HttpServer/MongooseServer.cpp Core/Toolbox.cpp Core/Toolbox.h NEWS Plugins/Engine/OrthancPlugins.cpp Plugins/Include/orthanc/OrthancCPlugin.h Resources/OrthancPlugin.doxygen UnitTestsSources/UnitTestsMain.cpp
diffstat 10 files changed, 290 insertions(+), 5 deletions(-) [+]
line wrap: on
line diff
--- 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<std::string>::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<std::string>(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);
+    }
+  }
 }
--- 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<std::string> 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;
+    }
   };
 }
--- 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<int>(length))
+          {
+            // status == 0 when the connection has been closed, -1 on error
+            throw OrthancException(ErrorCode_NetworkProtocol);
+          }
         }
       }
 
--- 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;
+    }
+  }
+
 }
 
--- 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);
   }
 }
--- 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)
 
--- 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<const _OrthancPluginStartMultipartAnswer*>(parameters);
+        HttpOutput* output = reinterpret_cast<HttpOutput*>(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<const _OrthancPluginAnswerBuffer*>(parameters);
+        HttpOutput* output = reinterpret_cast<HttpOutput*>(p.output);
+        output->SendMultipartItem(p.answer, p.answerSize);
+        return true;
+      }
+
       default:
         return false;
     }
--- 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, &params);
+  }
+
+
+  /**
+   * @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, &params);
+  }
+
 #ifdef  __cplusplus
 }
 #endif
--- 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
--- 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.