changeset 3457:9ea218c90057

merge
author Alain Mazy <alain@mazy.be>
date Wed, 03 Jul 2019 10:31:06 +0200
parents ca3ac0f210d6 (current diff) 0013818bf6d4 (diff)
children 4e34fd3e226a fbe22748cd9c
files
diffstat 24 files changed, 637 insertions(+), 170 deletions(-) [+]
line wrap: on
line diff
--- a/Core/DicomNetworking/Internals/CommandDispatcher.cpp	Wed Jul 03 10:28:17 2019 +0200
+++ b/Core/DicomNetworking/Internals/CommandDispatcher.cpp	Wed Jul 03 10:31:06 2019 +0200
@@ -459,7 +459,7 @@
       // The global variable "numberOfDcmAllStorageSOPClassUIDs" is
       // only published if DCMTK >= 3.6.2:
       // https://bitbucket.org/sjodogne/orthanc/issues/137
-      assert(count == numberOfDcmAllStorageSOPClassUIDs);
+      assert(static_cast<int>(count) == numberOfDcmAllStorageSOPClassUIDs);
 #endif
       
       cond = ASC_acceptContextsWithPreferredTransferSyntaxes(
--- a/Core/DicomNetworking/Internals/FindScp.cpp	Wed Jul 03 10:28:17 2019 +0200
+++ b/Core/DicomNetworking/Internals/FindScp.cpp	Wed Jul 03 10:31:06 2019 +0200
@@ -83,6 +83,7 @@
 #include "../../PrecompiledHeaders.h"
 #include "FindScp.h"
 
+#include "../../DicomFormat/DicomArray.h"
 #include "../../DicomParsing/FromDcmtkBridge.h"
 #include "../../DicomParsing/ToDcmtkBridge.h"
 #include "../../Logging.h"
--- a/Core/DicomParsing/DicomWebJsonVisitor.cpp	Wed Jul 03 10:28:17 2019 +0200
+++ b/Core/DicomParsing/DicomWebJsonVisitor.cpp	Wed Jul 03 10:31:06 2019 +0200
@@ -558,9 +558,9 @@
               tokens.size() > 1 &&
               tokens[0].empty())
           {
-            std::string s = tokens[1];
-            tokens.clear();
-            tokens.push_back(s);
+            // Specific character set with code extension: Remove the
+            // first element from the vector of encodings
+            tokens.erase(tokens.begin());
           }
 
           node[KEY_VALUE] = Json::arrayValue;
--- a/Core/DicomParsing/FromDcmtkBridge.cpp	Wed Jul 03 10:28:17 2019 +0200
+++ b/Core/DicomParsing/FromDcmtkBridge.cpp	Wed Jul 03 10:31:06 2019 +0200
@@ -2312,8 +2312,15 @@
 
       if (c != NULL)  // This case corresponds to the empty string
       {
-        std::string s(c);
-        utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions);
+        if (element.getTag() == DCM_SpecificCharacterSet)
+        {
+          utf8.assign(c);
+        }
+        else
+        {
+          std::string s(c);
+          utf8 = Toolbox::ConvertToUtf8(s, encoding, hasCodeExtensions);
+        }
       }
 
       std::string newValue;
--- a/Core/DicomParsing/ParsedDicomFile.cpp	Wed Jul 03 10:28:17 2019 +0200
+++ b/Core/DicomParsing/ParsedDicomFile.cpp	Wed Jul 03 10:31:06 2019 +0200
@@ -915,12 +915,23 @@
   {
     std::string patientId, studyUid, seriesUid, instanceUid;
 
-    if (!GetTagValue(patientId, DICOM_TAG_PATIENT_ID) ||
-        !GetTagValue(studyUid, DICOM_TAG_STUDY_INSTANCE_UID) ||
+    if (!GetTagValue(patientId, DICOM_TAG_PATIENT_ID))
+    {
+      /**
+       * If "PatientID" is absent, be tolerant by considering it
+       * equals the empty string, then proceed. In Orthanc <= 1.5.6,
+       * an exception "Bad file format" was generated.
+       * https://groups.google.com/d/msg/orthanc-users/aphG_h1AHVg/rfOTtTPTAgAJ
+       * https://bitbucket.org/sjodogne/orthanc/commits/4c45e018bd3de3cfa21d6efc6734673aaaee4435
+       **/
+      patientId.clear();
+    }        
+    
+    if (!GetTagValue(studyUid, DICOM_TAG_STUDY_INSTANCE_UID) ||
         !GetTagValue(seriesUid, DICOM_TAG_SERIES_INSTANCE_UID) ||
         !GetTagValue(instanceUid, DICOM_TAG_SOP_INSTANCE_UID))
     {
-      throw OrthancException(ErrorCode_BadFileFormat, "missing PatientID, StudyInstanceUID, SeriesInstanceUID or SOPInstanceUID");
+      throw OrthancException(ErrorCode_BadFileFormat, "missing StudyInstanceUID, SeriesInstanceUID or SOPInstanceUID");
     }
 
     return DicomInstanceHasher(patientId, studyUid, seriesUid, instanceUid);
--- a/Core/HttpServer/HttpServer.cpp	Wed Jul 03 10:28:17 2019 +0200
+++ b/Core/HttpServer/HttpServer.cpp	Wed Jul 03 10:31:06 2019 +0200
@@ -483,6 +483,7 @@
 
     //chunkStore.Print();
 
+    // TODO - Refactor using class "MultipartStreamReader"
     try
     {
       FindIterator last;
@@ -841,6 +842,10 @@
           ct->second.size() >= MULTIPART_FORM_LENGTH &&
           !memcmp(ct->second.c_str(), MULTIPART_FORM, MULTIPART_FORM_LENGTH))
       {
+        /** 
+         * The user uses the "upload" form of Orthanc Explorer, for
+         * file uploads through a HTML form.
+         **/
         status = ParseMultipartForm(body, connection, headers, ct->second, server.GetChunkStore());
         isMultipartForm = true;
       }
--- a/Core/HttpServer/HttpToolbox.cpp	Wed Jul 03 10:28:17 2019 +0200
+++ b/Core/HttpServer/HttpToolbox.cpp	Wed Jul 03 10:31:06 2019 +0200
@@ -225,25 +225,15 @@
   }
 
 
-  bool HttpToolbox::SimpleGet(std::string& result,
-                              IHttpHandler& handler,
-                              RequestOrigin origin,
-                              const std::string& uri)
-  {
-    IHttpHandler::Arguments headers;  // No HTTP header
-    return SimpleGet(result, handler, origin, uri, headers);
-  }
-
-
   static bool SimplePostOrPut(std::string& result,
                               IHttpHandler& handler,
                               RequestOrigin origin,
                               HttpMethod method,
                               const std::string& uri,
                               const void* bodyData,
-                              size_t bodySize)
+                              size_t bodySize,
+                              const IHttpHandler::Arguments& httpHeaders)
   {
-    IHttpHandler::Arguments headers;  // No HTTP header
     IHttpHandler::GetArguments getArguments;  // No GET argument for POST/PUT
 
     UriComponents curi;
@@ -253,7 +243,7 @@
     HttpOutput http(stream, false /* no keep alive */);
 
     if (handler.Handle(http, origin, LOCALHOST, "", method, curi, 
-                       headers, getArguments, bodyData, bodySize))
+                       httpHeaders, getArguments, bodyData, bodySize))
     {
       stream.GetOutput(result);
       return true;
@@ -270,9 +260,10 @@
                                RequestOrigin origin,
                                const std::string& uri,
                                const void* bodyData,
-                               size_t bodySize)
+                               size_t bodySize,
+                               const IHttpHandler::Arguments& httpHeaders)
   {
-    return SimplePostOrPut(result, handler, origin, HttpMethod_Post, uri, bodyData, bodySize);
+    return SimplePostOrPut(result, handler, origin, HttpMethod_Post, uri, bodyData, bodySize, httpHeaders);
   }
 
 
