changeset 105:e1e2b6b2139d dev

test with DICOMweb STOW-RS client
author Sebastien Jodogne <s.jodogne@gmail.com>
date Tue, 26 Apr 2016 17:38:47 +0200
parents 4274441e21d4
children 100c20770a25
files NEWS Plugin/Configuration.cpp Plugin/Configuration.h Plugin/Plugin.cpp Plugin/StowRs.cpp
diffstat 5 files changed, 386 insertions(+), 38 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Mon Apr 25 13:29:40 2016 +0200
+++ b/NEWS	Tue Apr 26 17:38:47 2016 +0200
@@ -1,6 +1,11 @@
 Pending changes in the mainline
 ===============================
 
+=> Minimum SDK version: 1.0.1 <=
+
+* STOW-RS client
+* Better robustness in the STOW-RS server
+
 
 Version 0.2 (2015/12/10)
 ========================
--- a/Plugin/Configuration.cpp	Mon Apr 25 13:29:40 2016 +0200
+++ b/Plugin/Configuration.cpp	Tue Apr 26 17:38:47 2016 +0200
@@ -23,6 +23,7 @@
 #include <fstream>
 #include <json/reader.h>
 #include <boost/regex.hpp>
+#include <boost/lexical_cast.hpp>
 
 #include "../Orthanc/Core/Toolbox.h"
 
@@ -81,48 +82,108 @@
 
 
   void ParseMultipartBody(std::vector<MultipartItem>& result,
+                          OrthancPluginContext* context,
                           const char* body,
                           const uint64_t bodySize,
                           const std::string& boundary)
   {
+    // Reference:
+    // https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
+
     result.clear();
 
-    boost::regex header("\r?(\n?)--" + boundary + "(--|.*\r?\n\r?\n)");
-    boost::regex pattern(".*^Content-Type\\s*:\\s*([^\\s]*).*",
-                         boost::regex::icase /* case insensitive */);
+    const boost::regex separator("\r\n--" + boundary + "(--|\r\n)");
+    const boost::regex encapsulation("(.*)\r\n\r\n(.*)");
+ 
+    std::vector< std::pair<const char*, const char*> > parts;
     
-    boost::cmatch what;
-    boost::match_flag_type flags = (boost::match_perl | 
-                                    boost::match_not_dot_null);
     const char* start = body;
     const char* end = body + bodySize;
-    std::string currentType;
 
-    while (boost::regex_search(start, end, what, header, flags))   
+    boost::cmatch what;
+    boost::match_flag_type flags = boost::match_perl;
+    while (boost::regex_search(start, end, what, separator, flags))   
     {
-      if (start != body)
+      if (start != body)  // Ignore the first separator
       {
-        MultipartItem item;
-        item.data_ = start;
-        item.size_ = what[0].first - start;
-        item.contentType_ = currentType;
+        parts.push_back(std::make_pair(start, what[0].first));
+      }
 
-        result.push_back(item);
+      if (*what[1].first == '-')
+      {
+        // This is the last separator (there is a trailing "--")
+        break;
       }
 
-      boost::cmatch contentType;
-      if (boost::regex_match(what[0].first, what[0].second, contentType, pattern))
-      {
-        currentType = contentType[1];
-      }
-      else
-      {
-        currentType.clear();
-      }
-    
       start = what[0].second;
       flags |= boost::match_prev_avail;
     }
+
+
+    for (size_t i = 0; i < parts.size(); i++)
+    {
+      if (boost::regex_match(parts[i].first, parts[i].second, what, encapsulation, boost::match_perl))
+      {
+        size_t dicomSize = what[2].second - what[2].first;
+
+        std::string contentType = "application/octet-stream";
+        std::vector<std::string> headers;
+
+        {
+          std::string tmp;
+          tmp.assign(what[1].first, what[1].second);
+          Orthanc::Toolbox::TokenizeString(headers, tmp, '\n');
+        }
+
+        bool valid = true;
+
+        for (size_t j = 0; j < headers.size(); j++)
+        {
+          std::vector<std::string> tokens;
+          Orthanc::Toolbox::TokenizeString(tokens, headers[j], ':');
+
+          if (tokens.size() == 2)
+          {
+            std::string key = Orthanc::Toolbox::StripSpaces(tokens[0]);
+            std::string value = Orthanc::Toolbox::StripSpaces(tokens[1]);
+            Orthanc::Toolbox::ToLowerCase(key);
+
+            if (key == "content-type")
+            {
+              contentType = value;
+            }
+            else if (key == "content-length")
+            {
+              try
+              {
+                size_t s = boost::lexical_cast<size_t>(value);
+                if (s != dicomSize)
+                {
+                  valid = false;
+                }
+              }
+              catch (boost::bad_lexical_cast&)
+              {
+                valid = false;
+              }
+            }
+          }
+        }
+
+        if (valid)
+        {
+          MultipartItem item;
+          item.data_ = what[2].first;
+          item.size_ = dicomSize;
+          item.contentType_ = contentType;
+          result.push_back(item);          
+        }
+        else
+        {
+          OrthancPluginLogWarning(context, "Ignoring a badly-formatted item in a multipart body");
+        }
+      }      
+    }
   }
 
 
