changeset 113:04fbfd59a60e dev

integration mainline->dev
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 28 Apr 2016 17:22:02 +0200
parents bcc9e98bb725 (diff) 528d18573c09 (current diff)
children bee10bb1331b
files NEWS Plugin/Configuration.cpp Plugin/Configuration.h
diffstat 14 files changed, 814 insertions(+), 79 deletions(-) [+]
line wrap: on
line diff
--- a/AUTHORS	Thu Apr 28 17:20:36 2016 +0200
+++ b/AUTHORS	Thu Apr 28 17:22:02 2016 +0200
@@ -1,5 +1,5 @@
-DICOM Web plugin for Orthanc
-============================
+DICOMweb plugin for Orthanc
+===========================
 
 
 Authors
--- a/CMakeLists.txt	Thu Apr 28 17:20:36 2016 +0200
+++ b/CMakeLists.txt	Thu Apr 28 17:22:02 2016 +0200
@@ -121,9 +121,11 @@
   )
 
 add_library(OrthancDicomWeb SHARED ${CORE_SOURCES}
+  ${CMAKE_SOURCE_DIR}/Plugin/DicomWebPeers.cpp
   ${CMAKE_SOURCE_DIR}/Plugin/Plugin.cpp
   ${CMAKE_SOURCE_DIR}/Plugin/QidoRs.cpp
   ${CMAKE_SOURCE_DIR}/Plugin/StowRs.cpp
+  ${CMAKE_SOURCE_DIR}/Plugin/StowRsClient.cpp
   ${CMAKE_SOURCE_DIR}/Plugin/WadoRs.cpp
   ${CMAKE_SOURCE_DIR}/Plugin/WadoRsRetrieveFrames.cpp
   ${CMAKE_SOURCE_DIR}/Plugin/WadoUri.cpp
--- a/NEWS	Thu Apr 28 17:20:36 2016 +0200
+++ b/NEWS	Thu Apr 28 17:22:02 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
+
 
 * Fix issue #13 (QIDO-RS study-level query is slow)
 * Fix issue #14 (Aggregate fields empty for QIDO-RS study/series-level queries)
--- a/Plugin/Configuration.cpp	Thu Apr 28 17:20:36 2016 +0200
+++ b/Plugin/Configuration.cpp	Thu Apr 28 17:22:02 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,107 @@
 
 
   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 | boost::match_single_line;
+    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[2].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 +192,7 @@
                         bool applyPlugins)
   {
     OrthancPluginMemoryBuffer buffer;
-    int code;
+    OrthancPluginErrorCode code;
 
     if (applyPlugins)
     {
@@ -143,7 +203,7 @@
       code = OrthancPluginRestApiGet(context, &buffer, uri.c_str());
     }
 
-    if (code)
+    if (code != OrthancPluginErrorCode_Success)
     {
       // Error
       return false;
@@ -179,10 +239,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 +257,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 +295,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	Thu Apr 28 17:20:36 2016 +0200
+++ b/Plugin/Configuration.h	Thu Apr 28 17:22:02 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);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugin/DicomWebPeers.cpp	Thu Apr 28 17:22:02 2016 +0200
@@ -0,0 +1,141 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomWebPeers.h"
+
+#include "Plugin.h"
+#include "../Orthanc/Core/OrthancException.h"
+
+namespace OrthancPlugins
+{
+  void DicomWebPeer::SetUrl(const std::string& url)
+  {
+    if (url.empty())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+
+    url_ = url;
+
+    // Add trailing slash
+    if (url_[url.size() - 1] != '/')
+    {
+      url_ += '/';
+    }
+  }
+
+
+  void DicomWebPeers::Clear()
+  {
+    for (Peers::iterator it = peers_.begin(); it != peers_.end(); ++it)
+    {
+      delete it->second;
+    }
+  }
+
+
+  void DicomWebPeers::Load(const Json::Value& configuration)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Clear();
+
+    if (!configuration.isMember("Peers"))
+    {
+      return;
+    }
+
+    bool ok = true;
+
+    if (configuration["Peers"].type() != Json::objectValue)
+    {
+      ok = false;
+    }
+    else
+    {
+      Json::Value::Members members = configuration["Peers"].getMemberNames();
+
+      for (size_t i = 0; i < members.size(); i++)
+      {
+        const Json::Value& peer = configuration["Peers"][members[i]];
+
+        if (peer.type() != Json::arrayValue ||
+            (peer.size() != 1 && peer.size() != 3) ||
+            peer[0].type() != Json::stringValue ||
+            (peer.size() == 3 && peer[1].type() != Json::stringValue) ||
+            (peer.size() == 3 && peer[2].type() != Json::stringValue))
+        {
+          ok = false;
+          break;
+        }
+        else
+        {
+          peers_[members[i]] = new DicomWebPeer(peer[0].asString(),
+                                                peer[1].asString(),
+                                                peer[2].asString());
+        }
+      }
+    }
+
+    if (!ok)
+    {
+      OrthancPluginLogError(context_, "Cannot parse the \"DicomWeb.Peers\" section of the configuration file");
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  DicomWebPeers& DicomWebPeers::GetInstance()
+  {
+    static DicomWebPeers singleton;
+    return singleton;
+  }
+
+
+  DicomWebPeer DicomWebPeers::GetPeer(const std::string& name)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    Peers::const_iterator peer = peers_.find(name);
+
+    if (peer == peers_.end() ||
+        peer->second == NULL)
+    {
+      std::string s = "Inexistent peer: " + name;
+      OrthancPluginLogError(context_, s.c_str());
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentItem);
+    }
+    else
+    {
+      return *peer->second;
+    }
+  }
+
+
+  void DicomWebPeers::ListPeers(std::list<std::string>& peers)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    peers.clear();
+    for (Peers::const_iterator it = peers_.begin(); it != peers_.end(); ++it)
+    {
+      peers.push_back(it->first);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugin/DicomWebPeers.h	Thu Apr 28 17:22:02 2016 +0200
@@ -0,0 +1,108 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+#pragma once
+
+#include <list>
+#include <string>
+#include <boost/thread/mutex.hpp>
+#include <json/value.h>
+
+namespace OrthancPlugins
+{
+  class DicomWebPeer
+  {
+  private:
+    std::string url_;
+    std::string username_;
+    std::string password_;
+
+    void SetUrl(const std::string& url);
+
+  public:
+    DicomWebPeer(const std::string& url,
+                 const std::string& username,
+                 const std::string& password) :
+      username_(username),
+      password_(password)
+    {
+      SetUrl(url);
+    }
+
+    DicomWebPeer(const std::string& url)
+    {
+      SetUrl(url);
+    }
+
+    const std::string& GetUrl() const
+    {
+      return url_;
+    }
+
+    const std::string& GetUsername() const
+    {
+      return username_;
+    }
+
+    const std::string& GetPassword() const
+    {
+      return password_;
+    }
+
+    const char* GetUsernameC() const
+    {
+      return username_.empty() ? NULL : username_.c_str();
+    }
+
+    const char* GetPasswordC() const
+    {
+      return password_.empty() ? NULL : password_.c_str();
+    }
+  };
+
+
+  class DicomWebPeers
+  {
+  private:
+    typedef std::map<std::string, DicomWebPeer*>  Peers;
+
+    boost::mutex  mutex_;
+    Peers         peers_;
+
+    void Clear();
+
+    DicomWebPeers()  // Forbidden (singleton pattern)
+    {
+    }
+
+  public:
+    void Load(const Json::Value& configuration);
+
+    ~DicomWebPeers()
+    {
+      Clear();
+    }
+
+    static DicomWebPeers& GetInstance();
+
+    DicomWebPeer GetPeer(const std::string& name);
+
+    void ListPeers(std::list<std::string>& peers);
+  };
+}
--- a/Plugin/Plugin.cpp	Thu Apr 28 17:20:36 2016 +0200
+++ b/Plugin/Plugin.cpp	Thu Apr 28 17:22:02 2016 +0200
@@ -22,10 +22,11 @@
 
 #include "QidoRs.h"
 #include "StowRs.h"
+#include "StowRsClient.h"
 #include "WadoRs.h"
 #include "WadoUri.h"
 #include "Configuration.h"
-
+#include "DicomWebPeers.h"
 
 #include <gdcmDictEntry.h>
 #include <gdcmDict.h>
@@ -60,7 +61,7 @@
   catch (Orthanc::OrthancException& e)
   {
     OrthancPluginLogError(context_, e.What());
-    return OrthancPluginErrorCode_Plugin;
+    return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
   }
   catch (boost::bad_lexical_cast& e)
   {
@@ -133,6 +134,54 @@
 
 
 
+void ListPeers(OrthancPluginRestOutput* output,
+               const char* url,
+               const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context_, output, "GET");
+  }
+  else
+  {
+    std::list<std::string> peers;
+    OrthancPlugins::DicomWebPeers::GetInstance().ListPeers(peers);
+
+    Json::Value json = Json::arrayValue;
+    for (std::list<std::string>::const_iterator it = peers.begin(); it != peers.end(); ++it)
+    {
+      json.append(*it);
+    }
+
+    std::string answer = json.toStyledString(); 
+    OrthancPluginAnswerBuffer(context_, output, answer.c_str(), answer.size(), "application/json");
+  }
+}
+
+
+void ListPeerOperations(OrthancPluginRestOutput* output,
+                        const char* url,
+                        const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context_, output, "GET");
+  }
+  else
+  {
+    // Make sure the peer does exist
+    OrthancPlugins::DicomWebPeers::GetInstance().GetPeer(request->groups[0]);
+
+    Json::Value json = Json::arrayValue;
+    json.append("stow");
+
+    std::string answer = json.toStyledString(); 
+    OrthancPluginAnswerBuffer(context_, output, answer.c_str(), answer.size(), "application/json");
+  }
+}
+
+
+
 extern "C"
 {
   ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
@@ -152,17 +201,7 @@
       return -1;
     }
 