@@ -281,26 +272,27 @@
                               RequestOrigin origin,
                               const std::string& uri,
                               const void* bodyData,
-                              size_t bodySize)
+                              size_t bodySize,
+                              const IHttpHandler::Arguments& httpHeaders)
   {
-    return SimplePostOrPut(result, handler, origin, HttpMethod_Put, uri, bodyData, bodySize);
+    return SimplePostOrPut(result, handler, origin, HttpMethod_Put, uri, bodyData, bodySize, httpHeaders);
   }
 
 
   bool HttpToolbox::SimpleDelete(IHttpHandler& handler,
                                  RequestOrigin origin,
-                                 const std::string& uri)
+                                 const std::string& uri,
+                                 const IHttpHandler::Arguments& httpHeaders)
   {
     UriComponents curi;
     Toolbox::SplitUriComponents(curi, uri);
 
-    IHttpHandler::Arguments headers;  // No HTTP header
     IHttpHandler::GetArguments getArguments;  // No GET argument for DELETE
 
     StringHttpOutput stream;
     HttpOutput http(stream, false /* no keep alive */);
 
     return handler.Handle(http, origin, LOCALHOST, "", HttpMethod_Delete, curi, 
-                          headers, getArguments, NULL /* no body for DELETE */, 0);
+                          httpHeaders, getArguments, NULL /* no body for DELETE */, 0);
   }
 }
--- a/Core/HttpServer/HttpToolbox.h	Wed Jul 03 10:28:17 2019 +0200
+++ b/Core/HttpServer/HttpToolbox.h	Wed Jul 03 10:31:06 2019 +0200
@@ -64,11 +64,6 @@
     static bool SimpleGet(std::string& result,
                           IHttpHandler& handler,
                           RequestOrigin origin,
-                          const std::string& uri);
-
-    static bool SimpleGet(std::string& result,
-                          IHttpHandler& handler,
-                          RequestOrigin origin,
                           const std::string& uri,
                           const IHttpHandler::Arguments& httpHeaders);
 
@@ -77,17 +72,20 @@
                            RequestOrigin origin,
                            const std::string& uri,
                            const void* bodyData,
-                           size_t bodySize);
+                           size_t bodySize,
+                          const IHttpHandler::Arguments& httpHeaders);
 
     static bool SimplePut(std::string& result,
                           IHttpHandler& handler,
                           RequestOrigin origin,
                           const std::string& uri,
                           const void* bodyData,
-                          size_t bodySize);
+                          size_t bodySize,
+                          const IHttpHandler::Arguments& httpHeaders);
 
     static bool SimpleDelete(IHttpHandler& handler,
                              RequestOrigin origin,
-                             const std::string& uri);
+                             const std::string& uri,
+                             const IHttpHandler::Arguments& httpHeaders);
   };
 }
--- a/Core/HttpServer/MultipartStreamReader.cpp	Wed Jul 03 10:28:17 2019 +0200
+++ b/Core/HttpServer/MultipartStreamReader.cpp	Wed Jul 03 10:31:06 2019 +0200
@@ -175,7 +175,7 @@
       HttpHeaders headers;
       ParseHeaders(headers, start, headersMatcher_.GetMatchBegin());
 
-      size_t contentLength;
+      size_t contentLength = 0;
       if (!LookupHeaderSizeValue(contentLength, headers, "content-length"))
       {
         if (boundaryMatcher_.Apply(headersMatcher_.GetMatchEnd(), corpusEnd))
--- a/Core/Lua/LuaContext.cpp	Wed Jul 03 10:28:17 2019 +0200
+++ b/Core/Lua/LuaContext.cpp	Wed Jul 03 10:31:06 2019 +0200
@@ -36,6 +36,7 @@
 
 #include "../Logging.h"
 #include "../OrthancException.h"
+#include "../Toolbox.h"
 
 #include <set>
 #include <cassert>
@@ -156,7 +157,7 @@
     }
 
     Json::Value json;
-    that.GetJson(json, 1, keepStrings);
+    that.GetJson(json, state, 1, keepStrings);
 
     Json::FastWriter writer;
     std::string s = writer.write(json);
@@ -208,27 +209,21 @@
 
     return true;
   }