@@ -132,7 +193,7 @@
                         bool applyPlugins)
   {
     OrthancPluginMemoryBuffer buffer;
-    int code;
+    OrthancPluginErrorCode code;
 
     if (applyPlugins)
     {
@@ -143,7 +204,7 @@
       code = OrthancPluginRestApiGet(context, &buffer, uri.c_str());
     }
 
-    if (code)
+    if (code != OrthancPluginErrorCode_Success)
     {
       // Error
       return false;
@@ -179,10 +240,15 @@
                       bool applyPlugins)
   {
     std::string content;
-    RestApiGetString(content, context, uri, applyPlugins);
-    
-    Json::Reader reader;
-    return reader.parse(content, result);
+    if (!RestApiGetString(content, context, uri, applyPlugins))
+    {
+      return false;
+    }
+    else
+    {
+      Json::Reader reader;
+      return reader.parse(content, result);
+    }
   }
 
 
@@ -192,9 +258,9 @@
                          const std::string& body)
   {
     OrthancPluginMemoryBuffer buffer;
-    int code = OrthancPluginRestApiPost(context, &buffer, uri.c_str(), body.c_str(), body.size());
+    OrthancPluginErrorCode code = OrthancPluginRestApiPost(context, &buffer, uri.c_str(), body.c_str(), body.size());
 
-    if (code)
+    if (code != OrthancPluginErrorCode_Success)
     {
       // Error
       return false;
@@ -230,10 +296,15 @@
                        const std::string& body)
   {
     std::string content;
-    RestApiPostString(content, context, uri, body);
-    
-    Json::Reader reader;
-    return reader.parse(content, result);
+    if (!RestApiPostString(content, context, uri, body))
+    {
+      return false;
+    }
+    else
+    {
+      Json::Reader reader;
+      return reader.parse(content, result);
+    }
   }
 
 
--- a/Plugin/Configuration.h	Mon Apr 25 13:29:40 2016 +0200
+++ b/Plugin/Configuration.h	Tue Apr 26 17:38:47 2016 +0200
@@ -49,6 +49,7 @@
                         const std::string& header);
 
   void ParseMultipartBody(std::vector<MultipartItem>& result,
+                          OrthancPluginContext* context,
                           const char* body,
                           const uint64_t bodySize,
                           const std::string& boundary);
--- a/Plugin/Plugin.cpp	Mon Apr 25 13:29:40 2016 +0200
+++ b/Plugin/Plugin.cpp	Tue Apr 26 17:38:47 2016 +0200
@@ -33,6 +33,12 @@
 #include <gdcmGlobal.h>
 
 
+#include <json/reader.h>
+#include <list>
+#include "../Orthanc/Core/ChunkedBuffer.h"
+#include "../Orthanc/Core/Toolbox.h"
+
+
 // Global state
 OrthancPluginContext* context_ = NULL;
 Json::Value configuration_;
@@ -133,6 +139,270 @@
 
 
 