-    {
-      std::string version(context_->orthancVersion);
-      if (version == "0.9.1")
-      {
-        OrthancPluginLogWarning(context_, "If using STOW-RS, the DICOMweb plugin can lead to "
-                                "deadlocks in Orthanc version 0.9.1. Please upgrade Orthanc!");
-      }
-    }
-
-
-    OrthancPluginSetDescription(context_, "Implementation of DICOM Web (QIDO-RS, STOW-RS and WADO-RS) and WADO.");
+    OrthancPluginSetDescription(context_, "Implementation of DICOMweb (QIDO-RS, STOW-RS and WADO-RS) and WADO-URI.");
 
     // Read the configuration
     dictionary_ = &gdcm::Global::GetInstance().GetDicts().GetPublicDict();
@@ -185,6 +224,9 @@
       }
     }
 
+    OrthancPlugins::DicomWebPeers::GetInstance().Load(configuration_);
+
+
     // Configure the DICOMweb callbacks
     if (OrthancPlugins::Configuration::GetBoolValue(configuration_, "Enable", true))
     {
@@ -208,6 +250,10 @@
       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", Protect<ListPeers>);
+      Register(root, "peers/([^/]*)", Protect<ListPeerOperations>);
+      Register(root, "peers/([^/]*)/stow", Protect<StowClient>);
     }
     else
     {
@@ -219,14 +265,14 @@
     {
       std::string wado = OrthancPlugins::Configuration::GetWadoRoot(configuration_);
 
-      std::string message = "URI to the WADO API: " + wado;
+      std::string message = "URI to the WADO-URI API: " + wado;
       OrthancPluginLogWarning(context_, message.c_str());
 
       OrthancPluginRegisterRestCallback(context_, wado.c_str(), Protect<WadoUriCallback>);
     }
     else
     {
-      OrthancPluginLogWarning(context_, "WADO support is disabled");
+      OrthancPluginLogWarning(context_, "WADO-URI support is disabled");
     }
 
     return 0;
--- a/Plugin/StowRs.cpp	Thu Apr 28 17:20:36 2016 +0200
+++ b/Plugin/StowRs.cpp	Thu Apr 28 17:22:02 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() &&
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugin/StowRsClient.cpp	Thu Apr 28 17:22:02 2016 +0200
@@ -0,0 +1,325 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "StowRsClient.h"
+
+#include "Plugin.h"
+#include "DicomWebPeers.h"
+
+#include <json/reader.h>
+#include <list>
+#include <boost/lexical_cast.hpp>
+
+#include "../Orthanc/Core/ChunkedBuffer.h"
+#include "../Orthanc/Core/OrthancException.h"
+#include "../Orthanc/Core/Toolbox.h"
+
+
+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 OrthancPlugins::DicomWebPeer& peer,
+                            const std::string& mime,
+                            const std::string& body,
+                            size_t countInstances)
+{
+  const char* headersKeys[] = {
+    "Accept",
+    "Expect",
+    "Content-Type"
+  };
+
+  const char* headersValues[] = {
+    "application/json",
+    "",
+    mime.c_str()
+  };
+
+  std::string url = peer.GetUrl() + "studies";
+
+  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(), peer.GetUsernameC(), peer.GetPasswordC(), 0);
+  if (code != OrthancPluginErrorCode_Success ||
+      (status != 200 && status != 202))
+  {
+    std::string s = ("Cannot send DICOM images through STOW-RS to DICOMweb peer " + peer.GetUrl() + 
+                     " (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 " + peer.GetUrl();
+    OrthancPluginLogError(context_, s.c_str());
+    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+  }
+
+  size_t size;
+  if (!GetSequenceSize(size, response, "00081199", true, peer.GetUrl()) ||
+      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, peer.GetUrl()) &&
+      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, peer.GetUrl()) &&
+      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);    
+  }
+}
+
+
+static void GetListOfInstances(std::list<std::string>& instances,
+                               const OrthancPluginHttpRequest* request)
+{
+  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
+  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();
+    if (resource.empty())
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    }
+
+    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
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    }   
+  }
+}
+
+
+static void SendStowChunks(const OrthancPlugins::DicomWebPeer& peer,
+                           const std::string& mime,
+                           const std::string& boundary,
+                           Orthanc::ChunkedBuffer& chunks,
+                           size_t& countInstances,
+                           bool force)
+{
+  if ((force && countInstances > 0) ||
+      countInstances > 10 /* TODO Parameter */ ||
+      chunks.GetNumBytes() > 10 * 1024 * 1024 /* TODO Parameter */)
+  {
+    chunks.AddChunk("\r\n--" + boundary + "--\r\n");
+
+    std::string body;
+    chunks.Flatten(body);
+
+    SendStowRequest(peer, mime, body, countInstances);
+    countInstances = 0;
+  }
+}
+
+
+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;
+  }
+
+  OrthancPlugins::DicomWebPeer peer(OrthancPlugins::DicomWebPeers::GetInstance().GetPeer(request->groups[0]));
+
+  std::list<std::string> instances;
+  GetListOfInstances(instances, request);
+
+  {
+    std::string s = ("Sending " + boost::lexical_cast<std::string>(instances.size()) + 
+                     " instances using STOW-RS to DICOMweb server: " + peer.GetUrl());
+    OrthancPluginLogInfo(context_, s.c_str());
+  }
+
+  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;
+  size_t countInstances = 0;
+
+  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("\r\n--" + 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);
+      countInstances ++;
+
+      SendStowChunks(peer, mime, boundary, chunks, countInstances, false);
+    }
+  }
+
+  SendStowChunks(peer, mime, boundary, chunks, countInstances, true);
+
+  std::string answer = "{}\n";
+  OrthancPluginAnswerBuffer(context_, output, answer.c_str(), answer.size(), "application/json");
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Plugin/StowRsClient.h	Thu Apr 28 17:22:02 2016 +0200
@@ -0,0 +1,28 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "Configuration.h"
+
+
+void StowClient(OrthancPluginRestOutput* output,
+                const char* url,
+                const OrthancPluginHttpRequest* request);
--- a/README	Thu Apr 28 17:20:36 2016 +0200
+++ b/README	Thu Apr 28 17:22:02 2016 +0200
@@ -1,5 +1,5 @@
-DICOM Web plugin for Orthanc
-============================
+DICOMweb plugin for Orthanc
+===========================
 
 
 General Information