-
-  void LuaContext::SetHttpHeaders(lua_State *state, int top)
-  {
-    this->httpClient_.ClearHeaders(); // always reset headers in case they have been set in a previous request
-
-    if (lua_gettop(state) >= top)
-    {
-      Json::Value headers;
-      this->GetJson(headers, top, true);
+  
 
-      Json::Value::Members members = headers.getMemberNames();
+  void LuaContext::SetHttpHeaders(int top)
+  {
+    std::map<std::string, std::string> headers;
+    GetDictionaryArgument(headers, lua_, top, false /* keep key case as provided by Lua script */);
+      
+    httpClient_.ClearHeaders(); // always reset headers in case they have been set in a previous request
 
-      for (Json::Value::Members::const_iterator 
-           it = members.begin(); it != members.end(); ++it)
-      {
-        this->httpClient_.AddHeader(*it, headers[*it].asString());
-      }
+    for (std::map<std::string, std::string>::const_iterator
+           it = headers.begin(); it != headers.end(); ++it)
+    {
+      httpClient_.AddHeader(it->first, it->second);
     }
-
-  }
-  
+  }  
 
 
   int LuaContext::CallHttpGet(lua_State *state)
@@ -237,8 +232,8 @@
 
     // Check the types of the arguments
     int nArgs = lua_gettop(state);
-    if ((nArgs < 1 || nArgs > 2) ||         // check args count
-       !lua_isstring(state, 1))             // URL is a string
+    if (nArgs < 1 || nArgs > 2 ||         // check args count
+       !lua_isstring(state, 1))           // URL is a string
     {
       LOG(ERROR) << "Lua: Bad parameters to HttpGet()";
       lua_pushnil(state);
@@ -250,7 +245,7 @@
     that.httpClient_.SetMethod(HttpMethod_Get);
     that.httpClient_.SetUrl(url);
     that.httpClient_.GetBody().clear();
-    that.SetHttpHeaders(state, 2);
+    that.SetHttpHeaders(2);
 
     // Do the HTTP GET request
     if (!that.AnswerHttpQuery(state))
@@ -283,7 +278,7 @@
     const char* url = lua_tostring(state, 1);
     that.httpClient_.SetMethod(method);
     that.httpClient_.SetUrl(url);
-    that.SetHttpHeaders(state, 3);
+    that.SetHttpHeaders(3);
 
     if (nArgs >= 2 && !lua_isnil(state, 2))
     {
@@ -345,7 +340,7 @@
     that.httpClient_.SetMethod(HttpMethod_Delete);
     that.httpClient_.SetUrl(url);
     that.httpClient_.GetBody().clear();
-    that.SetHttpHeaders(state, 2);
+    that.SetHttpHeaders(2);
 
     // Do the HTTP DELETE request
     std::string s;
@@ -434,10 +429,11 @@
 
 
   void LuaContext::GetJson(Json::Value& result,
+                           lua_State* state,
                            int top,
                            bool keepStrings)
   {
-    if (lua_istable(lua_, top))
+    if (lua_istable(state, top))
     {
       Json::Value tmp = Json::objectValue;
       bool isArray = true;
@@ -448,19 +444,19 @@
       // Push another reference to the table on top of the stack (so we know
       // where it is, and this function can work for negative, positive and
       // pseudo indices
-      lua_pushvalue(lua_, top);
+      lua_pushvalue(state, top);
       // stack now contains: -1 => table
-      lua_pushnil(lua_);
+      lua_pushnil(state);
       // stack now contains: -1 => nil; -2 => table
-      while (lua_next(lua_, -2))
+      while (lua_next(state, -2))
       {
         // stack now contains: -1 => value; -2 => key; -3 => table
         // copy the key so that lua_tostring does not modify the original
-        lua_pushvalue(lua_, -2);
+        lua_pushvalue(state, -2);
         // stack now contains: -1 => key; -2 => value; -3 => key; -4 => table
-        std::string key(lua_tostring(lua_, -1));
+        std::string key(lua_tostring(state, -1));
         Json::Value v;
-        GetJson(v, -2, keepStrings);
+        GetJson(v, state, -2, keepStrings);
 
         tmp[key] = v;
 
@@ -479,13 +475,13 @@
         }
         
         // pop value + copy of key, leaving original key
-        lua_pop(lua_, 2);
+        lua_pop(state, 2);
         // stack now contains: -1 => key; -2 => table
       }
       // stack now contains: -1 => table (when lua_next returns 0 it pops the key
       // but does not push anything.)
       // Pop table
-      lua_pop(lua_, 1);
+      lua_pop(state, 1);
 
       // Stack is now the same as it was on entry to this function
 
@@ -502,20 +498,20 @@
         result = tmp;
       }
     }
-    else if (lua_isnil(lua_, top))
+    else if (lua_isnil(state, top))
     {
       result = Json::nullValue;
     }
     else if (!keepStrings &&
-             lua_isboolean(lua_, top))
+             lua_isboolean(state, top))
     {
-      result = lua_toboolean(lua_, top) ? true : false;
+      result = lua_toboolean(state, top) ? true : false;
     }
     else if (!keepStrings &&
-             lua_isnumber(lua_, top))
+             lua_isnumber(state, top))
     {
       // Convert to "int" if truncation does not loose precision
-      double value = static_cast<double>(lua_tonumber(lua_, top));
+      double value = static_cast<double>(lua_tonumber(state, top));
       int truncated = static_cast<int>(value);
 
       if (std::abs(value - static_cast<double>(truncated)) <= 
@@ -528,15 +524,15 @@
         result = value;
       }
     }
-    else if (lua_isstring(lua_, top))
+    else if (lua_isstring(state, top))
     {
       // Caution: The "lua_isstring()" case must be the last, since
       // Lua can convert most types to strings by default.
-      result = std::string(lua_tostring(lua_, top));
+      result = std::string(lua_tostring(state, top));
     }
-    else if (lua_isboolean(lua_, top))
+    else if (lua_isboolean(state, top))
     {
-      result = lua_toboolean(lua_, top) ? true : false;
+      result = lua_toboolean(state, top) ? true : false;
     }
     else
     {
@@ -653,4 +649,33 @@
     lua_pop(state, 1);
     return value;
   }
+
+
+  void LuaContext::GetDictionaryArgument(std::map<std::string, std::string>& target,
+                                         lua_State* state,
+                                         int top,
+                                         bool keyToLowerCase)
+  {
+    target.clear();
+
+    if (lua_gettop(state) >= top)
+    {
+      Json::Value headers;
+      GetJson(headers, state, top, true);
+
+      Json::Value::Members members = headers.getMemberNames();
+
+      for (size_t i = 0; i < members.size(); i++)
+      {
+        std::string key = members[i];
+
+        if (keyToLowerCase)
+        {
+          Toolbox::ToLowerCase(key);
+        }
+        
+        target[key] = headers[members[i]].asString();
+      }
+    }
+  }
 }
--- a/Core/Lua/LuaContext.h	Wed Jul 03 10:28:17 2019 +0200
+++ b/Core/Lua/LuaContext.h	Wed Jul 03 10:31:06 2019 +0200
@@ -87,11 +87,12 @@
     void ExecuteInternal(std::string* output,
                          const std::string& command);
 
-    void GetJson(Json::Value& result,
-                 int top,
-                 bool keepStrings);
+    static void GetJson(Json::Value& result,
+                        lua_State* state,
+                        int top,
+                        bool keepStrings);
 
-    void SetHttpHeaders(lua_State* state, int top);
+    void SetHttpHeaders(int top);
     
   public:
     LuaContext();
@@ -136,5 +137,10 @@
                                          const char* name);
 
     void PushJson(const Json::Value& value);
+
+    static void GetDictionaryArgument(std::map<std::string, std::string>& target,
+                                      lua_State* state,
+                                      int top,
+                                      bool keyToLowerCase);
   };
 }
--- a/Core/Lua/LuaFunctionCall.cpp	Wed Jul 03 10:28:17 2019 +0200
+++ b/Core/Lua/LuaFunctionCall.cpp	Wed Jul 03 10:31:06 2019 +0200
@@ -134,7 +134,7 @@
                                       bool keepStrings)
   {
     ExecuteInternal(1);
-    context_.GetJson(result, lua_gettop(context_.lua_), keepStrings);
+    context_.GetJson(result, context_.lua_, lua_gettop(context_.lua_), keepStrings);
   }
 
 
--- a/Core/WebServiceParameters.cpp	Wed Jul 03 10:28:17 2019 +0200
+++ b/Core/WebServiceParameters.cpp	Wed Jul 03 10:31:06 2019 +0200
@@ -289,13 +289,22 @@
     {
       if (!IsReservedKey(*it))
       {
-        if (peer[*it].type() != Json::stringValue)
+        switch (peer[*it].type())
         {
-          throw OrthancException(ErrorCode_BadFileFormat);
-        }
-        else
-        {
-          userProperties_[*it] = peer[*it].asString();
+          case Json::stringValue:
+            userProperties_[*it] = peer[*it].asString();
+            break;
+
+          case Json::booleanValue:
+            userProperties_[*it] = peer[*it].asBool() ? "1" : "0";
+            break;
+
+          case Json::intValue:
+            userProperties_[*it] = boost::lexical_cast<std::string>(peer[*it].asInt());
+            break;
+
+          default:
+            throw OrthancException(ErrorCode_BadFileFormat);
         }
       }
     }