+
+
+
+static void AddInstance(std::list<std::string>& target,
+                        const Json::Value& instance)
+{
+  if (instance.type() != Json::objectValue ||
+      !instance.isMember("ID") ||
+      instance["ID"].type() != Json::stringValue)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+  }
+  else
+  {
+    target.push_back(instance["ID"].asString());
+  }
+}
+
+
+
+
+static bool GetSequenceSize(size_t& result,
+                            const Json::Value& answer,
+                            const std::string& tag,
+                            bool isMandatory,
+                            const std::string& peer)
+{
+  const Json::Value* value = NULL;
+
+  std::string upper, lower;
+  Orthanc::Toolbox::ToUpperCase(upper, tag);
+  Orthanc::Toolbox::ToLowerCase(lower, tag);
+  
+  if (answer.isMember(upper))
+  {
+    value = &answer[upper];
+  }
+  else if (answer.isMember(lower))
+  {
+    value = &answer[lower];
+  }
+  else if (isMandatory)
+  {
+    std::string s = ("The STOW-RS JSON response from DICOMweb peer " + peer + 
+                     " does not contain the mandatory tag " + upper);
+    OrthancPluginLogError(context_, s.c_str());
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+  }
+  else
+  {
+    return false;
+  }
+
+  if (value->type() != Json::objectValue ||
+      !value->isMember("Value") ||
+      (*value) ["Value"].type() != Json::arrayValue)
+  {
+    std::string s = "Unable to parse STOW-RS JSON response from DICOMweb peer " + peer;
+    OrthancPluginLogError(context_, s.c_str());
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+  }
+
+  result = (*value) ["Value"].size();
+  return true;
+}
+
+
+static void SendStowRequest(const std::string& url,
+                            const char* username,
+                            const char* password,
+                            const std::string& body,
+                            const std::string& mime,
+                            size_t countInstances)
+{
+  const char* headersKeys[] = {
+    "Accept",
+    "Expect",
+    "Content-Type"
+  };
+
+  const char* headersValues[] = {
+    "application/json",
+    "",
+    mime.c_str()
+  };
+
+  uint16_t status = 0;
+  OrthancPluginMemoryBuffer answer;
+  OrthancPluginErrorCode code = OrthancPluginHttpClient(context_, &answer, &status, OrthancPluginHttpMethod_Post,
+                                                        url.c_str(), 3, headersKeys, headersValues,
+                                                        body.c_str(), body.size(), username, password, 0);
+  if (code != OrthancPluginErrorCode_Success ||
+      (status != 200 && status != 202))
+  {
+    std::string s = ("Cannot send DICOM images through STOW-RS to DICOMweb peer " + url + 
+                     " (HTTP status: " + boost::lexical_cast<std::string>(status) + ")");
+    OrthancPluginLogError(context_, s.c_str());
+    throw Orthanc::OrthancException(static_cast<Orthanc::ErrorCode>(code));
+  }
+
+  Json::Value response;
+  Json::Reader reader;
+  bool success = reader.parse(reinterpret_cast<const char*>(answer.data),
+                              reinterpret_cast<const char*>(answer.data) + answer.size, response);
+  OrthancPluginFreeMemoryBuffer(context_, &answer);
+
+  if (!success ||
+      response.type() != Json::objectValue ||
+      !response.isMember("00081199"))
+  {
+    std::string s = "Unable to parse STOW-RS JSON response from DICOMweb peer " + url;
+    OrthancPluginLogError(context_, s.c_str());
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+  }
+
+  size_t size;
+  if (!GetSequenceSize(size, response, "00081199", true, url) ||
+      size != countInstances)
+  {
+    std::string s = ("The STOW-RS server was only able to receive " + 
+                     boost::lexical_cast<std::string>(size) + " instances out of " +
+                     boost::lexical_cast<std::string>(countInstances));
+    OrthancPluginLogError(context_, s.c_str());
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+  }
+
+  if (GetSequenceSize(size, response, "00081198", false, url) &&
+      size != 0)
+  {
+    std::string s = ("The response from the STOW-RS server contains " + 
+                     boost::lexical_cast<std::string>(size) + 
+                     " items in its Failed SOP Sequence (0008,1198) tag");
+    OrthancPluginLogError(context_, s.c_str());
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);    
+  }
+
+  if (GetSequenceSize(size, response, "0008119A", false, url) &&
+      size != 0)
+  {
+    std::string s = ("The response from the STOW-RS server contains " + 
+                     boost::lexical_cast<std::string>(size) + 
+                     " items in its Other Failures Sequence (0008,119A) tag");
+    OrthancPluginLogError(context_, s.c_str());
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);    
+  }
+}
+
+
+
+void StowClient(OrthancPluginRestOutput* output,
+                const char* url,
+                const OrthancPluginHttpRequest* request)
+{
+  if (request->groupsCount != 1)
+  {
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest);
+  }
+
+  if (request->method != OrthancPluginHttpMethod_Post)
+  {
+    OrthancPluginSendMethodNotAllowed(context_, output, "POST");
+    return;
+  }
+
+  std::string peer(request->groups[0]);
+
+  Json::Value resources;
+  Json::Reader reader;
+  if (!reader.parse(request->body, request->body + request->bodySize, resources) ||
+      resources.type() != Json::arrayValue)
+  {
+    std::string s = "The list of resources to be sent through DICOMweb STOW-RS must be given as a JSON array";
+    OrthancPluginLogError(context_, s.c_str());
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+  }
+
+  // Extract information about all the child instances
+  std::list<std::string> instances;
+  for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++)
+  {
+    if (resources[i].type() != Json::stringValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    std::string resource = resources[i].asString();
+
+    Json::Value tmp;
+    if (OrthancPlugins::RestApiGetJson(tmp, context_, "/instances/" + resource, false))
+    {
+      AddInstance(instances, tmp);
+    }
+    else if ((OrthancPlugins::RestApiGetJson(tmp, context_, "/series/" + resource, false) &&
+              OrthancPlugins::RestApiGetJson(tmp, context_, "/series/" + resource + "/instances", false)) ||
+             (OrthancPlugins::RestApiGetJson(tmp, context_, "/studies/" + resource, false) &&
+              OrthancPlugins::RestApiGetJson(tmp, context_, "/studies/" + resource + "/instances", false)) ||
+             (OrthancPlugins::RestApiGetJson(tmp, context_, "/patients/" + resource, false) &&
+              OrthancPlugins::RestApiGetJson(tmp, context_, "/patients/" + resource + "/instances", false)))
+    {
+      if (tmp.type() != Json::arrayValue)
+      {
+        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      }
+
+      for (Json::Value::ArrayIndex j = 0; j < tmp.size(); j++)
+      {
+        AddInstance(instances, tmp[j]);
+      }
+    }
+    else
+    {
+      // Unkown resource
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    }   
+  }
+
+
+  std::string boundary;
+
+  {
+    char* uuid = OrthancPluginGenerateUuid(context_);
+    try
+    {
+      boundary.assign(uuid);
+    }
+    catch (...)
+    {
+      OrthancPluginFreeString(context_, uuid);
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory);
+    }
+
+    OrthancPluginFreeString(context_, uuid);
+  }
+
+  std::string mime = "multipart/related; type=application/dicom; boundary=" + boundary;
+
+  Orthanc::ChunkedBuffer chunks;
+  chunks.AddChunk("\r\n"); // Empty preamble
+
+  for (std::list<std::string>::const_iterator it = instances.begin(); it != instances.end(); it++)
+  {
+    std::string dicom;
+    if (OrthancPlugins::RestApiGetString(dicom, context_, "/instances/" + *it + "/file"))
+    {
+      chunks.AddChunk("--" + boundary + "\r\n" +
+                      "Content-Type: application/dicom\r\n" +
+                      "Content-Length: " + boost::lexical_cast<std::string>(dicom.size()) +
+                      "\r\n\r\n");
+      chunks.AddChunk(dicom);
+      chunks.AddChunk("\r\n");
+    }
+  }
+
+  chunks.AddChunk("--" + boundary + "--\r\n");
+
+  std::string body;
+  chunks.Flatten(body);
+
+  // TODO Split the message
+
+  SendStowRequest("http://localhost:8043/dicom-web/studies", NULL, NULL, body, mime, instances.size());
+}
+
+
 extern "C"
 {
   ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
@@ -208,6 +478,8 @@
       Register(root, "studies/([^/]*)/series/([^/]*)/metadata", Protect<RetrieveSeriesMetadata>);
       Register(root, "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/frames", Protect<RetrieveFrames>);
       Register(root, "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/frames/([^/]*)", Protect<RetrieveFrames>);
+
+      Register(root, "peers/([^/]*)/stow", Protect<StowClient>);
     }
     else
     {
--- a/Plugin/StowRs.cpp	Mon Apr 25 13:29:40 2016 +0200
+++ b/Plugin/StowRs.cpp	Tue Apr 26 17:38:47 2016 +0200
@@ -147,14 +147,13 @@
   }
 
 
-
   bool isFirst = true;
   gdcm::DataSet result;
   gdcm::SmartPointer<gdcm::SequenceOfItems> success = new gdcm::SequenceOfItems();
   gdcm::SmartPointer<gdcm::SequenceOfItems> failed = new gdcm::SequenceOfItems();
   
   std::vector<OrthancPlugins::MultipartItem> items;
-  OrthancPlugins::ParseMultipartBody(items, request->body, request->bodySize, boundary);
+  OrthancPlugins::ParseMultipartBody(items, context_, request->body, request->bodySize, boundary);
   for (size_t i = 0; i < items.size(); i++)
   {
     if (!items[i].contentType_.empty() &&