@@ -10,8 +10,8 @@
 extends the RESTful API of Orthanc with WADO and DICOMweb support.
 
 
-DICOM Web Support
------------------
+DICOMweb Support
+----------------
 
 Currently, a basic support of the following protocols is provided:
 
@@ -41,14 +41,14 @@
 Samples
 -------
 
-Python samples to call the DICOM Web services can be found in the
+Python samples to call the DICOMweb services can be found in the
 "./Samples" folder.
 
 
 Licensing: AGPL
 ---------------
 
-The DICOM Web plugin for Orthanc is licensed under the Affero General
+The DICOMweb plugin for Orthanc is licensed under the Affero General
 Public License (AGPL) license. Pay attention to the fact that this
 license is more restrictive than the license of the Orthanc core.
 
--- a/Resources/Samples/JavaScript/index.html	Thu Apr 28 17:20:36 2016 +0200
+++ b/Resources/Samples/JavaScript/index.html	Thu Apr 28 17:22:02 2016 +0200
@@ -3,11 +3,11 @@
 <html lang="us">
   <head>
     <meta charset="utf-8" />
-    <title>Orthanc DICOM Web Demo</title>
+    <title>Orthanc DICOMweb Demo</title>
   </head>
 
   <body>
-    <h1>Orthanc DICOM Web Demo</h2>
+    <h1>Orthanc DICOMweb Demo</h2>
 
     <h2>STOW-RS - Upload DICOM file</h2>
     <form id="stow">