@@ -402,6 +411,33 @@
       return true;
     }
   }
+  
+
+  bool WebServiceParameters::GetBooleanUserProperty(const std::string& key,
+                                                    bool defaultValue) const
+  {
+    Dictionary::const_iterator found = userProperties_.find(key);
+
+    if (found == userProperties_.end())
+    {
+      return defaultValue;
+    }
+    else if (found->second == "0" ||
+             found->second == "false")
+    {
+      return false;
+    }
+    else if (found->second == "1" ||
+             found->second == "true")
+    {
+      return true;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Bad value for a Boolean user property in the parameters "
+                             "of a Web service: Property \"" + key + "\" equals: " + found->second);
+    }    
+  }
 
 
   bool WebServiceParameters::IsAdvancedFormatNeeded() const
--- a/Core/WebServiceParameters.h	Wed Jul 03 10:28:17 2019 +0200
+++ b/Core/WebServiceParameters.h	Wed Jul 03 10:31:06 2019 +0200
@@ -162,7 +162,10 @@
     void ListUserProperties(std::set<std::string>& target) const; 
 
     bool LookupUserProperty(std::string& value,
-                            const std::string& key) const; 
+                            const std::string& key) const;
+
+    bool GetBooleanUserProperty(const std::string& key,
+                                bool defaultValue) const;
 
     bool IsAdvancedFormatNeeded() const;
 
--- a/NEWS	Wed Jul 03 10:28:17 2019 +0200
+++ b/NEWS	Wed Jul 03 10:31:06 2019 +0200
@@ -2,9 +2,14 @@
 ===============================
 
 
+
+Version 1.5.7 (2019-06-25)
+==========================
+
 REST API
 --------
 
+* API version has been upgraded to 3
 * "/modalities/{id}/query": New argument "Normalize" can be set to "false"
   to bypass the automated correction of outgoing C-FIND queries
 * Reporting of "ParentResources" in "DicomModalityStore" and "DicomModalityStore" jobs
@@ -26,9 +31,12 @@
 * Allow the serialization of signed 16bpp images in PAM format
 * HTTP header "Accept-Encoding" is honored for streams without built-in support for compression
 * The default HTTP timeout is now 60 seconds (instead of 10 seconds in previous versions)
+* Allow anonymizing/modifying instances without the PatientID tag
+* Fix issue #106 (Unable to export preview as jpeg from Lua script)
 * Fix issue #136 (C-FIND request fails when found DICOM file does not have certain tags)
 * Fix issue #137 (C-STORE fails for unknown SOP Class although server is configured to accept any)
 * Fix issue #138 (POST to modalities/{name} accepts invalid characters)
+* Fix issue #141 (/tools/create-dicom removes non-ASCII characters from study description)
 
 
 Version 1.5.6 (2019-03-01)
--- a/OrthancServer/LuaScripting.cpp	Wed Jul 03 10:28:17 2019 +0200
+++ b/OrthancServer/LuaScripting.cpp	Wed Jul 03 10:31:06 2019 +0200
@@ -279,9 +279,9 @@
 
     // Check the types of the arguments
     int nArgs = lua_gettop(state);
-    if ((nArgs != 1 && nArgs != 2) || 
+    if (nArgs < 1 || nArgs > 3 || 
         !lua_isstring(state, 1) ||                 // URI
-        (nArgs == 2 && !lua_isboolean(state, 2)))  // Restrict to built-in API?
+        (nArgs >= 2 && !lua_isboolean(state, 2)))  // Restrict to built-in API?
     {
       LOG(ERROR) << "Lua: Bad parameters to RestApiGet()";
       lua_pushnil(state);
@@ -291,11 +291,14 @@
     const char* uri = lua_tostring(state, 1);
     bool builtin = (nArgs == 2 ? lua_toboolean(state, 2) != 0 : false);
 
+    std::map<std::string, std::string> headers;
+    LuaContext::GetDictionaryArgument(headers, state, 3, true /* HTTP header key to lower case */);
+
     try
     {
       std::string result;
       if (HttpToolbox::SimpleGet(result, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), 
-                                 RequestOrigin_Lua, uri))
+                                 RequestOrigin_Lua, uri, headers))
       {
         lua_pushlstring(state, result.c_str(), result.size());
         return 1;
@@ -325,10 +328,10 @@
 
     // Check the types of the arguments
     int nArgs = lua_gettop(state);
-    if ((nArgs != 2 && nArgs != 3) || 
+    if (nArgs < 2 || nArgs > 4 || 
         !lua_isstring(state, 1) ||                 // URI
         !lua_isstring(state, 2) ||                 // Body
-        (nArgs == 3 && !lua_isboolean(state, 3)))  // Restrict to built-in API?
+        (nArgs >= 3 && !lua_isboolean(state, 3)))  // Restrict to built-in API?
     {
       LOG(ERROR) << "Lua: Bad parameters to " << (isPost ? "RestApiPost()" : "RestApiPut()");
       lua_pushnil(state);
@@ -340,14 +343,17 @@
     const char* bodyData = lua_tolstring(state, 2, &bodySize);
     bool builtin = (nArgs == 3 ? lua_toboolean(state, 3) != 0 : false);
 
+    std::map<std::string, std::string> headers;
+    LuaContext::GetDictionaryArgument(headers, state, 4, true /* HTTP header key to lower case */);
+        
     try
     {
       std::string result;
       if (isPost ?
           HttpToolbox::SimplePost(result, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), 
-                                  RequestOrigin_Lua, uri, bodyData, bodySize) :
+                                  RequestOrigin_Lua, uri, bodyData, bodySize, headers) :
           HttpToolbox::SimplePut(result, serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), 
-                                 RequestOrigin_Lua, uri, bodyData, bodySize))
+                                 RequestOrigin_Lua, uri, bodyData, bodySize, headers))
       {
         lua_pushlstring(state, result.c_str(), result.size());
         return 1;
@@ -391,9 +397,9 @@
 
     // Check the types of the arguments
     int nArgs = lua_gettop(state);
-    if ((nArgs != 1 && nArgs != 2) || 
+    if (nArgs < 1 || nArgs > 3 ||
         !lua_isstring(state, 1) ||                 // URI
-        (nArgs == 2 && !lua_isboolean(state, 2)))  // Restrict to built-in API?
+        (nArgs >= 2 && !lua_isboolean(state, 2)))  // Restrict to built-in API?
     {
       LOG(ERROR) << "Lua: Bad parameters to RestApiDelete()";
       lua_pushnil(state);
@@ -403,10 +409,13 @@
     const char* uri = lua_tostring(state, 1);
     bool builtin = (nArgs == 2 ? lua_toboolean(state, 2) != 0 : false);
 
+    std::map<std::string, std::string> headers;
+    LuaContext::GetDictionaryArgument(headers, state, 3, true /* HTTP header key to lower case */);
+    
     try
     {
       if (HttpToolbox::SimpleDelete(serverContext->GetHttpHandler().RestrictToOrthancRestApi(builtin), 
-                                    RequestOrigin_Lua, uri))
+                                    RequestOrigin_Lua, uri, headers))
       {
         lua_pushboolean(state, 1);
         return 1;
--- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Wed Jul 03 10:28:17 2019 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Wed Jul 03 10:31:06 2019 +0200
@@ -518,11 +518,8 @@
           }
           else if (tag["Type"] == "String")
           {
-            std::string value = tag["Value"].asString();
-
-            bool hasCodeExtensions;
-            Encoding encoding = dicom.DetectEncoding(hasCodeExtensions);
-            dicom.ReplacePlainString(*it, Toolbox::ConvertFromUtf8(value, encoding));
+            std::string value = tag["Value"].asString();  // This is an UTF-8 value (as it comes from JSON)
+            dicom.ReplacePlainString(*it, value);
           }
         }
       }
--- a/Plugins/Engine/OrthancPlugins.cpp	Wed Jul 03 10:28:17 2019 +0200
+++ b/Plugins/Engine/OrthancPlugins.cpp	Wed Jul 03 10:31:06 2019 +0200
@@ -1948,8 +1948,10 @@
       handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins);
     }
 
+    std::map<std::string, std::string> httpHeaders;
+
     std::string result;
-    if (HttpToolbox::SimpleGet(result, *handler, RequestOrigin_Plugins, p.uri))
+    if (HttpToolbox::SimpleGet(result, *handler, RequestOrigin_Plugins, p.uri, httpHeaders))
     {
       CopyToMemoryBuffer(*p.target, result);
     }
@@ -2013,10 +2015,12 @@
       handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins);
     }
       
+    std::map<std::string, std::string> httpHeaders;
+
     std::string result;
     if (isPost ? 
-        HttpToolbox::SimplePost(result, *handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize) :
-        HttpToolbox::SimplePut (result, *handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize))
+        HttpToolbox::SimplePost(result, *handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize, httpHeaders) :
+        HttpToolbox::SimplePut (result, *handler, RequestOrigin_Plugins, p.uri, p.body, p.bodySize, httpHeaders))
     {
       CopyToMemoryBuffer(*p.target, result);
     }
@@ -2041,7 +2045,9 @@
       handler = &lock.GetContext().GetHttpHandler().RestrictToOrthancRestApi(!afterPlugins);
     }
       
-    if (!HttpToolbox::SimpleDelete(*handler, RequestOrigin_Plugins, uri))
+    std::map<std::string, std::string> httpHeaders;
+
+    if (!HttpToolbox::SimpleDelete(*handler, RequestOrigin_Plugins, uri, httpHeaders))
     {
       throw OrthancException(ErrorCode_UnknownResource);
     }
--- a/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Jul 03 10:28:17 2019 +0200
+++ b/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Jul 03 10:31:06 2019 +0200
@@ -25,6 +25,7 @@
  *    - Possibly register a callback to filter incoming HTTP requests using OrthancPluginRegisterIncomingHttpRequestFilter2().
  *    - Possibly register a callback to unserialize jobs using OrthancPluginRegisterJobsUnserializer().
  *    - Possibly register a callback to refresh its metrics using OrthancPluginRegisterRefreshMetricsCallback().
+ *    - Possibly register a callback to answer chunked HTTP transfers using ::OrthancPluginRegisterChunkedRestCallback().
  * -# <tt>void OrthancPluginFinalize()</tt>:
  *    This function is invoked by Orthanc during its shutdown. The plugin
  *    must free all its memory.