--- a/Resources/Samples/SendStow.py	Thu Apr 28 17:20:36 2016 +0200
+++ b/Resources/Samples/SendStow.py	Thu Apr 28 17:22:02 2016 +0200
@@ -18,12 +18,17 @@
 # along with this program. If not, see <http://www.gnu.org/licenses/>.
 
 
-import email
+
+# We do not use Python's "email" package, as it uses LF (\n) for line
+# endings instead of CRLF (\r\n) for binary messages, as required by
+# RFC 1341
+# http://stackoverflow.com/questions/3086860/how-do-i-generate-a-multipart-mime-message-with-correct-crlf-in-python
+# https://www.w3.org/Protocols/rfc1341/7_2_Multipart.html
+
 import requests
 import sys
 import json
-from email.mime.multipart import MIMEMultipart
-from email.mime.application import MIMEApplication
+import uuid
 
 if len(sys.argv) < 2:
     print('Usage: %s <StowUri> <file>...' % sys.argv[0])
@@ -33,27 +38,29 @@
 
 URL = sys.argv[1]
 
-related = MIMEMultipart('related')
-related.set_boundary('hello')
+# Create a multipart message whose body contains all the input DICOM files
+boundary = str(uuid.uuid4())  # The boundary is a random UUID
+body = bytearray()
 
 for i in range(2, len(sys.argv)):
     try:
         with open(sys.argv[i], 'rb') as f:
-            dicom = MIMEApplication(f.read(), 'dicom', email.encoders.encode_noop)
-            related.attach(dicom)
+            body += bytearray('--%s\r\n' % boundary, 'ascii')
+            body += bytearray('Content-Type: application/dicom\r\n\r\n', 'ascii')
+            body += f.read()
+            body += bytearray('\r\n', 'ascii')
     except:
         print('Ignoring directory %s' % sys.argv[i])
 
-headers = dict(related.items())
-body = related.as_string()
+# Closing boundary
+body += bytearray('--%s--' % boundary, 'ascii')
 
-# Discard the header
-body = body.split('\n\n', 1)[1]
+# Do the HTTP POST request to the STOW-RS server
+r = requests.post(URL, data=body, headers= {
+    'Content-Type' : 'multipart/related; type=application/dicom; boundary=%s' % boundary,
+    'Accept' : 'application/json',
+})
 
-headers['Content-Type'] = 'multipart/related; type=application/dicom; boundary=%s' % related.get_boundary()
-headers['Accept'] = 'application/json'
-
-r = requests.post(URL, data=body, headers=headers)
 j = json.loads(r.text)
 
 # Loop over the successful instances
@@ -64,4 +71,7 @@
         print(url)
 
 print('\nWADO-RS URL of the study:')
-print(j['00081190']['Value'][0])
+try:
+    print(j['00081190']['Value'][0])
+except:
+    print('No instance was uploaded!')