@@ -4055,6 +4056,7 @@
    * @param username The username (can be <tt>NULL</tt> if no password protection).
    * @param password The password (can be <tt>NULL</tt> if no password protection).
    * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpGet(
     OrthancPluginContext*       context,
@@ -4092,6 +4094,7 @@
    * @param username The username (can be <tt>NULL</tt> if no password protection).
    * @param password The password (can be <tt>NULL</tt> if no password protection).
    * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpPost(
     OrthancPluginContext*       context,
@@ -4133,6 +4136,7 @@
    * @param username The username (can be <tt>NULL</tt> if no password protection).
    * @param password The password (can be <tt>NULL</tt> if no password protection).
    * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpPut(
     OrthancPluginContext*       context,
@@ -4170,6 +4174,7 @@
    * @param username The username (can be <tt>NULL</tt> if no password protection).
    * @param password The password (can be <tt>NULL</tt> if no password protection).
    * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpDelete(
     OrthancPluginContext*       context,
@@ -5539,6 +5544,7 @@
    * @param pkcs11 Enable PKCS#11 client authentication for hardware security modules and smart cards.
    * @return 0 if success, or the error code if failure.
    * @see OrthancPluginCallPeerApi()
+   * @ingroup Toolbox
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpClient(
     OrthancPluginContext*       context,
@@ -6801,36 +6807,114 @@
   
 
 
-
-
-
-
-
-
-
-
-
-
-
-
-  typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientAnswerAddHeader) (void* answer,
-                                                                             const char* key,
-                                                                             const char* value);
-
-  typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientAnswerAddChunk) (void* answer,
-                                                                            const void* data,
-                                                                            uint32_t size);
-
+  /**
+   * @brief Callback executed when a HTTP header is received during a chunked transfer.
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP client during a chunked HTTP transfer, as soon as it
+   * receives one HTTP header from the answer of the remote HTTP
+   * server.
+   *
+   * @see OrthancPluginChunkedHttpClient()
+   * @param answer The user payload, as provided by the calling plugin.
+   * @param key The key of the HTTP header.
+   * @param value The value of the HTTP header.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientAnswerAddHeader) (
+    void* answer,
+    const char* key,
+    const char* value);
+
+
+  /**
+   * @brief Callback executed when an answer chunk is received during a chunked transfer.
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP client during a chunked HTTP transfer, as soon as it
+   * receives one data chunk from the answer of the remote HTTP
+   * server.
+   *
+   * @see OrthancPluginChunkedHttpClient()
+   * @param answer The user payload, as provided by the calling plugin.
+   * @param data The content of the data chunk.
+   * @param size The size of the data chunk.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientAnswerAddChunk) (
+    void* answer,
+    const void* data,
+    uint32_t size);
+  
+
+  /**
+   * @brief Callback to know whether the request body is entirely read during a chunked transfer 
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP client during a chunked HTTP transfer, while reading
+   * the body of a POST or PUT request. The plugin must answer "1" as
+   * soon as the body is entirely read: The "request" data structure
+   * must act as an iterator.
+   *
+   * @see OrthancPluginChunkedHttpClient()
+   * @param request The user payload, as provided by the calling plugin.
+   * @return "1" if the body is over, or "0" if there is still data to be read.
+   * @ingroup Toolbox
+   **/
   typedef uint8_t (*OrthancPluginChunkedClientRequestIsDone) (void* request);
 
+
+  /**
+   * @brief Callback to advance in the request body during a chunked transfer 
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP client during a chunked HTTP transfer, while reading
+   * the body of a POST or PUT request. This function asks the plugin
+   * to advance to the next chunk of data of the request body: The
+   * "request" data structure must act as an iterator.
+   *
+   * @see OrthancPluginChunkedHttpClient()
+   * @param request The user payload, as provided by the calling plugin.
+   * @return 0 if success, or the error code if failure.
+   * @ingroup Toolbox
+   **/
   typedef OrthancPluginErrorCode (*OrthancPluginChunkedClientRequestNext) (void* request);
 
+
+  /**
+   * @brief Callback to read the current chunk of the request body during a chunked transfer 
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP client during a chunked HTTP transfer, while reading
+   * the body of a POST or PUT request. The plugin must provide the
+   * content of the current chunk of data of the request body.
+   *
+   * @see OrthancPluginChunkedHttpClient()
+   * @param request The user payload, as provided by the calling plugin.
+   * @return The content of the current request chunk.
+   * @ingroup Toolbox
+   **/
   typedef const void* (*OrthancPluginChunkedClientRequestGetChunkData) (void* request);
 
+
+  /**
+   * @brief Callback to read the size of the current request chunk during a chunked transfer 
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP client during a chunked HTTP transfer, while reading
+   * the body of a POST or PUT request. The plugin must provide the
+   * size of the current chunk of data of the request body.
+   *
+   * @see OrthancPluginChunkedHttpClient()
+   * @param request The user payload, as provided by the calling plugin.
+   * @return The size of the current request chunk.
+   * @ingroup Toolbox
+   **/
   typedef uint32_t (*OrthancPluginChunkedClientRequestGetChunkSize) (void* request);
 
   
-
   typedef struct
   {
     void*                                          answer;
@@ -6856,6 +6940,57 @@
     uint8_t                                        pkcs11;
   } _OrthancPluginChunkedHttpClient;
 
+  
+  /**
+   * @brief Issue a HTTP call, using chunked HTTP transfers.
+   * 
+   * Make a HTTP call to the given URL using chunked HTTP
+   * transfers. The request body is provided as an iterator over data
+   * chunks. The answer is provided as a sequence of function calls
+   * with the individual HTTP headers and answer chunks.
+   * 
+   * Contrarily to OrthancPluginHttpClient() that entirely stores the
+   * request body and the answer body in memory buffers, this function
+   * uses chunked HTTP transfers. This results in a lower memory
+   * consumption. Pay attention to the fact that Orthanc servers with
+   * version <= 1.5.6 do not support chunked transfers: You must use
+   * OrthancPluginHttpClient() if contacting such older servers.
+   *
+   * The HTTP request will be done accordingly to the global
+   * configuration of Orthanc (in particular, the options "HttpProxy",
+   * "HttpTimeout", "HttpsVerifyPeers", "HttpsCACertificates", and
+   * "Pkcs11" will be taken into account).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param answer The user payload for the answer body. It will be provided to the callbacks for the answer.
+   * @param answerAddChunk Callback function to report a data chunk from the answer body.
+   * @param answerAddHeader Callback function to report an HTTP header sent by the remote server.
+   * @param httpStatus The HTTP status after the execution of the request (out argument).
+   * @param method HTTP method to be used.
+   * @param url The URL of interest.
+   * @param headersCount The number of HTTP headers.
+   * @param headersKeys Array containing the keys of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param headersValues Array containing the values of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param request The user payload containing the request body, and acting as an iterator.
+   * It will be provided to the callbacks for the request.
+   * @param requestIsDone Callback function to tell whether the request body is entirely read.
+   * @param requestChunkData Callback function to get the content of the current data chunk of the request body.
+   * @param requestChunkSize Callback function to get the size of the current data chunk of the request body.
+   * @param requestNext Callback function to advance to the next data chunk of the request body.
+   * @param username The username (can be <tt>NULL</tt> if no password protection).
+   * @param password The password (can be <tt>NULL</tt> if no password protection).
+   * @param timeout Timeout in seconds (0 for default timeout).
+   * @param certificateFile Path to the client certificate for HTTPS, in PEM format
+   * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS).
+   * @param certificateKeyFile Path to the key of the client certificate for HTTPS, in PEM format
+   * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS).
+   * @param certificateKeyPassword Password to unlock the key of the client certificate 
+   * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS).
+   * @param pkcs11 Enable PKCS#11 client authentication for hardware security modules and smart cards.
+   * @return 0 if success, or the error code if failure.
+   * @see OrthancPluginHttpClient()
+   * @ingroup Toolbox
+   **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginChunkedHttpClient(
     OrthancPluginContext*                          context,
     void*                                          answer,
@@ -6913,23 +7048,86 @@
 
 
 
+  /**
+   * @brief Opaque structure that reads the content of a HTTP request body during a chunked HTTP transfer.
+   * @ingroup Callback
+   **/
   typedef struct _OrthancPluginServerChunkedRequestReader_t OrthancPluginServerChunkedRequestReader;
 
-  /* POST and PUT must share the same reader */
+
+
+  /**
+   * @brief Callback to create a reader to handle incoming chunked HTTP transfers.
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP server that supports chunked HTTP transfers. This
+   * callback is only invoked if the HTTP method is POST or PUT. The
+   * callback must create an user-specific "reader" object that will
+   * be fed with the body of the incoming body.
+   * 
+   * @see OrthancPluginRegisterChunkedRestCallback()
+   * @param reader Memory location that must be filled with the newly-created reader.
+   * @param url The URI that is accessed.
+   * @param request The body of the HTTP request. Note that "body" and "bodySize" are not used.
+   * @return 0 if success, or the error code if failure.
+   **/
   typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderFactory) (
-    OrthancPluginServerChunkedRequestReader**  reader, /* out, for POST/PUT only */
+    OrthancPluginServerChunkedRequestReader**  reader,
     const char*                                url,
-    const OrthancPluginHttpRequest*            request); /* body and bodySize are not used */
-
+    const OrthancPluginHttpRequest*            request);
+
+  
+  /**
+   * @brief Callback invoked whenever a new data chunk is available during a chunked transfer.
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP server that supports chunked HTTP transfers. This callback
+   * is invoked as soon as a new data chunk is available for the request body.
+   * 
+   * @see OrthancPluginRegisterChunkedRestCallback()
+   * @param reader The user payload, as created by the OrthancPluginServerChunkedRequestReaderFactory() callback.
+   * @param data The content of the data chunk.
+   * @param size The size of the data chunk.
+   * @return 0 if success, or the error code if failure.
+   **/
   typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderAddChunk) (
     OrthancPluginServerChunkedRequestReader* reader,
     const void*                              data,
     uint32_t                                 size);
     
+
+  /**
+   * @brief Callback invoked whenever the request body is entirely received.
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP server that supports chunked HTTP transfers. This
+   * callback is invoked as soon as the full body of the HTTP request
+   * is available. The plugin can then send its answer thanks to the
+   * provided "output" object.
+   * 
+   * @see OrthancPluginRegisterChunkedRestCallback()
+   * @param reader The user payload, as created by the OrthancPluginServerChunkedRequestReaderFactory() callback.
+   * @param output The HTTP connection to the client application.
+   * @return 0 if success, or the error code if failure.
+   **/
   typedef OrthancPluginErrorCode (*OrthancPluginServerChunkedRequestReaderExecute) (
     OrthancPluginServerChunkedRequestReader* reader,
     OrthancPluginRestOutput*                 output);
     
+
+  /**
+   * @brief Callback invoked to release the resources associated with an incoming HTTP chunked transfer.
+   *
+   * Signature of a callback function that is called by Orthanc acting
+   * as a HTTP server that supports chunked HTTP transfers. This
+   * callback is invoked to release all the resources allocated by the
+   * given reader. Note that this function might be invoked even if
+   * the entire body was not read, to deal with client error or
+   * disconnection.
+   * 
+   * @see OrthancPluginRegisterChunkedRestCallback()
+   * @param reader The user payload, as created by the OrthancPluginServerChunkedRequestReaderFactory() callback.
+   **/
   typedef void (*OrthancPluginServerChunkedRequestReaderFinalize) (
     OrthancPluginServerChunkedRequestReader* reader);
   
@@ -6945,6 +7143,36 @@
     OrthancPluginServerChunkedRequestReaderFinalize  finalize;
   } _OrthancPluginChunkedRestCallback;
 
+
+  /**
+   * @brief Register a REST callback to handle chunked HTTP transfers.
+   *
+   * This function registers a REST callback against a regular
+   * expression for a URI. This function must be called during the
+   * initialization of the plugin, i.e. inside the
+   * OrthancPluginInitialize() public function.
+   *
+   * Contrarily to OrthancPluginRegisterRestCallback(), the callbacks
+   * will NOT be invoked in mutual exclusion, so it is up to the
+   * plugin to implement the required locking mechanisms.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param pathRegularExpression Regular expression for the URI. May contain groups. 
+   * @param getHandler The callback function to handle REST calls using the GET HTTP method.
+   * @param postHandler The callback function to handle REST calls using the GET POST method.
+   * @param deleteHandler The callback function to handle REST calls using the GET DELETE method.
+   * @param putHandler The callback function to handle REST calls using the GET PUT method.
+   * @param addChunk The callback invoked when a new chunk is available for the request body of a POST or PUT call.
+   * @param execute The callback invoked once the entire body of a POST or PUT call is read.
+   * @param finalize The callback invoked to release the resources associated with a POST or PUT call.
+   * @see OrthancPluginRegisterRestCallbackNoLock()
+   *
+   * @note
+   * The regular expression is case sensitive and must follow the
+   * [Perl syntax](https://www.boost.org/doc/libs/1_67_0/libs/regex/doc/html/boost_regex/syntax/perl_syntax.html).
+   *
+   * @ingroup Callbacks
+   **/
   ORTHANC_PLUGIN_INLINE void OrthancPluginRegisterChunkedRestCallback(
     OrthancPluginContext*                            context,
     const char*                                      pathRegularExpression,
@@ -6981,11 +7209,26 @@
     const char*  privateCreator;
   } _OrthancPluginGetTagName;
 
+  /**
+   * @brief Returns the symbolic name of a DICOM tag.
+   *
+   * This function makes a lookup to the dictionary of DICOM tags that
+   * are known to Orthanc, and returns the symbolic name of a DICOM tag.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param group The group of the tag.
+   * @param element The element of the tag.
+   * @param privateCreator For private tags, the name of the private creator (can be NULL).
+   * @return NULL in the case of an error, or a newly allocated string
+   * containing the path. This string must be freed by
+   * OrthancPluginFreeString().
+   * @ingroup Toolbox
+   **/
   ORTHANC_PLUGIN_INLINE char* OrthancPluginGetTagName(
     OrthancPluginContext*  context,
     uint16_t               group,
     uint16_t               element,
-    const char*            privateCreator /* can be NULL */)
+    const char*            privateCreator)
   {
     char* result;
 
--- a/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Wed Jul 03 10:28:17 2019 +0200
+++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp	Wed Jul 03 10:31:06 2019 +0200
@@ -33,6 +33,7 @@
 
 #include "OrthancPluginCppWrapper.h"
 
+#include <boost/thread.hpp>
 #include <boost/algorithm/string/predicate.hpp>
 #include <json/reader.h>
 #include <json/writer.h>
@@ -333,13 +334,10 @@
 
   void OrthancString::Assign(char* str)
   {
-    if (str == NULL)
+    Clear();
+
+    if (str != NULL)
     {
-      ORTHANC_PLUGINS_THROW_EXCEPTION(InternalError);
-    }
-    else
-    {
-      Clear();
       str_ = str;
     }
   }
@@ -2050,6 +2048,11 @@
   std::string OrthancJob::Submit(OrthancJob* job,
                                  int priority)
   {
+    if (job == NULL)
+    {
+      ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_NullPointer);
+    }
+
     OrthancPluginJob* orthanc = Create(job);
 
     char* id = OrthancPluginSubmitJob(GetGlobalContext(), orthanc, priority);
@@ -2069,6 +2072,70 @@
       return tmp;
     }
   }
+
+
+  void OrthancJob::SubmitAndWait(Json::Value& result,
+                                 OrthancJob* job /* takes ownership */,
+                                 int priority)
+  {
+    std::string id = Submit(job, priority);
+
+    for (;;)
+    {
+      boost::this_thread::sleep(boost::posix_time::milliseconds(100));
+
+      Json::Value status;
+      if (!RestApiGet(status, "/jobs/" + id, false) ||
+          !status.isMember("State") ||
+          status["State"].type() != Json::stringValue)
+      {
+        ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_InexistentItem);        
+      }
+
+      const std::string state = status["State"].asString();
+      if (state == "Success")
+      {
+        if (status.isMember("Content"))
+        {
+          result = status["Content"];
+        }
+        else
+        {
+          result = Json::objectValue;
+        }
+
+        return;
+      }
+      else if (state == "Running")
+      {
+        continue;
+      }
+      else if (!status.isMember("ErrorCode") ||
+               status["ErrorCode"].type() != Json::intValue)
+      {
+        ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(OrthancPluginErrorCode_InternalError);
+      }
+      else
+      {
+        if (!status.isMember("ErrorDescription") ||
+            status["ErrorDescription"].type() != Json::stringValue)
+        {
+          ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(status["ErrorCode"].asInt());
+        }
+        else
+        {
+#if HAS_ORTHANC_EXCEPTION == 1
+          throw Orthanc::OrthancException(static_cast<Orthanc::ErrorCode>(status["ErrorCode"].asInt()),
+                                          status["ErrorDescription"].asString());
+#else
+          LogError("Exception while executing the job: " + status["ErrorDescription"].asString());
+          ORTHANC_PLUGINS_THROW_PLUGIN_ERROR_CODE(status["ErrorCode"].asInt());          
+#endif
+        }
+      }
+    }
+  }
+
 #endif
 
 
@@ -2218,7 +2285,8 @@
     method_(OrthancPluginHttpMethod_Get),
     timeout_(0),
     pkcs11_(false),
-    chunkedBody_(NULL)
+    chunkedBody_(NULL),
+    allowChunkedTransfers_(true)
   {
   }
 
@@ -2598,35 +2666,63 @@
   }
 
 
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
   void HttpClient::Execute(IAnswer& answer)
   {
-    if (chunkedBody_ != NULL)
+#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
+    if (allowChunkedTransfers_)
     {
-      ExecuteWithStream(httpStatus_, answer, *chunkedBody_);
+      if (chunkedBody_ != NULL)
+      {
+        ExecuteWithStream(httpStatus_, answer, *chunkedBody_);
+      }
+      else
+      {
+        MemoryRequestBody wrapper(fullBody_);
+        ExecuteWithStream(httpStatus_, answer, wrapper);
+      }
+
+      return;
     }
-    else
+#endif
+    
+    // Compatibility mode for Orthanc SDK <= 1.5.6 or if chunked
+    // transfers are disabled. This results in higher memory usage
+    // (all chunks from the answer body are sent at once)
+
+    HttpHeaders answerHeaders;
+    std::string answerBody;
+    Execute(answerHeaders, answerBody);
+
+    for (HttpHeaders::const_iterator it = answerHeaders.begin(); 
+         it != answerHeaders.end(); ++it)
     {
-      MemoryRequestBody wrapper(fullBody_);
-      ExecuteWithStream(httpStatus_, answer, wrapper);
+      answer.AddHeader(it->first, it->second);      
+    }
+
+    if (!answerBody.empty())
+    {
+      answer.AddChunk(answerBody.c_str(), answerBody.size());
     }
   }
-#endif
 
 
   void HttpClient::Execute(HttpHeaders& answerHeaders /* out */,
                            std::string& answerBody /* out */)
   {
 #if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
-    MemoryAnswer answer;
-    Execute(answer);
-    answerHeaders = answer.GetHeaders();
-    answer.GetBody().Flatten(answerBody);
-
-#else
-    // Compatibility mode for Orthanc SDK <= 1.5.6. This results in
-    // higher memory usage (all chunks from the body request are sent
-    // at once)
+    if (allowChunkedTransfers_)
+    {
+      MemoryAnswer answer;
+      Execute(answer);
+      answerHeaders = answer.GetHeaders();
+      answer.GetBody().Flatten(answerBody);
+      return;
+    }
+#endif
+    
+    // Compatibility mode for Orthanc SDK <= 1.5.6 or if chunked
+    // transfers are disabled. This results in higher memory usage
+    // (all chunks from the request body are sent at once)
 
     if (chunkedBody_ != NULL)
     {
@@ -2647,7 +2743,6 @@
     {
       ExecuteWithoutStream(httpStatus_, answerHeaders, answerBody, fullBody_);
     }
-#endif
   }
 
 
--- a/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Wed Jul 03 10:28:17 2019 +0200
+++ b/Plugins/Samples/Common/OrthancPluginCppWrapper.h	Wed Jul 03 10:31:06 2019 +0200
@@ -768,6 +768,10 @@
 
     static std::string Submit(OrthancJob* job /* takes ownership */,
                               int priority);
+
+    static void SubmitAndWait(Json::Value& result,
+                              OrthancJob* job /* takes ownership */,
+                              int priority);
   };
 #endif
 
@@ -810,7 +814,7 @@
       virtual bool ReadNextChunk(std::string& chunk) = 0;
     };
 
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
+
     class IAnswer : public boost::noncopyable
     {
     public:
@@ -824,7 +828,6 @@
       virtual void AddChunk(const void* data,
                             size_t size) = 0;
     };
-#endif
 
 
   private:
@@ -842,7 +845,8 @@
     std::string              certificateKeyPassword_;
     bool                     pkcs11_;
     std::string              fullBody_;
-    IRequestBody*            chunkedBody_;    
+    IRequestBody*            chunkedBody_;
+    bool                     allowChunkedTransfers_;
 
 #if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
     void ExecuteWithStream(uint16_t& httpStatus,  // out
@@ -920,9 +924,19 @@
 
     void SetBody(IRequestBody& body);
 
-#if HAS_ORTHANC_PLUGIN_CHUNKED_HTTP_CLIENT == 1
+    // This function can be used to disable chunked transfers if the
+    // remote server is Orthanc with a version <= 1.5.6.
+    void SetChunkedTransfersAllowed(bool allow)
+    {
+      allowChunkedTransfers_ = allow;
+    }
+
+    bool IsChunkedTransfersAllowed() const
+    {
+      return allowChunkedTransfers_;
+    }
+
     void Execute(IAnswer& answer);
-#endif
 
     void Execute(HttpHeaders& answerHeaders /* out */,
                  std::string& answerBody /* out */);
@@ -1067,9 +1081,6 @@
         Internals::ChunkedRequestReaderExecute,
         Internals::ChunkedRequestReaderFinalize);
 #else
-      LogWarning("Performance warning: The plugin was compiled against a pre-1.5.7 version "
-                 "of the Orthanc SDK. Multipart transfers will be entirely stored in RAM.");
-      
       OrthancPluginRegisterRestCallbackNoLock(
         GetGlobalContext(), uri.c_str(), 
         Internals::ChunkedRestCompatibility<GetHandler, PostHandler, DeleteHandler, PutHandler>);
--- a/Resources/CMake/OrthancFrameworkParameters.cmake	Wed Jul 03 10:28:17 2019 +0200
+++ b/Resources/CMake/OrthancFrameworkParameters.cmake	Wed Jul 03 10:31:06 2019 +0200
@@ -17,7 +17,7 @@
 # Version of the Orthanc API, can be retrieved from "/system" URI in
 # order to check whether new URI endpoints are available even if using
 # the mainline version of Orthanc
-set(ORTHANC_API_VERSION "2")
+set(ORTHANC_API_VERSION "3")
 
 
 #####################################################################
--- a/Resources/DownloadOrthancFramework.cmake	Wed Jul 03 10:28:17 2019 +0200
+++ b/Resources/DownloadOrthancFramework.cmake	Wed Jul 03 10:31:06 2019 +0200
@@ -108,6 +108,8 @@
         set(ORTHANC_FRAMEWORK_MD5 "cfc437e0687ae4bd725fd93dc1f08bc4")
       elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.6")
         set(ORTHANC_FRAMEWORK_MD5 "3c29de1e289b5472342947168f0105c0")
+      elseif (ORTHANC_FRAMEWORK_VERSION STREQUAL "1.5.7")
+        set(ORTHANC_FRAMEWORK_MD5 "e1b76f01116d9b5d4ac8cc39980560e3")
       endif()
     endif()
   endif()
--- a/Resources/OldBuildInstructions.txt	Wed Jul 03 10:28:17 2019 +0200
+++ b/Resources/OldBuildInstructions.txt	Wed Jul 03 10:31:06 2019 +0200
@@ -133,6 +133,18 @@
               libpng-devel sqlite-devel libuuid-devel openssl-devel \
               lua-devel mercurial patch tar
 
+Using static linking with Civetweb (tested with Orthanc 1.5.7):
+
+# cmake -DSTATIC_BUILD=ON \
+        -DSTANDALONE_BUILD=ON \
+        -DUSE_LEGACY_JSONCPP=ON \
+        -DUSE_LEGACY_LIBICU=ON \
+        -DBOOST_LOCALE_BACKEND=icu \
+        -DCMAKE_BUILD_TYPE=Debug \
+        ~/Orthanc
+
+Using Mongoose (untested):
+
 # cmake -DALLOW_DOWNLOADS=ON \
         -DUSE_SYSTEM_JSONCPP=OFF \
         -DUSE_SYSTEM_CIVETWEB=OFF \