[med-svn] [orthanc-dicomweb] 02/05: Imported Upstream version 0.3+dfsg

Sebastien Jodogne jodogne-guest at moszumanska.debian.org
Tue Jun 28 14:28:44 UTC 2016


This is an automated email from the git hooks/post-receive script.

jodogne-guest pushed a commit to branch master
in repository orthanc-dicomweb.

commit fc6ba12115bedcfc8c9af193d49681e2ec7eaaae
Author: jodogne-guest <s.jodogne at gmail.com>
Date:   Tue Jun 28 16:07:53 2016 +0200

    Imported Upstream version 0.3+dfsg
---
 .hg_archival.txt                                   |   6 +-
 AUTHORS                                            |   4 +-
 CMakeLists.txt                                     |  17 +-
 NEWS                                               |  13 +
 Orthanc/Core/ChunkedBuffer.cpp                     |  15 +-
 Orthanc/Core/ChunkedBuffer.h                       |   4 +-
 Orthanc/Core/Enumerations.cpp                      | 248 +++++-
 Orthanc/Core/Enumerations.h                        |  84 ++-
 Orthanc/Core/Logging.h                             |  15 +-
 Orthanc/Core/OrthancException.h                    |   2 +-
 Orthanc/Core/PrecompiledHeaders.h                  |   2 +-
 Orthanc/Core/Toolbox.cpp                           | 232 +++++-
 Orthanc/Core/Toolbox.h                             |  20 +-
 Orthanc/Core/WebServiceParameters.cpp              | 265 +++++++
 Orthanc/Core/WebServiceParameters.h                | 124 +++
 .../Samples/Common/OrthancPluginCppWrapper.cpp     | 829 +++++++++++++++++++++
 .../Samples/Common/OrthancPluginCppWrapper.h       | 384 ++++++++++
 Orthanc/Resources/CMake/BoostConfiguration.cmake   |  65 +-
 Orthanc/Resources/CMake/Compiler.cmake             |  35 +-
 Orthanc/Resources/CMake/DownloadPackage.cmake      |  14 +-
 Orthanc/Resources/CMake/JsonCppConfiguration.cmake |  25 +
 Orthanc/Resources/WindowsResources.py              |   2 +-
 .../orthanc/OrthancCPlugin.h                       | 735 +++++++++++++++++-
 Plugin/ChunkedBuffer.cpp                           |  86 ---
 Plugin/ChunkedBuffer.h                             |  59 --
 Plugin/Configuration.cpp                           | 354 ++++-----
 Plugin/Configuration.h                             |  65 +-
 Plugin/Dicom.cpp                                   | 195 +++--
 Plugin/Dicom.h                                     |  38 +-
 Plugin/DicomResults.cpp                            | 356 ++++++++-
 Plugin/DicomResults.h                              |  10 +-
 Plugin/DicomWebClient.cpp                          | 616 +++++++++++++++
 Plugin/{QidoRs.h => DicomWebClient.h}              |  18 +-
 Plugin/DicomWebServers.cpp                         | 279 +++++++
 Plugin/DicomWebServers.h                           |  73 ++
 Plugin/Plugin.cpp                                  | 234 +++---
 Plugin/Plugin.h                                    |  10 +-
 Plugin/QidoRs.cpp                                  | 383 +++++++---
 Plugin/QidoRs.h                                    |   2 +-
 Plugin/StowRs.cpp                                  |  56 +-
 Plugin/StowRs.h                                    |   2 +-
 Plugin/WadoRs.cpp                                  | 180 ++---
 Plugin/WadoRs.h                                    |   2 +-
 Plugin/WadoRsRetrieveFrames.cpp                    |  97 +--
 Plugin/{Wado.cpp => WadoUri.cpp}                   | 111 ++-
 Plugin/{Wado.h => WadoUri.h}                       |   8 +-
 README                                             |  21 +-
 Resources/CMake/GdcmConfiguration.cmake            |   2 +-
 Resources/Samples/JavaScript/index.html            |   4 +-
 Resources/Samples/JavaScript/qido-rs.js            |   2 +-
 Resources/Samples/JavaScript/stow-rs.js            |   2 +-
 Resources/Samples/{ => Python}/SendStow.py         |  44 +-
 .../Samples/{ => Python}/WadoRetrieveStudy.py      |   2 +-
 Resources/SyncOrthancFolder.py                     |  22 +-
 UnitTestsSources/UnitTestsMain.cpp                 |   4 +-
 Usage.txt                                          | 284 +++++++
 56 files changed, 5722 insertions(+), 1039 deletions(-)

diff --git a/.hg_archival.txt b/.hg_archival.txt
index 59b1f36..447ae03 100644
--- a/.hg_archival.txt
+++ b/.hg_archival.txt
@@ -1,5 +1,5 @@
 repo: d5f45924411123cfd02d035fd50b8e37536eadef
-node: 8263a7bb98a15d2b1a0a6efb9cdc3b78471871fd
-branch: OrthancDicomWeb-0.2
+node: b6f0743a917faa06e53e0b34d663a74b2ef9d4ab
+branch: OrthancDicomWeb-0.3
 latesttag: null
-latesttagdistance: 93
+latesttagdistance: 141
diff --git a/AUTHORS b/AUTHORS
index 661754b..2d804cf 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -1,5 +1,5 @@
-DICOM Web plugin for Orthanc
-============================
+DICOMweb plugin for Orthanc
+===========================
 
 
 Authors
diff --git a/CMakeLists.txt b/CMakeLists.txt
index a0cad32..d41d2a6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,5 +1,5 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# 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
@@ -20,7 +20,7 @@ cmake_minimum_required(VERSION 2.8)
 
 project(OrthancDicomWeb)
 
-set(ORTHANC_DICOM_WEB_VERSION "0.2")
+set(ORTHANC_DICOM_WEB_VERSION "0.3")
 
 
 # Parameters of the build
@@ -59,7 +59,7 @@ include(${CMAKE_SOURCE_DIR}/Resources/CMake/GdcmConfiguration.cmake)
 
 
 if (STATIC_BUILD OR NOT USE_SYSTEM_ORTHANC_SDK)
-  include_directories(${ORTHANC_ROOT}/Sdk-0.9.5)
+  include_directories(${ORTHANC_ROOT}/Sdk-1.1.0)
 else ()
   CHECK_INCLUDE_FILE_CXX(orthanc/OrthancCPlugin.h HAVE_ORTHANC_H)
   if (NOT HAVE_ORTHANC_H)
@@ -102,8 +102,11 @@ add_definitions(
   -DORTHANC_ENABLE_MD5=0
   -DORTHANC_ENABLE_BASE64=0
   -DORTHANC_ENABLE_LOGGING=0
+  -DHAS_ORTHANC_EXCEPTION=1
   )
 
+include_directories(${ORTHANC_ROOT}/Core)  # To access "OrthancException.h"
+
 set(CORE_SOURCES
   ${BOOST_SOURCES}
   ${JSONCPP_SOURCES}
@@ -113,20 +116,23 @@ set(CORE_SOURCES
   ${ORTHANC_ROOT}/Core/ChunkedBuffer.cpp
   ${ORTHANC_ROOT}/Core/Enumerations.cpp
   ${ORTHANC_ROOT}/Core/Toolbox.cpp
+  ${ORTHANC_ROOT}/Core/WebServiceParameters.cpp
+  ${ORTHANC_ROOT}/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
 
-  Plugin/ChunkedBuffer.cpp
   Plugin/Configuration.cpp
   Plugin/Dicom.cpp
   Plugin/DicomResults.cpp
   )
 
 add_library(OrthancDicomWeb SHARED ${CORE_SOURCES}
+  ${CMAKE_SOURCE_DIR}/Plugin/DicomWebClient.cpp
+  ${CMAKE_SOURCE_DIR}/Plugin/DicomWebServers.cpp
   ${CMAKE_SOURCE_DIR}/Plugin/Plugin.cpp
   ${CMAKE_SOURCE_DIR}/Plugin/QidoRs.cpp
   ${CMAKE_SOURCE_DIR}/Plugin/StowRs.cpp
-  ${CMAKE_SOURCE_DIR}/Plugin/Wado.cpp
   ${CMAKE_SOURCE_DIR}/Plugin/WadoRs.cpp
   ${CMAKE_SOURCE_DIR}/Plugin/WadoRsRetrieveFrames.cpp
+  ${CMAKE_SOURCE_DIR}/Plugin/WadoUri.cpp
   ${AUTOGENERATED_SOURCES}
   )
 
@@ -150,6 +156,7 @@ install(
 add_executable(UnitTests
   ${CORE_SOURCES}
   ${GTEST_SOURCES}
+  ${CMAKE_SOURCE_DIR}/Plugin/DicomWebServers.cpp
   UnitTestsSources/UnitTestsMain.cpp
   )
 
diff --git a/NEWS b/NEWS
index 401b546..ccf9faf 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,19 @@ Pending changes in the mainline
 ===============================
 
 
+Version 0.3 (2016/06/28)
+========================
+
+=> Minimum SDK version: 1.1.0 <=
+
+* STOW-RS client with URI "/{dicom-web}/servers/{id}/stow"
+* QIDO-RS and WADO-RS client with URI "/{dicom-web}/servers/{id}/get"
+* Retrieval of DICOM instances with WADO-RS through URI "/{dicom-web}/servers/{id}/retrieve"
+* Improved 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)
+
+
 Version 0.2 (2015/12/10)
 ========================
 
diff --git a/Orthanc/Core/ChunkedBuffer.cpp b/Orthanc/Core/ChunkedBuffer.cpp
index dc6d091..5d2c2c8 100644
--- a/Orthanc/Core/ChunkedBuffer.cpp
+++ b/Orthanc/Core/ChunkedBuffer.cpp
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -51,17 +51,19 @@ namespace Orthanc
   }
 
 
-  void ChunkedBuffer::AddChunk(const char* chunkData,
+  void ChunkedBuffer::AddChunk(const void* chunkData,
                                size_t chunkSize)
   {
     if (chunkSize == 0)
     {
       return;
     }
-
-    assert(chunkData != NULL);
-    chunks_.push_back(new std::string(chunkData, chunkSize));
-    numBytes_ += chunkSize;
+    else
+    {
+      assert(chunkData != NULL);
+      chunks_.push_back(new std::string(reinterpret_cast<const char*>(chunkData), chunkSize));
+      numBytes_ += chunkSize;
+    }
   }
 
 
@@ -95,5 +97,6 @@ namespace Orthanc
     }
 
     chunks_.clear();
+    numBytes_ = 0;
   }
 }
diff --git a/Orthanc/Core/ChunkedBuffer.h b/Orthanc/Core/ChunkedBuffer.h
index 93ffc2b..552c1ec 100644
--- a/Orthanc/Core/ChunkedBuffer.h
+++ b/Orthanc/Core/ChunkedBuffer.h
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -61,7 +61,7 @@ namespace Orthanc
       return numBytes_;
     }
 
-    void AddChunk(const char* chunkData,
+    void AddChunk(const void* chunkData,
                   size_t chunkSize);
 
     void AddChunk(const std::string& chunk);
diff --git a/Orthanc/Core/Enumerations.cpp b/Orthanc/Core/Enumerations.cpp
index eec80e6..e600828 100644
--- a/Orthanc/Core/Enumerations.cpp
+++ b/Orthanc/Core/Enumerations.cpp
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -35,8 +35,10 @@
 
 #include "OrthancException.h"
 #include "Toolbox.h"
+#include "Logging.h"
 
 #include <string.h>
+#include <cassert>
 
 namespace Orthanc
 {
@@ -212,10 +214,10 @@ namespace Orthanc
         return "The specified path does not point to a directory";
 
       case ErrorCode_HttpPortInUse:
-        return "The TCP port of the HTTP server is already in use";
+        return "The TCP port of the HTTP server is privileged or already in use";
 
       case ErrorCode_DicomPortInUse:
-        return "The TCP port of the DICOM server is already in use";
+        return "The TCP port of the DICOM server is privileged or already in use";
 
       case ErrorCode_BadHttpStatusInRest:
         return "This HTTP status is not allowed in a REST API";
@@ -328,6 +330,9 @@ namespace Orthanc
       case ErrorCode_NoWorklistHandler:
         return "No request handler factory for DICOM C-Find Modality SCP";
 
+      case ErrorCode_AlreadyExistingTag:
+        return "Cannot override the value of a tag that already exists";
+
       default:
         if (error >= ErrorCode_START_PLUGINS)
         {
@@ -718,6 +723,34 @@ namespace Orthanc
   }
 
 
+  const char* EnumerationToString(PixelFormat format)
+  {
+    switch (format)
+    {
+      case PixelFormat_RGB24:
+        return "RGB24";
+
+      case PixelFormat_RGBA32:
+        return "RGBA32";
+
+      case PixelFormat_Grayscale8:
+        return "Grayscale (unsigned 8bpp)";
+
+      case PixelFormat_Grayscale16:
+        return "Grayscale (unsigned 16bpp)";
+
+      case PixelFormat_SignedGrayscale16:
+        return "Grayscale (signed 16bpp)";
+
+      case PixelFormat_Float32:
+        return "Grayscale (float 32bpp)";
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
   Encoding StringToEncoding(const char* encoding)
   {
     std::string s(encoding);
@@ -868,6 +901,151 @@ namespace Orthanc
   }
 
 
+  ValueRepresentation StringToValueRepresentation(const std::string& vr,
+                                                  bool throwIfUnsupported)
+  {
+    if (vr == "AE")
+    {
+      return ValueRepresentation_ApplicationEntity;
+    }
+    else if (vr == "AS")
+    {
+      return ValueRepresentation_AgeString;
+    }
+    else if (vr == "AT")
+    {
+      return ValueRepresentation_AttributeTag;
+    }
+    else if (vr == "CS")
+    {
+      return ValueRepresentation_CodeString;
+    }
+    else if (vr == "DA")
+    {
+      return ValueRepresentation_Date;
+    }
+    else if (vr == "DS")
+    {
+      return ValueRepresentation_DecimalString;
+    }
+    else if (vr == "DT")
+    {
+      return ValueRepresentation_DateTime;
+    }
+    else if (vr == "FL")
+    {
+      return ValueRepresentation_FloatingPointSingle;
+    }
+    else if (vr == "FD")
+    {
+      return ValueRepresentation_FloatingPointDouble;
+    }
+    else if (vr == "IS")
+    {
+      return ValueRepresentation_IntegerString;
+    }
+    else if (vr == "LO")
+    {
+      return ValueRepresentation_LongString;
+    }
+    else if (vr == "LT")
+    {
+      return ValueRepresentation_LongText;
+    }
+    else if (vr == "OB")
+    {
+      return ValueRepresentation_OtherByte;
+    }
+    else if (vr == "OD")
+    {
+      return ValueRepresentation_OtherDouble;
+    }
+    else if (vr == "OF")
+    {
+      return ValueRepresentation_OtherFloat;
+    }
+    else if (vr == "OL")
+    {
+      return ValueRepresentation_OtherLong;
+    }
+    else if (vr == "OW")
+    {
+      return ValueRepresentation_OtherWord;
+    }
+    else if (vr == "PN")
+    {
+      return ValueRepresentation_PersonName;
+    }
+    else if (vr == "SH")
+    {
+      return ValueRepresentation_ShortString;
+    }
+    else if (vr == "SL")
+    {
+      return ValueRepresentation_SignedLong;
+    }
+    else if (vr == "SQ")
+    {
+      return ValueRepresentation_Sequence;
+    }
+    else if (vr == "SS")
+    {
+      return ValueRepresentation_SignedShort;
+    }
+    else if (vr == "ST")
+    {
+      return ValueRepresentation_ShortText;
+    }
+    else if (vr == "TM")
+    {
+      return ValueRepresentation_Time;
+    }
+    else if (vr == "UC")
+    {
+      return ValueRepresentation_UnlimitedCharacters;
+    }
+    else if (vr == "UI")
+    {
+      return ValueRepresentation_UniqueIdentifier;
+    }
+    else if (vr == "UL")
+    {
+      return ValueRepresentation_UnsignedLong;
+    }
+    else if (vr == "UN")
+    {
+      return ValueRepresentation_Unknown;
+    }
+    else if (vr == "UR")
+    {
+      return ValueRepresentation_UniversalResource;
+    }
+    else if (vr == "US")
+    {
+      return ValueRepresentation_UnsignedShort;
+    }
+    else if (vr == "UT")
+    {
+      return ValueRepresentation_UnlimitedText;
+    }
+    else
+    {
+      std::string s = "Unsupported value representation encountered: " + vr;
+
+      if (throwIfUnsupported)
+      {
+        LOG(ERROR) << s;
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+      else
+      {
+        LOG(INFO) << s;
+        return ValueRepresentation_NotSupported;
+      }
+    }
+  }
+
+
   unsigned int GetBytesPerPixel(PixelFormat format)
   {
     switch (format)
@@ -885,6 +1063,10 @@ namespace Orthanc
       case PixelFormat_RGBA32:
         return 4;
 
+      case PixelFormat_Float32:
+        assert(sizeof(float) == 4);
+        return 4;
+
       default:
         throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
@@ -897,7 +1079,7 @@ namespace Orthanc
     std::string s = specificCharacterSet;
     Toolbox::ToUpperCase(s);
 
-    // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/
+    // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2
     // https://github.com/dcm4che/dcm4che/blob/master/dcm4che-core/src/main/java/org/dcm4che3/data/SpecificCharacterSet.java
     if (s == "ISO_IR 6" ||
         s == "ISO_IR 192" ||
@@ -1046,7 +1228,7 @@ namespace Orthanc
 
   const char* GetDicomSpecificCharacterSet(Encoding encoding)
   {
-    // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/
+    // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2
     switch (encoding)
     {
       case Encoding_Utf8:
@@ -1151,4 +1333,60 @@ namespace Orthanc
     return (type >= FileContentType_StartUser &&
             type <= FileContentType_EndUser);
   }
+
+
+  bool IsBinaryValueRepresentation(ValueRepresentation vr)
+  {
+    // http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html
+
+    switch (vr)
+    {
+      case ValueRepresentation_ApplicationEntity:     // AE
+      case ValueRepresentation_AgeString:             // AS
+      case ValueRepresentation_CodeString:            // CS
+      case ValueRepresentation_Date:                  // DA
+      case ValueRepresentation_DecimalString:         // DS
+      case ValueRepresentation_DateTime:              // DT
+      case ValueRepresentation_IntegerString:         // IS
+      case ValueRepresentation_LongString:            // LO
+      case ValueRepresentation_LongText:              // LT
+      case ValueRepresentation_PersonName:            // PN
+      case ValueRepresentation_ShortString:           // SH
+      case ValueRepresentation_ShortText:             // ST
+      case ValueRepresentation_Time:                  // TM
+      case ValueRepresentation_UnlimitedCharacters:   // UC
+      case ValueRepresentation_UniqueIdentifier:      // UI (UID)
+      case ValueRepresentation_UniversalResource:     // UR (URI or URL)
+      case ValueRepresentation_UnlimitedText:         // UT
+      {
+        return false;
+      }
+
+      /**
+       * Below are all the VR whose character repertoire is tagged as
+       * "not applicable"
+       **/
+      case ValueRepresentation_AttributeTag:          // AT (2 x uint16_t)
+      case ValueRepresentation_FloatingPointSingle:   // FL (float)
+      case ValueRepresentation_FloatingPointDouble:   // FD (double)
+      case ValueRepresentation_OtherByte:             // OB
+      case ValueRepresentation_OtherDouble:           // OD
+      case ValueRepresentation_OtherFloat:            // OF
+      case ValueRepresentation_OtherLong:             // OL
+      case ValueRepresentation_OtherWord:             // OW
+      case ValueRepresentation_SignedLong:            // SL (int32_t)
+      case ValueRepresentation_Sequence:              // SQ
+      case ValueRepresentation_SignedShort:           // SS (int16_t)
+      case ValueRepresentation_UnsignedLong:          // UL (uint32_t)
+      case ValueRepresentation_Unknown:               // UN
+      case ValueRepresentation_UnsignedShort:         // US (uint16_t)
+      {
+        return true;
+      }
+
+      case ValueRepresentation_NotSupported:
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
 }
diff --git a/Orthanc/Core/Enumerations.h b/Orthanc/Core/Enumerations.h
index 19b6b31..7eef119 100644
--- a/Orthanc/Core/Enumerations.h
+++ b/Orthanc/Core/Enumerations.h
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -32,6 +32,8 @@
 
 #pragma once
 
+#include <string>
+
 namespace Orthanc
 {
   enum Endianness
@@ -100,8 +102,8 @@ namespace Orthanc
     ErrorCode_DirectoryOverFile = 2000    /*!< The directory to be created is already occupied by a regular file */,
     ErrorCode_FileStorageCannotWrite = 2001    /*!< Unable to create a subdirectory or a file in the file storage */,
     ErrorCode_DirectoryExpected = 2002    /*!< The specified path does not point to a directory */,
-    ErrorCode_HttpPortInUse = 2003    /*!< The TCP port of the HTTP server is already in use */,
-    ErrorCode_DicomPortInUse = 2004    /*!< The TCP port of the DICOM server is already in use */,
+    ErrorCode_HttpPortInUse = 2003    /*!< The TCP port of the HTTP server is privileged or already in use */,
+    ErrorCode_DicomPortInUse = 2004    /*!< The TCP port of the DICOM server is privileged or already in use */,
     ErrorCode_BadHttpStatusInRest = 2005    /*!< This HTTP status is not allowed in a REST API */,
     ErrorCode_RegularFileExpected = 2006    /*!< The specified path does not point to a regular file */,
     ErrorCode_PathToExecutable = 2007    /*!< Unable to get the path to the executable */,
@@ -139,6 +141,7 @@ namespace Orthanc
     ErrorCode_SslDisabled = 2039    /*!< Orthanc has been built without SSL support */,
     ErrorCode_CannotOrderSlices = 2040    /*!< Unable to order the slices of the series */,
     ErrorCode_NoWorklistHandler = 2041    /*!< No request handler factory for DICOM C-Find Modality SCP */,
+    ErrorCode_AlreadyExistingTag = 2042    /*!< Cannot override the value of a tag that already exists */,
     ErrorCode_START_PLUGINS = 1000000
   };
 
@@ -186,7 +189,13 @@ namespace Orthanc
      * {summary}{Graylevel, signed 16bpp image.}
      * {description}{The image is graylevel. Each pixel is signed and stored in two bytes.}
      **/
-    PixelFormat_SignedGrayscale16 = 5
+    PixelFormat_SignedGrayscale16 = 5,
+      
+    /**
+     * {summary}{Graylevel, floating-point image.}
+     * {description}{The image is graylevel. Each pixel is floating-point and stored in 4 bytes.}
+     **/
+    PixelFormat_Float32 = 6
   };
 
 
@@ -314,7 +323,8 @@ namespace Orthanc
   };
 
 
-  // http://www.dabsoft.ch/dicom/3/C.12.1.1.2/
+  // Specific Character Sets
+  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.12.1.1.2
   enum Encoding
   {
     Encoding_Ascii,
@@ -338,7 +348,7 @@ namespace Orthanc
   };
 
 
-  // https://www.dabsoft.ch/dicom/3/C.7.6.3.1.2/
+  // http://dicom.nema.org/medical/dicom/current/output/html/part03.html#sect_C.7.6.3.1.2
   enum PhotometricInterpretation
   {
     PhotometricInterpretation_ARGB,  // Retired
@@ -375,6 +385,59 @@ namespace Orthanc
     RequestOrigin_Lua
   };
 
+  enum ServerBarrierEvent
+  {
+    ServerBarrierEvent_Stop,
+    ServerBarrierEvent_Reload  // SIGHUP signal: reload configuration file
+  };
+
+  enum FileMode
+  {
+    FileMode_ReadBinary,
+    FileMode_WriteBinary
+  };
+
+  /**
+   * The value representations Orthanc knows about. They correspond to
+   * the DICOM 2016b version of the standard.
+   * http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html
+   **/
+  enum ValueRepresentation
+  {
+    ValueRepresentation_ApplicationEntity = 1,     // AE
+    ValueRepresentation_AgeString = 2,             // AS
+    ValueRepresentation_AttributeTag = 3,          // AT (2 x uint16_t)
+    ValueRepresentation_CodeString = 4,            // CS
+    ValueRepresentation_Date = 5,                  // DA
+    ValueRepresentation_DecimalString = 6,         // DS
+    ValueRepresentation_DateTime = 7,              // DT
+    ValueRepresentation_FloatingPointSingle = 8,   // FL (float)
+    ValueRepresentation_FloatingPointDouble = 9,   // FD (double)
+    ValueRepresentation_IntegerString = 10,        // IS
+    ValueRepresentation_LongString = 11,           // LO
+    ValueRepresentation_LongText = 12,             // LT
+    ValueRepresentation_OtherByte = 13,            // OB
+    ValueRepresentation_OtherDouble = 14,          // OD
+    ValueRepresentation_OtherFloat = 15,           // OF
+    ValueRepresentation_OtherLong = 16,            // OL
+    ValueRepresentation_OtherWord = 17,            // OW
+    ValueRepresentation_PersonName = 18,           // PN
+    ValueRepresentation_ShortString = 19,          // SH
+    ValueRepresentation_SignedLong = 20,           // SL (int32_t)
+    ValueRepresentation_Sequence = 21,             // SQ
+    ValueRepresentation_SignedShort = 22,          // SS (int16_t)
+    ValueRepresentation_ShortText = 23,            // ST
+    ValueRepresentation_Time = 24,                 // TM
+    ValueRepresentation_UnlimitedCharacters = 25,  // UC
+    ValueRepresentation_UniqueIdentifier = 26,     // UI (UID)
+    ValueRepresentation_UnsignedLong = 27,         // UL (uint32_t)
+    ValueRepresentation_Unknown = 28,              // UN
+    ValueRepresentation_UniversalResource = 29,    // UR (URI or URL)
+    ValueRepresentation_UnsignedShort = 30,        // US (uint16_t)
+    ValueRepresentation_UnlimitedText = 31,        // UT
+    ValueRepresentation_NotSupported               // Not supported by Orthanc, or tag not in dictionary
+  };
+
 
   /**
    * WARNING: Do not change the explicit values in the enumerations
@@ -443,13 +506,18 @@ namespace Orthanc
 
   const char* EnumerationToString(RequestOrigin origin);
 
+  const char* EnumerationToString(PixelFormat format);
+
   Encoding StringToEncoding(const char* encoding);
 
   ResourceType StringToResourceType(const char* type);
 
   ImageFormat StringToImageFormat(const char* format);
 
-  LogLevel StringToLogLevel(const char* format);
+  LogLevel StringToLogLevel(const char* level);
+
+  ValueRepresentation StringToValueRepresentation(const std::string& vr,
+                                                  bool throwIfUnsupported);
 
   unsigned int GetBytesPerPixel(PixelFormat format);
 
@@ -467,4 +535,6 @@ namespace Orthanc
   HttpStatus ConvertErrorCodeToHttpStatus(ErrorCode error);
 
   bool IsUserContentType(FileContentType type);
+
+  bool IsBinaryValueRepresentation(ValueRepresentation vr);
 }
diff --git a/Orthanc/Core/Logging.h b/Orthanc/Core/Logging.h
index 586f7b3..7318246 100644
--- a/Orthanc/Core/Logging.h
+++ b/Orthanc/Core/Logging.h
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -42,10 +42,16 @@ namespace Orthanc
 
     void Finalize();
 
+    void Reset();
+
+    void Flush();
+
     void EnableInfoLevel(bool enabled);
 
     void EnableTraceLevel(bool enabled);
 
+    void SetTargetFile(const std::string& path);
+
     void SetTargetFolder(const std::string& path);
 
     struct NullStream : public std::ostream 
@@ -72,16 +78,10 @@ namespace Orthanc
 
 #else  /* ORTHANC_ENABLE_LOGGING == 1 */
 
-#if ORTHANC_ENABLE_GOOGLE_LOG == 1
-#  include <stdlib.h>  // Including this fixes a problem in glog for recent releases of MinGW
-#  include <glog/logging.h>
-#else
 #  include <boost/thread/mutex.hpp>
 #  define LOG(level)  ::Orthanc::Logging::InternalLogger(#level,  __FILE__, __LINE__)
 #  define VLOG(level) ::Orthanc::Logging::InternalLogger("TRACE", __FILE__, __LINE__)
-#endif
 
-#if ORTHANC_ENABLE_GOOGLE_LOG != 1
 namespace Orthanc
 {
   namespace Logging
@@ -107,6 +107,5 @@ namespace Orthanc
     };
   }
 }
-#endif
 
 #endif  // ORTHANC_ENABLE_LOGGING
diff --git a/Orthanc/Core/OrthancException.h b/Orthanc/Core/OrthancException.h
index 26d937e..5afa41f 100644
--- a/Orthanc/Core/OrthancException.h
+++ b/Orthanc/Core/OrthancException.h
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
diff --git a/Orthanc/Core/PrecompiledHeaders.h b/Orthanc/Core/PrecompiledHeaders.h
index da70ea4..e22f012 100644
--- a/Orthanc/Core/PrecompiledHeaders.h
+++ b/Orthanc/Core/PrecompiledHeaders.h
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
diff --git a/Orthanc/Core/Toolbox.cpp b/Orthanc/Core/Toolbox.cpp
index c89d9f2..4990204 100644
--- a/Orthanc/Core/Toolbox.cpp
+++ b/Orthanc/Core/Toolbox.cpp
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -67,7 +67,7 @@
 #include <limits.h>      /* PATH_MAX */
 #endif
 
-#if defined(__linux) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
+#if defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
 #include <limits.h>      /* PATH_MAX */
 #include <signal.h>
 #include <unistd.h>
@@ -111,27 +111,34 @@ extern "C"
 
 namespace Orthanc
 {
-  static bool finish;
+  static bool finish_;
+  static ServerBarrierEvent barrierEvent_;
 
 #if defined(_WIN32)
   static BOOL WINAPI ConsoleControlHandler(DWORD dwCtrlType)
   {
     // http://msdn.microsoft.com/en-us/library/ms683242(v=vs.85).aspx
-    finish = true;
+    finish_ = true;
     return true;
   }
 #else
-  static void SignalHandler(int)
+  static void SignalHandler(int signal)
   {
-    finish = true;
+    if (signal == SIGHUP)
+    {
+      barrierEvent_ = ServerBarrierEvent_Reload;
+    }
+
+    finish_ = true;
   }
 #endif
 
+
   void Toolbox::USleep(uint64_t microSeconds)
   {
 #if defined(_WIN32)
     ::Sleep(static_cast<DWORD>(microSeconds / static_cast<uint64_t>(1000)));
-#elif defined(__linux) || defined(__APPLE__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
+#elif defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
     usleep(microSeconds);
 #else
 #error Support your platform here
@@ -139,7 +146,7 @@ namespace Orthanc
   }
 
 
-  static void ServerBarrierInternal(const bool* stopFlag)
+  static ServerBarrierEvent ServerBarrierInternal(const bool* stopFlag)
   {
 #if defined(_WIN32)
     SetConsoleCtrlHandler(ConsoleControlHandler, true);
@@ -147,11 +154,13 @@ namespace Orthanc
     signal(SIGINT, SignalHandler);
     signal(SIGQUIT, SignalHandler);
     signal(SIGTERM, SignalHandler);
+    signal(SIGHUP, SignalHandler);
 #endif
   
     // Active loop that awakens every 100ms
-    finish = false;
-    while (!(*stopFlag || finish))
+    finish_ = false;
+    barrierEvent_ = ServerBarrierEvent_Stop;
+    while (!(*stopFlag || finish_))
     {
       Toolbox::USleep(100 * 1000);
     }
@@ -162,19 +171,22 @@ namespace Orthanc
     signal(SIGINT, NULL);
     signal(SIGQUIT, NULL);
     signal(SIGTERM, NULL);
+    signal(SIGHUP, NULL);
 #endif
+
+    return barrierEvent_;
   }
 
 
-  void Toolbox::ServerBarrier(const bool& stopFlag)
+  ServerBarrierEvent Toolbox::ServerBarrier(const bool& stopFlag)
   {
-    ServerBarrierInternal(&stopFlag);
+    return ServerBarrierInternal(&stopFlag);
   }
 
-  void Toolbox::ServerBarrier()
+  ServerBarrierEvent Toolbox::ServerBarrier()
   {
     const bool stopFlag = false;
-    ServerBarrierInternal(&stopFlag);
+    return ServerBarrierInternal(&stopFlag);
   }
 
 
@@ -205,10 +217,21 @@ namespace Orthanc
   }
 
 
+  static std::streamsize GetStreamSize(std::istream& f)
+  {
+    // http://www.cplusplus.com/reference/iostream/istream/tellg/
+    f.seekg(0, std::ios::end);
+    std::streamsize size = f.tellg();
+    f.seekg(0, std::ios::beg);
+
+    return size;
+  }
+
+
   void Toolbox::ReadFile(std::string& content,
                          const std::string& path) 
   {
-    if (!boost::filesystem::is_regular_file(path))
+    if (!IsRegularFile(path))
     {
       LOG(ERROR) << std::string("The path does not point to a regular file: ") << path;
       throw OrthancException(ErrorCode_RegularFileExpected);
@@ -221,11 +244,7 @@ namespace Orthanc
       throw OrthancException(ErrorCode_InexistentFile);
     }
 
-    // http://www.cplusplus.com/reference/iostream/istream/tellg/
-    f.seekg(0, std::ios::end);
-    std::streamsize size = f.tellg();
-    f.seekg(0, std::ios::beg);
-
+    std::streamsize size = GetStreamSize(f);
     content.resize(size);
     if (size != 0)
     {
@@ -236,6 +255,51 @@ namespace Orthanc
   }
 
 
+  bool Toolbox::ReadHeader(std::string& header,
+                           const std::string& path,
+                           size_t headerSize)
+  {
+    if (!IsRegularFile(path))
+    {
+      LOG(ERROR) << std::string("The path does not point to a regular file: ") << path;
+      throw OrthancException(ErrorCode_RegularFileExpected);
+    }
+
+    boost::filesystem::ifstream f;
+    f.open(path, std::ifstream::in | std::ifstream::binary);
+    if (!f.good())
+    {
+      throw OrthancException(ErrorCode_InexistentFile);
+    }
+
+    bool full = true;
+
+    {
+      std::streamsize size = GetStreamSize(f);
+      if (size <= 0)
+      {
+        headerSize = 0;
+        full = false;
+      }
+      else if (static_cast<size_t>(size) < headerSize)
+      {
+        headerSize = size;  // Truncate to the size of the file
+        full = false;
+      }
+    }
+
+    header.resize(headerSize);
+    if (headerSize != 0)
+    {
+      f.read(reinterpret_cast<char*>(&header[0]), headerSize);
+    }
+
+    f.close();
+
+    return full;
+  }
+
+
   void Toolbox::WriteFile(const void* content,
                           size_t size,
                           const std::string& path)
@@ -268,7 +332,7 @@ namespace Orthanc
   {
     if (boost::filesystem::exists(path))
     {
-      if (boost::filesystem::is_regular_file(path))
+      if (IsRegularFile(path))
       {
         boost::filesystem::remove(path);
       }
@@ -529,12 +593,24 @@ namespace Orthanc
   void Toolbox::DecodeBase64(std::string& result, 
                              const std::string& data)
   {
+    for (size_t i = 0; i < data.length(); i++)
+    {
+      if (!isalnum(data[i]) &&
+          data[i] != '+' &&
+          data[i] != '/' &&
+          data[i] != '=')
+      {
+        // This is not a valid character for a Base64 string
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+
     result = base64_decode(data);
   }
 
 
 #  if BOOST_HAS_REGEX == 1
-  void Toolbox::DecodeDataUriScheme(std::string& mime,
+  bool Toolbox::DecodeDataUriScheme(std::string& mime,
                                     std::string& content,
                                     const std::string& source)
   {
@@ -546,10 +622,11 @@ namespace Orthanc
     {
       mime = what[1];
       DecodeBase64(content, what[2]);
+      return true;
     }
     else
     {
-      throw OrthancException(ErrorCode_BadFileFormat);
+      return false;
     }
   }
 #  endif
@@ -576,7 +653,7 @@ namespace Orthanc
     return std::string(&buffer[0]);
   }
 
-#elif defined(__linux) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
+#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__FreeBSD__)
   static std::string GetPathToExecutableInternal()
   {
     std::vector<char> buffer(PATH_MAX + 1);
@@ -1382,5 +1459,110 @@ namespace Orthanc
     return static_cast<int>(getpid());
 #endif
   }
-}
 
+
+  bool Toolbox::IsRegularFile(const std::string& path)
+  {
+    namespace fs = boost::filesystem;
+
+    try
+    {
+      if (fs::exists(path))
+      {
+        fs::file_status status = fs::status(path);
+        return (status.type() == boost::filesystem::regular_file ||
+                status.type() == boost::filesystem::reparse_file);   // Fix BitBucket issue #11
+      }
+    }
+    catch (fs::filesystem_error&)
+    {
+    }
+
+    return false;
+  }
+
+
+  FILE* Toolbox::OpenFile(const std::string& path,
+                          FileMode mode)
+  {
+#if defined(_WIN32)
+    // TODO Deal with special characters by converting to the current locale
+#endif
+
+    const char* m;
+    switch (mode)
+    {
+      case FileMode_ReadBinary:
+        m = "rb";
+        break;
+
+      case FileMode_WriteBinary:
+        m = "wb";
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    return fopen(path.c_str(), m);
+  }
+
+  
+
+  static bool IsUnreservedCharacter(char c)
+  {
+    // This function checks whether "c" is an unserved character
+    // wrt. an URI percent-encoding
+    // https://en.wikipedia.org/wiki/Percent-encoding#Percent-encoding%5Fin%5Fa%5FURI
+
+    return ((c >= 'A' && c <= 'Z') ||
+            (c >= 'a' && c <= 'z') ||
+            (c >= '0' && c <= '9') ||
+            c == '-' ||
+            c == '_' ||
+            c == '.' ||
+            c == '~');
+  }
+
+  void Toolbox::UriEncode(std::string& target,
+                          const std::string& source)
+  {
+    // Estimate the length of the percent-encoded URI
+    size_t length = 0;
+
+    for (size_t i = 0; i < source.size(); i++)
+    {
+      if (IsUnreservedCharacter(source[i]))
+      {
+        length += 1;
+      }
+      else
+      {
+        // This character must be percent-encoded
+        length += 3;
+      }
+    }
+
+    target.clear();
+    target.reserve(length);
+
+    for (size_t i = 0; i < source.size(); i++)
+    {
+      if (IsUnreservedCharacter(source[i]))
+      {
+        target.push_back(source[i]);
+      }
+      else
+      {
+        // This character must be percent-encoded
+        uint8_t byte = static_cast<uint8_t>(source[i]);
+        uint8_t a = byte >> 4;
+        uint8_t b = byte & 0x0f;
+
+        target.push_back('%');
+        target.push_back(a < 10 ? a + '0' : a - 10 + 'A');
+        target.push_back(b < 10 ? b + '0' : b - 10 + 'A');
+      }
+    }
+  }  
+}
diff --git a/Orthanc/Core/Toolbox.h b/Orthanc/Core/Toolbox.h
index 456a482..cb67473 100644
--- a/Orthanc/Core/Toolbox.h
+++ b/Orthanc/Core/Toolbox.h
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -49,9 +49,9 @@ namespace Orthanc
 
   namespace Toolbox
   {
-    void ServerBarrier(const bool& stopFlag);
+    ServerBarrierEvent ServerBarrier(const bool& stopFlag);
 
-    void ServerBarrier();
+    ServerBarrierEvent ServerBarrier();
 
     void ToUpperCase(std::string& s);  // Inplace version
 
@@ -66,6 +66,10 @@ namespace Orthanc
     void ReadFile(std::string& content,
                   const std::string& path);
 
+    bool ReadHeader(std::string& header,
+                    const std::string& path,
+                    size_t headerSize);
+
     void WriteFile(const std::string& content,
                    const std::string& path);
 
@@ -123,7 +127,7 @@ namespace Orthanc
                       const std::string& data);
 
 #  if BOOST_HAS_REGEX == 1
-    void DecodeDataUriScheme(std::string& mime,
+    bool DecodeDataUriScheme(std::string& mime,
                              std::string& content,
                              const std::string& source);
 #  endif
@@ -190,5 +194,13 @@ namespace Orthanc
                     const std::string& prefix);
 
     int GetProcessId();
+
+    bool IsRegularFile(const std::string& path);
+
+    FILE* OpenFile(const std::string& path,
+                   FileMode mode);
+
+    void UriEncode(std::string& target,
+                   const std::string& source);
   }
 }
diff --git a/Orthanc/Core/WebServiceParameters.cpp b/Orthanc/Core/WebServiceParameters.cpp
new file mode 100644
index 0000000..52e0ea6
--- /dev/null
+++ b/Orthanc/Core/WebServiceParameters.cpp
@@ -0,0 +1,265 @@
+/**
+ * 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 General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PrecompiledHeaders.h"
+#include "WebServiceParameters.h"
+
+#include "../Core/Logging.h"
+#include "../Core/Toolbox.h"
+#include "../Core/OrthancException.h"
+
+#include <cassert>
+
+namespace Orthanc
+{
+  WebServiceParameters::WebServiceParameters() : 
+    advancedFormat_(false),
+    url_("http://127.0.0.1:8042/"),
+    pkcs11Enabled_(false)
+  {
+  }
+
+
+  void WebServiceParameters::ClearClientCertificate()
+  {
+    certificateFile_.clear();
+    certificateKeyFile_.clear();
+    certificateKeyPassword_.clear();
+  }
+
+
+  void WebServiceParameters::SetClientCertificate(const std::string& certificateFile,
+                                                  const std::string& certificateKeyFile,
+                                                  const std::string& certificateKeyPassword)
+  {
+    if (certificateFile.empty())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    if (!Toolbox::IsRegularFile(certificateFile))
+    {
+      LOG(ERROR) << "Cannot open certificate file: " << certificateFile;
+      throw OrthancException(ErrorCode_InexistentFile);
+    }
+
+    if (!certificateKeyFile.empty() && 
+        !Toolbox::IsRegularFile(certificateKeyFile))
+    {
+      LOG(ERROR) << "Cannot open key file: " << certificateKeyFile;
+      throw OrthancException(ErrorCode_InexistentFile);
+    }
+
+    advancedFormat_ = true;
+    certificateFile_ = certificateFile;
+    certificateKeyFile_ = certificateKeyFile;
+    certificateKeyPassword_ = certificateKeyPassword;
+  }
+
+
+  static void AddTrailingSlash(std::string& url)
+  {
+    if (url.size() != 0 && 
+        url[url.size() - 1] != '/')
+    {
+      url += '/';
+    }
+  }
+
+
+  void WebServiceParameters::FromJsonArray(const Json::Value& peer)
+  {
+    assert(peer.isArray());
+
+    advancedFormat_ = false;
+    pkcs11Enabled_ = false;
+
+    if (peer.size() != 1 && 
+        peer.size() != 3)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    std::string url = peer.get(0u, "").asString();
+    if (url.empty())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    AddTrailingSlash(url);
+    SetUrl(url);
+
+    if (peer.size() == 1)
+    {
+      SetUsername("");
+      SetPassword("");
+    }
+    else if (peer.size() == 3)
+    {
+      SetUsername(peer.get(1u, "").asString());
+      SetPassword(peer.get(2u, "").asString());
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  static std::string GetStringMember(const Json::Value& peer,
+                                     const std::string& key,
+                                     const std::string& defaultValue)
+  {
+    if (!peer.isMember(key))
+    {
+      return defaultValue;
+    }
+    else if (peer[key].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+    else
+    {
+      return peer[key].asString();
+    }
+  }
+
+
+  void WebServiceParameters::FromJsonObject(const Json::Value& peer)
+  {
+    assert(peer.isObject());
+    advancedFormat_ = true;
+
+    std::string url = GetStringMember(peer, "Url", "");
+    if (url.empty())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    AddTrailingSlash(url);
+    SetUrl(url);
+
+    SetUsername(GetStringMember(peer, "Username", ""));
+    SetPassword(GetStringMember(peer, "Password", ""));
+
+    if (peer.isMember("CertificateFile"))
+    {
+      SetClientCertificate(GetStringMember(peer, "CertificateFile", ""),
+                           GetStringMember(peer, "CertificateKeyFile", ""),
+                           GetStringMember(peer, "CertificateKeyPassword", ""));
+    }
+
+    if (peer.isMember("Pkcs11"))
+    {
+      if (peer["Pkcs11"].type() == Json::booleanValue)
+      {
+        pkcs11Enabled_ = peer["Pkcs11"].asBool();
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+  }
+
+
+  void WebServiceParameters::FromJson(const Json::Value& peer)
+  {
+    try
+    {
+      if (peer.isArray())
+      {
+        FromJsonArray(peer);
+      }
+      else if (peer.isObject())
+      {
+        FromJsonObject(peer);
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+    catch (OrthancException&)
+    {
+      throw;
+    }
+    catch (...)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  void WebServiceParameters::ToJson(Json::Value& value) const
+  {
+    if (advancedFormat_)
+    {
+      value = Json::objectValue;
+      value["Url"] = url_;
+
+      if (!username_.empty() ||
+          !password_.empty())
+      {
+        value["Username"] = username_;
+        value["Password"] = password_;
+      }
+
+      if (!certificateFile_.empty())
+      {
+        value["CertificateFile"] = certificateFile_;
+      }
+
+      if (!certificateKeyFile_.empty())
+      {
+        value["CertificateKeyFile"] = certificateKeyFile_;
+      }
+
+      if (!certificateKeyPassword_.empty())
+      {
+        value["CertificateKeyPassword"] = certificateKeyPassword_;
+      }
+    }
+    else
+    {
+      value = Json::arrayValue;
+      value.append(url_);
+
+      if (!username_.empty() ||
+          !password_.empty())
+      {
+        value.append(username_);
+        value.append(password_);
+      }
+    }
+  }
+}
diff --git a/Orthanc/Core/WebServiceParameters.h b/Orthanc/Core/WebServiceParameters.h
new file mode 100644
index 0000000..40d6cf9
--- /dev/null
+++ b/Orthanc/Core/WebServiceParameters.h
@@ -0,0 +1,124 @@
+/**
+ * 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 General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <string>
+#include <json/json.h>
+
+namespace Orthanc
+{
+  class WebServiceParameters
+  {
+  private:
+    bool        advancedFormat_;
+    std::string url_;
+    std::string username_;
+    std::string password_;
+    std::string certificateFile_;
+    std::string certificateKeyFile_;
+    std::string certificateKeyPassword_;
+    bool        pkcs11Enabled_;
+
+    void FromJsonArray(const Json::Value& peer);
+
+    void FromJsonObject(const Json::Value& peer);
+
+  public:
+    WebServiceParameters();
+
+    const std::string& GetUrl() const
+    {
+      return url_;
+    }
+
+    void SetUrl(const std::string& url)
+    {
+      url_ = url;
+    }
+
+    const std::string& GetUsername() const
+    {
+      return username_;
+    }
+
+    void SetUsername(const std::string& username)
+    {
+      username_ = username;
+    }
+    
+    const std::string& GetPassword() const
+    {
+      return password_;
+    }
+
+    void SetPassword(const std::string& password)
+    {
+      password_ = password;
+    }
+
+    void ClearClientCertificate();
+
+    void SetClientCertificate(const std::string& certificateFile,
+                              const std::string& certificateKeyFile,
+                              const std::string& certificateKeyPassword);
+
+    const std::string& GetCertificateFile() const
+    {
+      return certificateFile_;
+    }
+
+    const std::string& GetCertificateKeyFile() const
+    {
+      return certificateKeyFile_;
+    }
+
+    const std::string& GetCertificateKeyPassword() const
+    {
+      return certificateKeyPassword_;
+    }
+
+    void SetPkcs11Enabled(bool pkcs11Enabled)
+    {
+      pkcs11Enabled_ = pkcs11Enabled;
+    }
+
+    bool IsPkcs11Enabled() const
+    {
+      return pkcs11Enabled_;
+    }
+
+    void FromJson(const Json::Value& peer);
+
+    void ToJson(Json::Value& value) const;
+  };
+}
diff --git a/Orthanc/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp b/Orthanc/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
new file mode 100644
index 0000000..6a7dbc0
--- /dev/null
+++ b/Orthanc/Plugins/Samples/Common/OrthancPluginCppWrapper.cpp
@@ -0,0 +1,829 @@
+/**
+ * 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 General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "OrthancPluginCppWrapper.h"
+
+#include <json/reader.h>
+
+
+namespace OrthancPlugins
+{
+  const char* PluginException::GetErrorDescription(OrthancPluginContext* context) const
+  {
+    const char* description = OrthancPluginGetErrorDescription(context, code_);
+    if (description)
+    {
+      return description;
+    }
+    else
+    {
+      return "No description available";
+    }
+  }
+
+
+  MemoryBuffer::MemoryBuffer(OrthancPluginContext* context) : 
+    context_(context)
+  {
+    buffer_.data = NULL;
+    buffer_.size = 0;
+  }
+
+
+  void MemoryBuffer::Clear()
+  {
+    if (buffer_.data != NULL)
+    {
+      OrthancPluginFreeMemoryBuffer(context_, &buffer_);
+      buffer_.data = NULL;
+      buffer_.size = 0;
+    }
+  }
+
+
+  void MemoryBuffer::Assign(OrthancPluginMemoryBuffer& other)
+  {
+    Clear();
+
+    buffer_.data = other.data;
+    buffer_.size = other.size;
+
+    other.data = NULL;
+    other.size = 0;
+  }
+
+
+  void MemoryBuffer::ToString(std::string& target) const
+  {
+    if (buffer_.size == 0)
+    {
+      target.clear();
+    }
+    else
+    {
+      target.assign(reinterpret_cast<const char*>(buffer_.data), buffer_.size);
+    }
+  }
+
+
+  void MemoryBuffer::ToJson(Json::Value& target) const
+  {
+    if (buffer_.data == NULL ||
+        buffer_.size == 0)
+    {
+      throw PluginException(OrthancPluginErrorCode_InternalError);
+    }
+
+    const char* tmp = reinterpret_cast<const char*>(buffer_.data);
+
+    Json::Reader reader;
+    if (!reader.parse(tmp, tmp + buffer_.size, target))
+    {
+      OrthancPluginLogError(context_, "Cannot convert some memory buffer to JSON");
+      throw PluginException(OrthancPluginErrorCode_BadFileFormat);
+    }
+  }
+
+
+  bool MemoryBuffer::RestApiGet(const std::string& uri,
+                                bool applyPlugins)
+  {
+    Clear();
+
+    OrthancPluginErrorCode error;
+
+    if (applyPlugins)
+    {
+      error = OrthancPluginRestApiGetAfterPlugins(context_, &buffer_, uri.c_str());
+    }
+    else
+    {
+      error = OrthancPluginRestApiGet(context_, &buffer_, uri.c_str());
+    }
+
+    if (error == OrthancPluginErrorCode_Success)
+    {
+      return true;
+    }
+    else if (error == OrthancPluginErrorCode_UnknownResource ||
+             error == OrthancPluginErrorCode_InexistentItem)
+    {
+      return false;
+    }
+    else
+    {
+      throw PluginException(error);
+    }
+  }
+
+  
+  bool MemoryBuffer::RestApiPost(const std::string& uri,
+                                 const char* body,
+                                 size_t bodySize,
+                                 bool applyPlugins)
+  {
+    Clear();
+
+    OrthancPluginErrorCode error;
+
+    if (applyPlugins)
+    {
+      error = OrthancPluginRestApiPostAfterPlugins(context_, &buffer_, uri.c_str(), body, bodySize);
+    }
+    else
+    {
+      error = OrthancPluginRestApiPost(context_, &buffer_, uri.c_str(), body, bodySize);
+    }
+
+    if (error == OrthancPluginErrorCode_Success)
+    {
+      return true;
+    }
+    else if (error == OrthancPluginErrorCode_UnknownResource ||
+             error == OrthancPluginErrorCode_InexistentItem)
+    {
+      return false;
+    }
+    else
+    {
+      throw PluginException(error);
+    }
+  }
+
+
+  bool MemoryBuffer::RestApiPut(const std::string& uri,
+                                const char* body,
+                                size_t bodySize,
+                                bool applyPlugins)
+  {
+    Clear();
+
+    OrthancPluginErrorCode error;
+
+    if (applyPlugins)
+    {
+      error = OrthancPluginRestApiPutAfterPlugins(context_, &buffer_, uri.c_str(), body, bodySize);
+    }
+    else
+    {
+      error = OrthancPluginRestApiPut(context_, &buffer_, uri.c_str(), body, bodySize);
+    }
+
+    if (error == OrthancPluginErrorCode_Success)
+    {
+      return true;
+    }
+    else if (error == OrthancPluginErrorCode_UnknownResource ||
+             error == OrthancPluginErrorCode_InexistentItem)
+    {
+      return false;
+    }
+    else
+    {
+      throw PluginException(error);
+    }
+  }
+
+
+  OrthancString::OrthancString(OrthancPluginContext* context,
+                               char* str) :
+    context_(context),
+    str_(str)
+  {
+  }
+
+
+  void OrthancString::Clear()
+  {
+    if (str_ != NULL)
+    {
+      OrthancPluginFreeString(context_, str_);
+      str_ = NULL;
+    }
+  }
+
+
+  void OrthancString::ToString(std::string& target) const
+  {
+    if (str_ == NULL)
+    {
+      target.clear();
+    }
+    else
+    {
+      target.assign(str_);
+    }
+  }
+
+
+  void OrthancString::ToJson(Json::Value& target) const
+  {
+    if (str_ == NULL)
+    {
+      OrthancPluginLogError(context_, "Cannot convert an empty memory buffer to JSON");
+      throw PluginException(OrthancPluginErrorCode_InternalError);
+    }
+
+    Json::Reader reader;
+    if (!reader.parse(str_, target))
+    {
+      OrthancPluginLogError(context_, "Cannot convert some memory buffer to JSON");
+      throw PluginException(OrthancPluginErrorCode_BadFileFormat);
+    }
+  }
+  
+
+  OrthancConfiguration::OrthancConfiguration(OrthancPluginContext* context) : 
+    context_(context)
+  {
+    OrthancString str(context, OrthancPluginGetConfiguration(context));
+
+    if (str.GetContent() == NULL)
+    {
+      OrthancPluginLogError(context, "Cannot access the Orthanc configuration");
+      throw PluginException(OrthancPluginErrorCode_InternalError);
+    }
+
+    str.ToJson(configuration_);
+
+    if (configuration_.type() != Json::objectValue)
+    {
+      OrthancPluginLogError(context, "Unable to read the Orthanc configuration");
+      throw PluginException(OrthancPluginErrorCode_InternalError);
+    }
+  }
+
+
+  OrthancPluginContext* OrthancConfiguration::GetContext() const
+  {
+    if (context_ == NULL)
+    {
+      throw PluginException(OrthancPluginErrorCode_Plugin);
+    }
+    else
+    {
+      return context_;
+    }
+  }
+
+
+  std::string OrthancConfiguration::GetPath(const std::string& key) const
+  {
+    if (path_.empty())
+    {
+      return key;
+    }
+    else
+    {
+      return path_ + "." + key;
+    }
+  }
+
+
+  void OrthancConfiguration::GetSection(OrthancConfiguration& target,
+                                        const std::string& key) const
+  {
+    assert(configuration_.type() == Json::objectValue);
+
+    target.context_ = context_;
+    target.path_ = GetPath(key);
+
+    if (!configuration_.isMember(key))
+    {
+      target.configuration_ = Json::objectValue;
+    }
+    else
+    {
+      if (configuration_[key].type() != Json::objectValue)
+      {
+        if (context_ != NULL)
+        {
+          std::string s = "The configuration section \"" + target.path_ + "\" is not an associative array as expected";
+          OrthancPluginLogError(context_, s.c_str());
+        }
+
+        throw PluginException(OrthancPluginErrorCode_BadFileFormat);
+      }
+
+      target.configuration_ = configuration_[key];
+    }
+  }
+
+
+  bool OrthancConfiguration::LookupStringValue(std::string& target,
+                                               const std::string& key) const
+  {
+    assert(configuration_.type() == Json::objectValue);
+
+    if (!configuration_.isMember(key))
+    {
+      return false;
+    }
+
+    if (configuration_[key].type() != Json::stringValue)
+    {
+      if (context_ != NULL)
+      {
+        std::string s = "The configuration option \"" + GetPath(key) + "\" is not a string as expected";
+        OrthancPluginLogError(context_, s.c_str());
+      }
+
+      throw PluginException(OrthancPluginErrorCode_BadFileFormat);
+    }
+
+    target = configuration_[key].asString();
+    return true;
+  }
+
+
+  bool OrthancConfiguration::LookupIntegerValue(int& target,
+                                                const std::string& key) const
+  {
+    assert(configuration_.type() == Json::objectValue);
+
+    if (!configuration_.isMember(key))
+    {
+      return false;
+    }
+
+    switch (configuration_[key].type())
+    {
+      case Json::intValue:
+        target = configuration_[key].asInt();
+        return true;
+        
+      case Json::uintValue:
+        target = configuration_[key].asUInt();
+        return true;
+        
+      default:
+        if (context_ != NULL)
+        {
+          std::string s = "The configuration option \"" + GetPath(key) + "\" is not an integer as expected";
+          OrthancPluginLogError(context_, s.c_str());
+        }
+
+        throw PluginException(OrthancPluginErrorCode_BadFileFormat);
+    }
+  }
+
+
+  bool OrthancConfiguration::LookupUnsignedIntegerValue(unsigned int& target,
+                                                        const std::string& key) const
+  {
+    int tmp;
+    if (!LookupIntegerValue(tmp, key))
+    {
+      return false;
+    }
+
+    if (tmp < 0)
+    {
+      if (context_ != NULL)
+      {
+        std::string s = "The configuration option \"" + GetPath(key) + "\" is not a positive integer as expected";
+        OrthancPluginLogError(context_, s.c_str());
+      }
+
+      throw PluginException(OrthancPluginErrorCode_BadFileFormat);
+    }
+    else
+    {
+      target = static_cast<unsigned int>(tmp);
+      return true;
+    }
+  }
+
+
+  bool OrthancConfiguration::LookupBooleanValue(bool& target,
+                                                const std::string& key) const
+  {
+    assert(configuration_.type() == Json::objectValue);
+
+    if (!configuration_.isMember(key))
+    {
+      return false;
+    }
+
+    if (configuration_[key].type() != Json::booleanValue)
+    {
+      if (context_ != NULL)
+      {
+        std::string s = "The configuration option \"" + GetPath(key) + "\" is not a Boolean as expected";
+        OrthancPluginLogError(context_, s.c_str());
+      }
+
+      throw PluginException(OrthancPluginErrorCode_BadFileFormat);
+    }
+
+    target = configuration_[key].asBool();
+    return true;
+  }
+
+
+  bool OrthancConfiguration::LookupFloatValue(float& target,
+                                              const std::string& key) const
+  {
+    assert(configuration_.type() == Json::objectValue);
+
+    if (!configuration_.isMember(key))
+    {
+      return false;
+    }
+
+    switch (configuration_[key].type())
+    {
+      case Json::realValue:
+        target = configuration_[key].asFloat();
+        return true;
+        
+      case Json::intValue:
+        target = configuration_[key].asInt();
+        return true;
+        
+      case Json::uintValue:
+        target = configuration_[key].asUInt();
+        return true;
+        
+      default:
+        if (context_ != NULL)
+        {
+          std::string s = "The configuration option \"" + GetPath(key) + "\" is not an integer as expected";
+          OrthancPluginLogError(context_, s.c_str());
+        }
+
+        throw PluginException(OrthancPluginErrorCode_BadFileFormat);
+    }
+  }
+
+  
+  std::string OrthancConfiguration::GetStringValue(const std::string& key,
+                                                   const std::string& defaultValue) const
+  {
+    std::string tmp;
+    if (LookupStringValue(tmp, key))
+    {
+      return tmp;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  int OrthancConfiguration::GetIntegerValue(const std::string& key,
+                                            int defaultValue) const
+  {
+    int tmp;
+    if (LookupIntegerValue(tmp, key))
+    {
+      return tmp;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  unsigned int OrthancConfiguration::GetUnsignedIntegerValue(const std::string& key,
+                                                             unsigned int defaultValue) const
+  {
+    unsigned int tmp;
+    if (LookupUnsignedIntegerValue(tmp, key))
+    {
+      return tmp;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  bool OrthancConfiguration::GetBooleanValue(const std::string& key,
+                                             bool defaultValue) const
+  {
+    bool tmp;
+    if (LookupBooleanValue(tmp, key))
+    {
+      return tmp;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  float OrthancConfiguration::GetFloatValue(const std::string& key,
+                                            float defaultValue) const
+  {
+    float tmp;
+    if (LookupFloatValue(tmp, key))
+    {
+      return tmp;
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
+
+  void OrthancImage::Clear()
+  {
+    if (image_ != NULL)
+    {
+      OrthancPluginFreeImage(context_, image_);
+      image_ = NULL;
+    }
+  }
+
+
+  void OrthancImage::CheckImageAvailable()
+  {
+    if (image_ == NULL)
+    {
+      OrthancPluginLogError(context_, "Trying to access a NULL image");
+      throw PluginException(OrthancPluginErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  OrthancImage::OrthancImage(OrthancPluginContext*  context) :
+    context_(context),
+    image_(NULL)
+  {
+    if (context == NULL)
+    {
+      throw PluginException(OrthancPluginErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  OrthancImage::OrthancImage(OrthancPluginContext*  context,
+                             OrthancPluginImage*    image) :
+    context_(context),
+    image_(image)
+  {
+    if (context == NULL)
+    {
+      throw PluginException(OrthancPluginErrorCode_ParameterOutOfRange);
+    }
+  }
+  
+
+  OrthancImage::OrthancImage(OrthancPluginContext*     context,
+                             OrthancPluginPixelFormat  format,
+                             uint32_t                  width,
+                             uint32_t                  height) :
+    context_(context)
+  {
+    if (context == NULL)
+    {
+      throw PluginException(OrthancPluginErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      image_ = OrthancPluginCreateImage(context, format, width, height);
+    }
+  }
+
+
+  void OrthancImage::UncompressPngImage(const void* data,
+                                        size_t size)
+  {
+    Clear();
+    image_ = OrthancPluginUncompressImage(context_, data, size, OrthancPluginImageFormat_Png);
+    if (image_ == NULL)
+    {
+      OrthancPluginLogError(context_, "Cannot uncompress a PNG image");
+      throw PluginException(OrthancPluginErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void OrthancImage::UncompressJpegImage(const void* data,
+                                         size_t size)
+  {
+    Clear();
+    image_ = OrthancPluginUncompressImage(context_, data, size, OrthancPluginImageFormat_Jpeg);
+    if (image_ == NULL)
+    {
+      OrthancPluginLogError(context_, "Cannot uncompress a JPEG image");
+      throw PluginException(OrthancPluginErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  void OrthancImage::DecodeDicomImage(const void* data,
+                                      size_t size,
+                                      unsigned int frame)
+  {
+    Clear();
+    image_ = OrthancPluginDecodeDicomImage(context_, data, size, frame);
+    if (image_ == NULL)
+    {
+      OrthancPluginLogError(context_, "Cannot uncompress a DICOM image");
+      throw PluginException(OrthancPluginErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  OrthancPluginPixelFormat OrthancImage::GetPixelFormat()
+  {
+    CheckImageAvailable();
+    return OrthancPluginGetImagePixelFormat(context_, image_);
+  }
+
+
+  unsigned int OrthancImage::GetWidth()
+  {
+    CheckImageAvailable();
+    return OrthancPluginGetImageWidth(context_, image_);
+  }
+
+
+  unsigned int OrthancImage::GetHeight()
+  {
+    CheckImageAvailable();
+    return OrthancPluginGetImageHeight(context_, image_);
+  }
+
+
+  unsigned int OrthancImage::GetPitch()
+  {
+    CheckImageAvailable();
+    return OrthancPluginGetImagePitch(context_, image_);
+  }
+
+    
+  const void* OrthancImage::GetBuffer()
+  {
+    CheckImageAvailable();
+    return OrthancPluginGetImageBuffer(context_, image_);
+  }
+
+
+  void OrthancImage::CompressPngImage(MemoryBuffer& target)
+  {
+    CheckImageAvailable();
+    
+    OrthancPluginMemoryBuffer tmp;
+    OrthancPluginCompressPngImage(context_, &tmp, GetPixelFormat(), 
+                                  GetWidth(), GetHeight(), GetPitch(), GetBuffer());
+
+    target.Assign(tmp);
+  }
+
+
+  void OrthancImage::CompressJpegImage(MemoryBuffer& target,
+                                       uint8_t quality)
+  {
+    CheckImageAvailable();
+    
+    OrthancPluginMemoryBuffer tmp;
+    OrthancPluginCompressJpegImage(context_, &tmp, GetPixelFormat(), 
+                                   GetWidth(), GetHeight(), GetPitch(), GetBuffer(), quality);
+    
+    target.Assign(tmp);
+  }
+
+
+  void OrthancImage::AnswerPngImage(OrthancPluginRestOutput* output)
+  {
+    CheckImageAvailable();
+    OrthancPluginCompressAndAnswerPngImage(context_, output, GetPixelFormat(),
+                                           GetWidth(), GetHeight(), GetPitch(), GetBuffer());
+  }
+
+
+  void OrthancImage::AnswerJpegImage(OrthancPluginRestOutput* output,
+                                     uint8_t quality)
+  {
+    CheckImageAvailable();
+    OrthancPluginCompressAndAnswerJpegImage(context_, output, GetPixelFormat(),
+                                            GetWidth(), GetHeight(), GetPitch(), GetBuffer(), quality);
+  }
+
+
+  bool RestApiGetJson(Json::Value& result,
+                      OrthancPluginContext* context,
+                      const std::string& uri,
+                      bool applyPlugins)
+  {
+    MemoryBuffer answer(context);
+    if (!answer.RestApiGet(uri, applyPlugins))
+    {
+      return false;
+    }
+    else
+    {
+      answer.ToJson(result);
+      return true;
+    }
+  }
+
+
+  bool RestApiPostJson(Json::Value& result,
+                       OrthancPluginContext* context,
+                       const std::string& uri,
+                       const char* body,
+                       size_t bodySize,
+                       bool applyPlugins)
+  {
+    MemoryBuffer answer(context);
+    if (!answer.RestApiPost(uri, body, bodySize, applyPlugins))
+    {
+      return false;
+    }
+    else
+    {
+      answer.ToJson(result);
+      return true;
+    }
+  }
+
+
+  bool RestApiPutJson(Json::Value& result,
+                      OrthancPluginContext* context,
+                      const std::string& uri,
+                      const char* body,
+                      size_t bodySize,
+                      bool applyPlugins)
+  {
+    MemoryBuffer answer(context);
+    if (!answer.RestApiPut(uri, body, bodySize, applyPlugins))
+    {
+      return false;
+    }
+    else
+    {
+      answer.ToJson(result);
+      return true;
+    }
+  }
+
+
+  bool RestApiDelete(OrthancPluginContext* context,
+                     const std::string& uri,
+                     bool applyPlugins)
+  {
+    OrthancPluginErrorCode error;
+
+    if (applyPlugins)
+    {
+      error = OrthancPluginRestApiDeleteAfterPlugins(context, uri.c_str());
+    }
+    else
+    {
+      error = OrthancPluginRestApiDelete(context, uri.c_str());
+    }
+
+    if (error == OrthancPluginErrorCode_Success)
+    {
+      return true;
+    }
+    else if (error == OrthancPluginErrorCode_UnknownResource ||
+             error == OrthancPluginErrorCode_InexistentItem)
+    {
+      return false;
+    }
+    else
+    {
+      throw PluginException(error);
+    }
+  }
+}
+
diff --git a/Orthanc/Plugins/Samples/Common/OrthancPluginCppWrapper.h b/Orthanc/Plugins/Samples/Common/OrthancPluginCppWrapper.h
new file mode 100644
index 0000000..33b550e
--- /dev/null
+++ b/Orthanc/Plugins/Samples/Common/OrthancPluginCppWrapper.h
@@ -0,0 +1,384 @@
+/**
+ * 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 General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include <orthanc/OrthancCPlugin.h>
+#include <boost/noncopyable.hpp>
+#include <boost/lexical_cast.hpp>
+#include <json/value.h>
+
+#if HAS_ORTHANC_EXCEPTION == 1
+#  include <OrthancException.h>
+#endif
+
+
+namespace OrthancPlugins
+{
+  typedef void (*RestCallback) (OrthancPluginRestOutput* output,
+                                const char* url,
+                                const OrthancPluginHttpRequest* request);
+
+
+  class PluginException
+  {
+  private:
+    OrthancPluginErrorCode  code_;
+
+  public:
+    PluginException(OrthancPluginErrorCode code) : code_(code)
+    {
+    }
+
+    OrthancPluginErrorCode GetErrorCode() const
+    {
+      return code_;
+    }
+
+    const char* GetErrorDescription(OrthancPluginContext* context) const;
+  };
+
+
+  class MemoryBuffer : public boost::noncopyable
+  {
+  private:
+    OrthancPluginContext*      context_;
+    OrthancPluginMemoryBuffer  buffer_;
+
+  public:
+    MemoryBuffer(OrthancPluginContext* context);
+
+    ~MemoryBuffer()
+    {
+      Clear();
+    }
+
+    OrthancPluginMemoryBuffer* operator*()
+    {
+      return &buffer_;
+    }
+
+    // This transfers ownership
+    void Assign(OrthancPluginMemoryBuffer& other);
+
+    const char* GetData() const
+    {
+      if (buffer_.size > 0)
+      {
+        return reinterpret_cast<const char*>(buffer_.data);
+      }
+      else
+      {
+        return NULL;
+      }
+    }
+
+    size_t GetSize() const
+    {
+      return buffer_.size;
+    }
+
+    void Clear();
+
+    void ToString(std::string& target) const;
+
+    void ToJson(Json::Value& target) const;
+
+    bool RestApiGet(const std::string& uri,
+                    bool applyPlugins);
+
+    bool RestApiPost(const std::string& uri,
+                     const char* body,
+                     size_t bodySize,
+                     bool applyPlugins);
+
+    bool RestApiPut(const std::string& uri,
+                    const char* body,
+                    size_t bodySize,
+                    bool applyPlugins);
+
+    bool RestApiPost(const std::string& uri,
+                     const std::string& body,
+                     bool applyPlugins)
+    {
+      return RestApiPost(uri, body.empty() ? NULL : body.c_str(), body.size(), applyPlugins);
+    }
+
+    bool RestApiPut(const std::string& uri,
+                    const std::string& body,
+                    bool applyPlugins)
+    {
+      return RestApiPut(uri, body.empty() ? NULL : body.c_str(), body.size(), applyPlugins);
+    }
+  };
+
+
+  class OrthancString : public boost::noncopyable
+  {
+  private:
+    OrthancPluginContext*  context_;
+    char*                  str_;
+
+  public:
+    OrthancString(OrthancPluginContext* context,
+                  char* str);
+
+    ~OrthancString()
+    {
+      Clear();
+    }
+
+    void Clear();
+
+    const char* GetContent() const
+    {
+      return str_;
+    }
+
+    void ToString(std::string& target) const;
+
+    void ToJson(Json::Value& target) const;
+  };
+
+
+  class OrthancConfiguration : public boost::noncopyable
+  {
+  private:
+    OrthancPluginContext*  context_;
+    Json::Value            configuration_;
+    std::string            path_;
+
+    std::string GetPath(const std::string& key) const;
+
+  public:
+    OrthancConfiguration() : context_(NULL)
+    {
+    }
+
+    OrthancConfiguration(OrthancPluginContext* context);
+
+    OrthancPluginContext* GetContext() const;
+
+    const Json::Value& GetJson() const
+    {
+      return configuration_;
+    }
+
+    void GetSection(OrthancConfiguration& target,
+                    const std::string& key) const;
+
+    bool LookupStringValue(std::string& target,
+                           const std::string& key) const;
+    
+    bool LookupIntegerValue(int& target,
+                            const std::string& key) const;
+
+    bool LookupUnsignedIntegerValue(unsigned int& target,
+                                    const std::string& key) const;
+
+    bool LookupBooleanValue(bool& target,
+                            const std::string& key) const;
+
+    bool LookupFloatValue(float& target,
+                          const std::string& key) const;
+
+    std::string GetStringValue(const std::string& key,
+                               const std::string& defaultValue) const;
+
+    int GetIntegerValue(const std::string& key,
+                        int defaultValue) const;
+
+    unsigned int GetUnsignedIntegerValue(const std::string& key,
+                                         unsigned int defaultValue) const;
+
+    bool GetBooleanValue(const std::string& key,
+                         bool defaultValue) const;
+
+    float GetFloatValue(const std::string& key,
+                        float defaultValue) const;
+  };
+
+  class OrthancImage
+  {
+  private:
+    OrthancPluginContext*  context_;
+    OrthancPluginImage*    image_;
+
+    void Clear();
+
+    void CheckImageAvailable();
+
+  public:
+    OrthancImage(OrthancPluginContext*  context);
+
+    OrthancImage(OrthancPluginContext*  context,
+                 OrthancPluginImage*    image);
+
+    OrthancImage(OrthancPluginContext*     context,
+                 OrthancPluginPixelFormat  format,
+                 uint32_t                  width,
+                 uint32_t                  height);
+
+    ~OrthancImage()
+    {
+      Clear();
+    }
+
+    void UncompressPngImage(const void* data,
+                            size_t size);
+
+    void UncompressJpegImage(const void* data,
+                             size_t size);
+
+    void DecodeDicomImage(const void* data,
+                          size_t size,
+                          unsigned int frame);
+
+    OrthancPluginPixelFormat GetPixelFormat();
+
+    unsigned int GetWidth();
+
+    unsigned int GetHeight();
+
+    unsigned int GetPitch();
+    
+    const void* GetBuffer();
+
+    void CompressPngImage(MemoryBuffer& target);
+
+    void CompressJpegImage(MemoryBuffer& target,
+                           uint8_t quality);
+
+    void AnswerPngImage(OrthancPluginRestOutput* output);
+
+    void AnswerJpegImage(OrthancPluginRestOutput* output,
+                         uint8_t quality);
+  };
+
+
+  bool RestApiGetJson(Json::Value& result,
+                      OrthancPluginContext* context,
+                      const std::string& uri,
+                      bool applyPlugins);
+
+  bool RestApiPostJson(Json::Value& result,
+                       OrthancPluginContext* context,
+                       const std::string& uri,
+                       const char* body,
+                       size_t bodySize,
+                       bool applyPlugins);
+
+  bool RestApiPutJson(Json::Value& result,
+                      OrthancPluginContext* context,
+                      const std::string& uri,
+                      const char* body,
+                      size_t bodySize,
+                      bool applyPlugins);
+
+  inline bool RestApiPostJson(Json::Value& result,
+                              OrthancPluginContext* context,
+                              const std::string& uri,
+                              const std::string& body,
+                              bool applyPlugins)
+  {
+    return RestApiPostJson(result, context, uri, body.empty() ? NULL : body.c_str(), 
+                           body.size(), applyPlugins);
+  }
+
+  bool RestApiDelete(OrthancPluginContext* context,
+                     const std::string& uri,
+                     bool applyPlugins);
+
+  inline bool RestApiPutJson(Json::Value& result,
+                             OrthancPluginContext* context,
+                             const std::string& uri,
+                             const std::string& body,
+                             bool applyPlugins)
+  {
+    return RestApiPutJson(result, context, uri, body.empty() ? NULL : body.c_str(), 
+                          body.size(), applyPlugins);
+  }
+
+  bool RestApiDelete(OrthancPluginContext* context,
+                     const std::string& uri,
+                     bool applyPlugins);
+
+
+  namespace Internals
+  {
+    template <RestCallback Callback>
+    OrthancPluginErrorCode Protect(OrthancPluginRestOutput* output,
+                                   const char* url,
+                                   const OrthancPluginHttpRequest* request)
+    {
+      try
+      {
+        Callback(output, url, request);
+        return OrthancPluginErrorCode_Success;
+      }
+      catch (OrthancPlugins::PluginException& e)
+      {
+        return e.GetErrorCode();
+      }
+#if HAS_ORTHANC_EXCEPTION == 1
+      catch (Orthanc::OrthancException& e)
+      {
+        return static_cast<OrthancPluginErrorCode>(e.GetErrorCode());
+      }
+#endif
+      catch (boost::bad_lexical_cast&)
+      {
+        return OrthancPluginErrorCode_BadFileFormat;
+      }
+      catch (...)
+      {
+        return OrthancPluginErrorCode_Plugin;
+      }
+    }
+  }
+
+  
+  template <RestCallback Callback>
+  void RegisterRestCallback(OrthancPluginContext* context,
+                            const std::string& uri,
+                            bool isThreadSafe)
+  {
+    if (isThreadSafe)
+    {
+      OrthancPluginRegisterRestCallbackNoLock(context, uri.c_str(), Internals::Protect<Callback>);
+    }
+    else
+    {
+      OrthancPluginRegisterRestCallback(context, uri.c_str(), Internals::Protect<Callback>);
+    }
+  }
+}
diff --git a/Orthanc/Resources/CMake/BoostConfiguration.cmake b/Orthanc/Resources/CMake/BoostConfiguration.cmake
index eb42dde..20c50df 100644
--- a/Orthanc/Resources/CMake/BoostConfiguration.cmake
+++ b/Orthanc/Resources/CMake/BoostConfiguration.cmake
@@ -8,7 +8,7 @@ else()
   #set(Boost_USE_STATIC_LIBS ON)
 
   find_package(Boost
-    COMPONENTS filesystem thread system date_time regex locale)
+    COMPONENTS filesystem thread system date_time regex locale ${ORTHANC_BOOST_COMPONENTS})
 
   if (NOT Boost_FOUND)
     message(FATAL_ERROR "Unable to locate Boost on this system")
@@ -39,10 +39,10 @@ endif()
 
 
 if (BOOST_STATIC)
-  # Parameters for Boost 1.59.0
-  set(BOOST_NAME boost_1_59_0)
-  set(BOOST_BCP_SUFFIX bcpdigest-0.9.5)
-  set(BOOST_MD5 "08abb7cdbea0b380f9ab0d5cce476f12")
+  # Parameters for Boost 1.60.0
+  set(BOOST_NAME boost_1_60_0)
+  set(BOOST_BCP_SUFFIX bcpdigest-1.0.1)
+  set(BOOST_MD5 "a789f8ec2056ad1c2d5f0cb64687cc7b")
   set(BOOST_URL "http://www.montefiore.ulg.ac.be/~jodogne/Orthanc/ThirdPartyDownloads/${BOOST_NAME}_${BOOST_BCP_SUFFIX}.tar.gz")
   set(BOOST_FILESYSTEM_SOURCES_DIR "${BOOST_NAME}/libs/filesystem/src") 
   set(BOOST_SOURCES_DIR ${CMAKE_BINARY_DIR}/${BOOST_NAME})
@@ -61,6 +61,8 @@ if (BOOST_STATIC)
       )
     add_definitions(
       -DBOOST_LOCALE_WITH_ICONV=1
+      -DBOOST_LOCALE_NO_WINAPI_BACKEND=1
+      -DBOOST_LOCALE_NO_STD_BACKEND=1
       )
 
     if ("${CMAKE_SYSTEM_VERSION}" STREQUAL "LinuxStandardBase")
@@ -86,6 +88,10 @@ if (BOOST_STATIC)
       add_definitions(-DBOOST_LOCALE_WITH_WCONV=1)
     endif()
 
+    add_definitions(
+      -DBOOST_LOCALE_NO_POSIX_BACKEND=1
+      -DBOOST_LOCALE_NO_STD_BACKEND=1
+      )
   else()
     message(FATAL_ERROR "Support your platform here")
   endif()
@@ -109,6 +115,55 @@ if (BOOST_STATIC)
     ${BOOST_SOURCES_DIR}/libs/system/src/error_code.cpp
     )
 
+  if (USE_BOOST_LOCALE_BACKENDS)
+    if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
+        ${CMAKE_SYSTEM_NAME} STREQUAL "Darwin" OR
+        ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD" OR
+        ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD")
+      list(APPEND BOOST_SOURCES
+        ${BOOST_SOURCES_DIR}/libs/locale/src/posix/codecvt.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/posix/collate.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/posix/converter.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/posix/numeric.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/posix/posix_backend.cpp
+        )
+    elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
+      list(APPEND BOOST_SOURCES
+        ${BOOST_SOURCES_DIR}/libs/locale/src/win32/collate.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/win32/converter.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/win32/lcid.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/win32/numeric.cpp
+        ${BOOST_SOURCES_DIR}/libs/locale/src/win32/win_backend.cpp
+        )
+    else()
+      message(FATAL_ERROR "Support your platform here")
+    endif()
+
+    list(APPEND BOOST_SOURCES
+      ${BOOST_REGEX_SOURCES}
+      ${BOOST_SOURCES_DIR}/libs/date_time/src/gregorian/greg_month.cpp
+      ${BOOST_SOURCES_DIR}/libs/system/src/error_code.cpp
+
+      ${BOOST_FILESYSTEM_SOURCES_DIR}/codecvt_error_category.cpp
+      ${BOOST_FILESYSTEM_SOURCES_DIR}/operations.cpp
+      ${BOOST_FILESYSTEM_SOURCES_DIR}/path.cpp
+      ${BOOST_FILESYSTEM_SOURCES_DIR}/path_traits.cpp
+
+      ${BOOST_SOURCES_DIR}/libs/locale/src/shared/generator.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/shared/date_time.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/shared/formatting.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/shared/ids.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/shared/localization_backend.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/shared/message.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/shared/mo_lambda.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/util/codecvt_converter.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/util/default_locale.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/util/gregorian.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/util/info.cpp
+      ${BOOST_SOURCES_DIR}/libs/locale/src/util/locale_data.cpp
+      )        
+  endif()
+
   add_definitions(
     # Static build of Boost
     -DBOOST_ALL_NO_LIB 
diff --git a/Orthanc/Resources/CMake/Compiler.cmake b/Orthanc/Resources/CMake/Compiler.cmake
index a82efc7..52f4fab 100644
--- a/Orthanc/Resources/CMake/Compiler.cmake
+++ b/Orthanc/Resources/CMake/Compiler.cmake
@@ -10,7 +10,7 @@ if (CMAKE_COMPILER_IS_GNUCXX)
   set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wno-long-long -Wno-implicit-function-declaration")  
   # --std=c99 makes libcurl not to compile
   # -pedantic gives a lot of warnings on OpenSSL 
-  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -pedantic -Wno-long-long -Wno-variadic-macros")
+  set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wno-long-long -Wno-variadic-macros")
 
   if (CMAKE_CROSSCOMPILING)
     # http://stackoverflow.com/a/3543845/881731
@@ -50,7 +50,12 @@ if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
     ${CMAKE_SYSTEM_NAME} STREQUAL "kFreeBSD" OR
     ${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
   set(CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--no-undefined")
-  set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined -Wl,--version-script=${ORTHANC_ROOT}/Plugins/Samples/Common/VersionScript.map")
+  set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--no-undefined")
+
+  if (NOT DEFINED ENABLE_PLUGINS_VERSION_SCRIPT OR 
+      ENABLE_PLUGINS_VERSION_SCRIPT)
+    set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--version-script=${ORTHANC_ROOT}/Plugins/Samples/Common/VersionScript.map")
+  endif()
 
   # Remove the "-rdynamic" option
   # http://www.mail-archive.com/cmake@cmake.org/msg08837.html
@@ -68,6 +73,11 @@ if (${CMAKE_SYSTEM_NAME} STREQUAL "Linux" OR
     link_libraries(dl)
   endif()
 
+  CHECK_INCLUDE_FILES(uuid/uuid.h HAVE_UUID_H)
+  if (NOT HAVE_UUID_H)
+    message(FATAL_ERROR "Please install the uuid-dev package")
+  endif()
+
 elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
   if (MSVC)
     message("MSVC compiler version = " ${MSVC_VERSION} "\n")
@@ -92,6 +102,11 @@ elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
   link_libraries(rpcrt4 ws2_32)
 
   if (CMAKE_COMPILER_IS_GNUCXX)
+    # Some additional C/C++ compiler flags for MinGW
+    SET(MINGW_NO_WARNINGS "-Wno-unused-function -Wno-unused-variable")
+    SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${MINGW_NO_WARNINGS} -Wno-pointer-to-int-cast -Wno-int-to-pointer-cast")
+    SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${MINGW_NO_WARNINGS}")
+
     # This is a patch for MinGW64
     SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--allow-multiple-definition -static-libgcc -static-libstdc++")
     SET(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--allow-multiple-definition -static-libgcc -static-libstdc++")
@@ -115,6 +130,11 @@ elseif (${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
     )
   link_libraries(iconv)
 
+  CHECK_INCLUDE_FILES(uuid/uuid.h HAVE_UUID_H)
+  if (NOT HAVE_UUID_H)
+    message(FATAL_ERROR "Please install the uuid-dev package")
+  endif()
+
 endif()
 
 
@@ -134,17 +154,6 @@ if (${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
 endif()
 
 
-if (${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
-  CHECK_INCLUDE_FILES(rpc.h HAVE_UUID_H)
-else()
-  CHECK_INCLUDE_FILES(uuid/uuid.h HAVE_UUID_H)
-endif()
-
-if (NOT HAVE_UUID_H)
-  message(FATAL_ERROR "Please install the uuid-dev package")
-endif()
-
-
 if (STATIC_BUILD)
   add_definitions(-DORTHANC_STATIC=1)
 else()
diff --git a/Orthanc/Resources/CMake/DownloadPackage.cmake b/Orthanc/Resources/CMake/DownloadPackage.cmake
index 00647be..492a352 100644
--- a/Orthanc/Resources/CMake/DownloadPackage.cmake
+++ b/Orthanc/Resources/CMake/DownloadPackage.cmake
@@ -83,7 +83,9 @@ macro(DownloadPackage MD5 Url TargetDirectory)
       # How to silently extract files using 7-zip
       # http://superuser.com/questions/331148/7zip-command-line-extract-silently-quietly
 
-      if (("${TMP_EXTENSION}" STREQUAL "gz") OR ("${TMP_EXTENSION}" STREQUAL "tgz"))
+      if (("${TMP_EXTENSION}" STREQUAL "gz") OR 
+          ("${TMP_EXTENSION}" STREQUAL "tgz") OR
+          ("${TMP_EXTENSION}" STREQUAL "xz"))
         execute_process(
           COMMAND ${ZIP_EXECUTABLE} e -y ${TMP_PATH}
           WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
@@ -97,8 +99,10 @@ macro(DownloadPackage MD5 Url TargetDirectory)
 
         if ("${TMP_EXTENSION}" STREQUAL "tgz")
           string(REGEX REPLACE ".tgz$" ".tar" TMP_FILENAME2 "${TMP_FILENAME}")
-        else()
+        elseif ("${TMP_EXTENSION}" STREQUAL "gz")
           string(REGEX REPLACE ".gz$" "" TMP_FILENAME2 "${TMP_FILENAME}")
+        elseif ("${TMP_EXTENSION}" STREQUAL "xz")
+          string(REGEX REPLACE ".xz" "" TMP_FILENAME2 "${TMP_FILENAME}")
         endif()
 
         execute_process(
@@ -138,6 +142,12 @@ macro(DownloadPackage MD5 Url TargetDirectory)
           WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
           RESULT_VARIABLE Failure
           )
+      elseif ("${TMP_EXTENSION}" STREQUAL "xz")
+        execute_process(
+          COMMAND sh -c "${TAR_EXECUTABLE} xf ${TMP_PATH}"
+          WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+          RESULT_VARIABLE Failure
+          )
       else()
         message(FATAL_ERROR "Unknown package format.")
       endif()
diff --git a/Orthanc/Resources/CMake/JsonCppConfiguration.cmake b/Orthanc/Resources/CMake/JsonCppConfiguration.cmake
index 7c6d8a1..bad61b8 100644
--- a/Orthanc/Resources/CMake/JsonCppConfiguration.cmake
+++ b/Orthanc/Resources/CMake/JsonCppConfiguration.cmake
@@ -32,4 +32,29 @@ else()
     message(FATAL_ERROR "Please install the libjsoncpp-dev package")
   endif()
 
+  # Switch to the C++11 standard if the version of JsonCpp is 1.y.z
+  if (EXISTS ${JSONCPP_INCLUDE_DIR}/json/version.h)
+    file(STRINGS
+      "${JSONCPP_INCLUDE_DIR}/json/version.h" 
+      JSONCPP_VERSION_MAJOR1 REGEX
+      ".*define JSONCPP_VERSION_MAJOR.*")
+
+    if (NOT JSONCPP_VERSION_MAJOR1)
+      message(FATAL_ERROR "Unable to extract the major version of JsonCpp")
+    endif()
+    
+    string(REGEX REPLACE
+      ".*JSONCPP_VERSION_MAJOR.*([0-9]+)$" "\\1" 
+      JSONCPP_VERSION_MAJOR ${JSONCPP_VERSION_MAJOR1})
+    message("JsonCpp major version: ${JSONCPP_VERSION_MAJOR}")
+
+    if (CMAKE_COMPILER_IS_GNUCXX AND 
+        JSONCPP_VERSION_MAJOR GREATER 0)
+      message("Switching to C++11 standard, as version of JsonCpp is >= 1.0.0")
+      set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wno-deprecated-declarations")
+    endif()
+  else()
+    message("Unable to detect the major version of JsonCpp, assuming < 1.0.0")
+  endif()
+
 endif()
diff --git a/Orthanc/Resources/WindowsResources.py b/Orthanc/Resources/WindowsResources.py
index 649a453..c56733b 100755
--- a/Orthanc/Resources/WindowsResources.py
+++ b/Orthanc/Resources/WindowsResources.py
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# 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
diff --git a/Orthanc/Sdk-0.9.5/orthanc/OrthancCPlugin.h b/Orthanc/Sdk-1.1.0/orthanc/OrthancCPlugin.h
similarity index 85%
rename from Orthanc/Sdk-0.9.5/orthanc/OrthancCPlugin.h
rename to Orthanc/Sdk-1.1.0/orthanc/OrthancCPlugin.h
index fe40348..cb653d9 100644
--- a/Orthanc/Sdk-0.9.5/orthanc/OrthancCPlugin.h
+++ b/Orthanc/Sdk-1.1.0/orthanc/OrthancCPlugin.h
@@ -18,8 +18,11 @@
  *    - Possibly register its callback for changes to the DICOM store using ::OrthancPluginRegisterOnChangeCallback().
  *    - Possibly register a custom storage area using ::OrthancPluginRegisterStorageArea().
  *    - Possibly register a custom database back-end area using OrthancPluginRegisterDatabaseBackendV2().
+ *    - Possibly register a handler for C-Find SCP using OrthancPluginRegisterFindCallback().
  *    - Possibly register a handler for C-Find SCP against DICOM worklists using OrthancPluginRegisterWorklistCallback().
+ *    - Possibly register a handler for C-Move SCP using OrthancPluginRegisterMoveCallback().
  *    - Possibly register a custom decoder for DICOM images using OrthancPluginRegisterDecodeImageCallback().
+ *    - Possibly register a callback to filter incoming HTTP requests using OrthancPluginRegisterIncomingHttpRequestFilter().
  * -# <tt>void OrthancPluginFinalize()</tt>:
  *    This function is invoked by Orthanc during its shutdown. The plugin
  *    must free all its memory.
@@ -51,8 +54,8 @@
  * @defgroup Callbacks Callbacks
  * @brief Functions to register and manage callbacks by the plugins.
  *
- * @defgroup Worklists Worklists
- * @brief Functions to register and manage worklists.
+ * @defgroup DicomCallbaks DicomCallbaks
+ * @brief Functions to register and manage DICOM callbacks (worklists, C-Find, C-MOVE).
  *
  * @defgroup Orthanc Orthanc
  * @brief Functions to access the content of the Orthanc server.
@@ -69,7 +72,7 @@
 
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -112,9 +115,9 @@
 #define ORTHANC_PLUGINS_API
 #endif
 
-#define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     0
-#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     9
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  5
+#define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
+#define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     1
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  0
 
 
 
@@ -238,8 +241,8 @@ extern "C"
     OrthancPluginErrorCode_DirectoryOverFile = 2000    /*!< The directory to be created is already occupied by a regular file */,
     OrthancPluginErrorCode_FileStorageCannotWrite = 2001    /*!< Unable to create a subdirectory or a file in the file storage */,
     OrthancPluginErrorCode_DirectoryExpected = 2002    /*!< The specified path does not point to a directory */,
-    OrthancPluginErrorCode_HttpPortInUse = 2003    /*!< The TCP port of the HTTP server is already in use */,
-    OrthancPluginErrorCode_DicomPortInUse = 2004    /*!< The TCP port of the DICOM server is already in use */,
+    OrthancPluginErrorCode_HttpPortInUse = 2003    /*!< The TCP port of the HTTP server is privileged or already in use */,
+    OrthancPluginErrorCode_DicomPortInUse = 2004    /*!< The TCP port of the DICOM server is privileged or already in use */,
     OrthancPluginErrorCode_BadHttpStatusInRest = 2005    /*!< This HTTP status is not allowed in a REST API */,
     OrthancPluginErrorCode_RegularFileExpected = 2006    /*!< The specified path does not point to a regular file */,
     OrthancPluginErrorCode_PathToExecutable = 2007    /*!< Unable to get the path to the executable */,
@@ -277,6 +280,7 @@ extern "C"
     OrthancPluginErrorCode_SslDisabled = 2039    /*!< Orthanc has been built without SSL support */,
     OrthancPluginErrorCode_CannotOrderSlices = 2040    /*!< Unable to order the slices of the series */,
     OrthancPluginErrorCode_NoWorklistHandler = 2041    /*!< No request handler factory for DICOM C-Find Modality SCP */,
+    OrthancPluginErrorCode_AlreadyExistingTag = 2042    /*!< Cannot override the value of a tag that already exists */,
 
     _OrthancPluginErrorCode_INTERNAL = 0x7fffffff
   } OrthancPluginErrorCode;
@@ -401,6 +405,8 @@ extern "C"
     _OrthancPluginService_ComputeMd5 = 24,
     _OrthancPluginService_ComputeSha1 = 25,
     _OrthancPluginService_LookupDictionary = 26,
+    _OrthancPluginService_CallHttpClient2 = 27,
+    _OrthancPluginService_GenerateUuid = 28,
 
     /* Registration of callbacks */
     _OrthancPluginService_RegisterRestCallback = 1000,
@@ -410,6 +416,9 @@ extern "C"
     _OrthancPluginService_RegisterRestCallbackNoLock = 1004,
     _OrthancPluginService_RegisterWorklistCallback = 1005,
     _OrthancPluginService_RegisterDecodeImageCallback = 1006,
+    _OrthancPluginService_RegisterIncomingHttpRequestFilter = 1007,
+    _OrthancPluginService_RegisterFindCallback = 1008,
+    _OrthancPluginService_RegisterMoveCallback = 1009,
 
     /* Sending answers to REST calls */
     _OrthancPluginService_AnswerBuffer = 2000,
@@ -424,6 +433,7 @@ extern "C"
     _OrthancPluginService_SendMultipartItem = 2009,
     _OrthancPluginService_SendHttpStatus = 2010,
     _OrthancPluginService_CompressAndAnswerImage = 2011,
+    _OrthancPluginService_SendMultipartItem2 = 2012,
 
     /* Access to the Orthanc database and API */
     _OrthancPluginService_GetDicomForInstance = 3000,
@@ -478,11 +488,17 @@ extern "C"
     _OrthancPluginService_CreateImageAccessor = 6013,
     _OrthancPluginService_DecodeDicomImage = 6014,
 
-    /* Primitives for handling worklists */
+    /* Primitives for handling C-Find, C-Move and worklists */
     _OrthancPluginService_WorklistAddAnswer = 7000,
     _OrthancPluginService_WorklistMarkIncomplete = 7001,
     _OrthancPluginService_WorklistIsMatch = 7002,
     _OrthancPluginService_WorklistGetDicomQuery = 7003,
+    _OrthancPluginService_FindAddAnswer = 7004,
+    _OrthancPluginService_FindMarkIncomplete = 7005,
+    _OrthancPluginService_GetFindQuerySize = 7006,
+    _OrthancPluginService_GetFindQueryTag = 7007,
+    _OrthancPluginService_GetFindQueryTagName = 7008,
+    _OrthancPluginService_GetFindQueryValue = 7009,
 
     _OrthancPluginService_INTERNAL = 0x7fffffff
   } _OrthancPluginService;
@@ -806,22 +822,38 @@ extern "C"
 
 
   /**
-   * @brief Opaque structure to an object that represents a C-Find query.
-   * @ingroup Worklists
+   * @brief Opaque structure to an object that represents a C-Find query for worklists.
+   * @ingroup DicomCallbacks
    **/
   typedef struct _OrthancPluginWorklistQuery_t OrthancPluginWorklistQuery;
 
 
 
   /**
-   * @brief Opaque structure to an object that represents the answers to a C-Find query.
-   * @ingroup Worklists
+   * @brief Opaque structure to an object that represents the answers to a C-Find query for worklists.
+   * @ingroup DicomCallbacks
    **/
   typedef struct _OrthancPluginWorklistAnswers_t OrthancPluginWorklistAnswers;
 
 
 
   /**
+   * @brief Opaque structure to an object that represents a C-Find query.
+   * @ingroup DicomCallbacks
+   **/
+  typedef struct _OrthancPluginFindQuery_t OrthancPluginFindQuery;
+
+
+
+  /**
+   * @brief Opaque structure to an object that represents the answers to a C-Find query for worklists.
+   * @ingroup DicomCallbacks
+   **/
+  typedef struct _OrthancPluginFindAnswers_t OrthancPluginFindAnswers;
+
+
+
+  /**
    * @brief Signature of a callback function that answers to a REST request.
    * @ingroup Callbacks
    **/
@@ -929,27 +961,165 @@ extern "C"
 
 
   /**
-   * @brief Callback to handle the C-Find SCP requests received by Orthanc.
+   * @brief Callback to handle the C-Find SCP requests for worklists.
    *
    * Signature of a callback function that is triggered when Orthanc
    * receives a C-Find SCP request against modality worklists.
    *
    * @param answers The target structure where answers must be stored.
    * @param query The worklist query.
-   * @param remoteAet The Application Entity Title (AET) of the modality from which the request originates.
+   * @param issuerAet The Application Entity Title (AET) of the modality from which the request originates.
    * @param calledAet The Application Entity Title (AET) of the modality that is called by the request.
    * @return 0 if success, other value if error.
-   * @ingroup Worklists
+   * @ingroup DicomCallbacks
    **/
   typedef OrthancPluginErrorCode (*OrthancPluginWorklistCallback) (
     OrthancPluginWorklistAnswers*     answers,
     const OrthancPluginWorklistQuery* query,
-    const char*                       remoteAet,
+    const char*                       issuerAet,
     const char*                       calledAet);
 
 
 
   /**
+   * @brief Callback to filter incoming HTTP requests received by Orthanc.
+   *
+   * Signature of a callback function that is triggered whenever
+   * Orthanc receives an HTTP/REST request, and that answers whether
+   * this request should be allowed. If the callback returns "0"
+   * ("false"), the server answers with HTTP status code 403
+   * (Forbidden).
+   *
+   * @param method The HTTP method used by the request.
+   * @param uri The URI of interest.
+   * @param ip The IP address of the HTTP client.
+   * @param headersCount The number of HTTP headers.
+   * @param headersKeys The keys of the HTTP headers (always converted to low-case).
+   * @param headersValues The values of the HTTP headers.
+   * @return 0 if forbidden access, 1 if allowed access, -1 if error.
+   * @ingroup Callback
+   **/
+  typedef int32_t (*OrthancPluginIncomingHttpRequestFilter) (
+    OrthancPluginHttpMethod  method,
+    const char*              uri,
+    const char*              ip,
+    uint32_t                 headersCount,
+    const char* const*       headersKeys,
+    const char* const*       headersValues);
+
+
+
+  /**
+   * @brief Callback to handle incoming C-Find SCP requests.
+   *
+   * Signature of a callback function that is triggered whenever
+   * Orthanc receives a C-Find SCP request not concerning modality
+   * worklists.
+   *
+   * @param answers The target structure where answers must be stored.
+   * @param query The worklist query.
+   * @param issuerAet The Application Entity Title (AET) of the modality from which the request originates.
+   * @param calledAet The Application Entity Title (AET) of the modality that is called by the request.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginFindCallback) (
+    OrthancPluginFindAnswers*     answers,
+    const OrthancPluginFindQuery* query,
+    const char*                   issuerAet,
+    const char*                   calledAet);
+
+
+
+  /**
+   * @brief Callback to handle incoming C-Move SCP requests.
+   *
+   * Signature of a callback function that is triggered whenever
+   * Orthanc receives a C-Move SCP request. The callback receives the
+   * type of the resource of interest (study, series, instance...)
+   * together with the DICOM tags containing its identifiers. In turn,
+   * the plugin must create a driver object that will be responsible
+   * for driving the successive move suboperations.
+   *
+   * @param resourceType The type of the resource of interest. Note
+   * that this might be set to ResourceType_None if the
+   * QueryRetrieveLevel (0008,0052) tag was not provided by the
+   * issuer.
+   * @param patientId Content of the PatientID (0x0010, 0x0020) tag of the resource of interest. Might be NULL.
+   * @param accessionNumber Content of the AccessionNumber (0x0008, 0x0050) tag. Might be NULL.
+   * @param studyInstanceUid Content of the StudyInstanceUID (0x0020, 0x000d) tag. Might be NULL.
+   * @param seriesInstanceUid Content of the SeriesInstanceUID (0x0020, 0x000e) tag. Might be NULL.
+   * @param sopInstanceUid Content of the SOPInstanceUID (0x0008, 0x0018) tag. Might be NULL.
+   * @param issuerAet The Application Entity Title (AET) of the
+   * modality from which the request originates.
+   * @param sourceAet The Application Entity Title (AET) of the
+   * modality that should send its DICOM files to another modality.
+   * @param targetAet The Application Entity Title (AET) of the
+   * modality that should receive the DICOM files.
+   *
+   * @return The NULL value if the plugin cannot deal with this query,
+   * or a pointer to the driver object that is responsible for
+   * handling the successive move suboperations.
+   * 
+   * @note If targetAet equals sourceAet, this is actually a query/retrieve operation.
+   * @ingroup DicomCallbacks
+   **/
+  typedef void* (*OrthancPluginMoveCallback) (
+    OrthancPluginResourceType  resourceType,
+    const char*                patientId,
+    const char*                accessionNumber,
+    const char*                studyInstanceUid,
+    const char*                seriesInstanceUid,
+    const char*                sopInstanceUid,
+    const char*                issuerAet,
+    const char*                sourceAet,
+    const char*                targetAet,
+    uint16_t                   moveOriginatorId);
+    
+
+  /**
+   * @brief Callback to read the size of a C-Move driver.
+   * 
+   * Signature of a callback function that returns the number of
+   * C-Move suboperations that are to be achieved by the given C-Move
+   * driver. This driver is the return value of a previous call to the
+   * OrthancPluginMoveCallback() callback.
+   *
+   * @param moveDriver The C-Move driver of interest.
+   * @return The number of suboperations. 
+   **/
+  typedef uint32_t (*OrthancPluginGetMoveSize) (void* moveDriver);
+
+
+  /**
+   * @brief Callback to apply one C-Move suboperation.
+   * 
+   * Signature of a callback function that applies the next C-Move
+   * suboperation that os to be achieved by the given C-Move
+   * driver. This driver is the return value of a previous call to the
+   * OrthancPluginMoveCallback() callback.
+   *
+   * @param moveDriver The C-Move driver of interest.
+   * @return 0 if success, or the error code if failure.
+   **/
+  typedef OrthancPluginErrorCode (*OrthancPluginApplyMove) (void* moveDriver);
+
+
+  /**
+   * @brief Callback to free one C-Move driver.
+   * 
+   * Signature of a callback function that releases the resources
+   * allocated by the given C-Move driver. This driver is the return
+   * value of a previous call to the OrthancPluginMoveCallback()
+   * callback.
+   *
+   * @param moveDriver The C-Move driver of interest.
+   **/
+  typedef void (*OrthancPluginFreeMove) (void* moveDriver);
+
+
+
+  /**
    * @brief Data structure that contains information about the Orthanc core.
    **/
   typedef struct _OrthancPluginContext_t
@@ -1399,6 +1569,7 @@ extern "C"
    * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
    * @param uri The URI in the built-in Orthanc API.
    * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
    * @see OrthancPluginRestApiGetAfterPlugins
    * @ingroup Orthanc
    **/
@@ -1428,6 +1599,7 @@ extern "C"
    * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
    * @param uri The URI in the built-in Orthanc API.
    * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
    * @see OrthancPluginRestApiGet
    * @ingroup Orthanc
    **/
@@ -1464,6 +1636,7 @@ extern "C"
    * @param body The body of the POST request.
    * @param bodySize The size of the body.
    * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
    * @see OrthancPluginRestApiPostAfterPlugins
    * @ingroup Orthanc
    **/
@@ -1498,6 +1671,7 @@ extern "C"
    * @param body The body of the POST request.
    * @param bodySize The size of the body.
    * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
    * @see OrthancPluginRestApiPost
    * @ingroup Orthanc
    **/
@@ -1526,6 +1700,7 @@ extern "C"
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param uri The URI to delete in the built-in Orthanc API.
    * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
    * @see OrthancPluginRestApiDeleteAfterPlugins
    * @ingroup Orthanc
    **/
@@ -1548,6 +1723,7 @@ extern "C"
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param uri The URI to delete in the built-in Orthanc API.
    * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
    * @see OrthancPluginRestApiDelete
    * @ingroup Orthanc
    **/
@@ -1572,6 +1748,7 @@ extern "C"
    * @param body The body of the PUT request.
    * @param bodySize The size of the body.
    * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
    * @see OrthancPluginRestApiPutAfterPlugins
    * @ingroup Orthanc
    **/
@@ -1607,6 +1784,7 @@ extern "C"
    * @param body The body of the PUT request.
    * @param bodySize The size of the body.
    * @return 0 if success, or the error code if failure.
+   * @note If the resource is not existing (error 404), the error code will be OrthancPluginErrorCode_UnknownResource.
    * @see OrthancPluginRestApiPut
    * @ingroup Orthanc
    **/
@@ -2396,7 +2574,7 @@ extern "C"
    * Orthanc, you should make these calls in a separate thread (with
    * the events passing through a message queue). Otherwise, this
    * could result in deadlocks in the presence of other plugins or Lua
-   * script.
+   * scripts.
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param callback The callback function.
@@ -2719,7 +2897,7 @@ extern "C"
    * @param subType The sub-type of the multipart answer ("mixed" or "related").
    * @param contentType The MIME type of the items in the multipart answer.
    * @return 0 if success, or the error code if failure.
-   * @see OrthancPluginSendMultipartItem()
+   * @see OrthancPluginSendMultipartItem(), OrthancPluginSendMultipartItem2()
    * @ingroup REST
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginStartMultipartAnswer(
@@ -2748,6 +2926,7 @@ extern "C"
    * @param answerSize Number of bytes of the item.
    * @return 0 if success, or the error code if failure (this notably happens
    * if the connection is closed by the client).
+   * @see OrthancPluginSendMultipartItem2()
    * @ingroup REST
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSendMultipartItem(
@@ -3989,7 +4168,7 @@ extern "C"
   {
     char**                          result;
     const char*                     instanceId;
-    const char*                     buffer;
+    const void*                     buffer;
     uint32_t                        size;
     OrthancPluginDicomToJsonFormat  format;
     OrthancPluginDicomToJsonFlags   flags;
@@ -4018,7 +4197,7 @@ extern "C"
    **/
   ORTHANC_PLUGIN_INLINE char* OrthancPluginDicomBufferToJson(
     OrthancPluginContext*           context,
-    const char*                     buffer,
+    const void*                     buffer,
     uint32_t                        size,
     OrthancPluginDicomToJsonFormat  format,
     OrthancPluginDicomToJsonFlags   flags, 
@@ -4115,8 +4294,8 @@ extern "C"
    * @param target The target memory buffer. It must be freed with OrthancPluginFreeMemoryBuffer().
    * @param uri The URI in the built-in Orthanc API.
    * @param headersCount The number of HTTP headers.
-   * @param headersKeys Array containing the keys of the HTTP headers.
-   * @param headersValues Array containing the values of the 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 afterPlugins If 0, the built-in API of Orthanc is used.
    * If 1, the API is tainted by the plugins.
    * @return 0 if success, or the error code if failure.
@@ -4159,7 +4338,7 @@ extern "C"
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param callback The callback.
    * @return 0 if success, other value if error.
-   * @ingroup Worklists
+   * @ingroup DicomCallbacks
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterWorklistCallback(
     OrthancPluginContext*          context,
@@ -4194,7 +4373,8 @@ extern "C"
    * @param dicom The worklist to answer, encoded as a DICOM file.
    * @param size The size of the DICOM file.
    * @return 0 if success, other value if error.
-   * @ingroup Worklists
+   * @ingroup DicomCallbacks
+   * @see OrthancPluginCreateDicom()
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginWorklistAddAnswer(
     OrthancPluginContext*             context,
@@ -4224,7 +4404,7 @@ extern "C"
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
    * @param answers The set of answers.
    * @return 0 if success, other value if error.
-   * @ingroup Worklists
+   * @ingroup DicomCallbacks
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginWorklistMarkIncomplete(
     OrthancPluginContext*          context,
@@ -4262,7 +4442,7 @@ extern "C"
    * @param dicom The worklist to answer, encoded as a DICOM file.
    * @param size The size of the DICOM file.
    * @return 1 if the worklist matches the query, 0 otherwise.
-   * @ingroup Worklists
+   * @ingroup DicomCallbacks
    **/
   ORTHANC_PLUGIN_INLINE int32_t  OrthancPluginWorklistIsMatch(
     OrthancPluginContext*              context,
@@ -4301,7 +4481,7 @@ extern "C"
    * @param target Memory buffer where to store the DICOM file. It must be freed with OrthancPluginFreeMemoryBuffer().
    * @param query The worklist query, as received by the callback.
    * @return 0 if success, other value if error.
-   * @ingroup Worklists
+   * @ingroup DicomCallbacks
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginWorklistGetDicomQuery(
     OrthancPluginContext*              context,
@@ -4677,6 +4857,505 @@ extern "C"
   }
 
 
+
+  typedef struct
+  {
+    OrthancPluginRestOutput* output;
+    const char*              answer;
+    uint32_t                 answerSize;
+    uint32_t                 headersCount;
+    const char* const*       headersKeys;
+    const char* const*       headersValues;
+  } _OrthancPluginSendMultipartItem2;
+
+  /**
+   * @brief Send an item as a part of some HTTP multipart answer, with custom headers.
+   *
+   * This function sends an item as a part of some HTTP multipart
+   * answer that was initiated by OrthancPluginStartMultipartAnswer(). In addition to
+   * OrthancPluginSendMultipartItem(), this function will set HTTP header associated
+   * with the item.
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param output The HTTP connection to the client application.
+   * @param answer Pointer to the memory buffer containing the item.
+   * @param answerSize Number of bytes of the item.
+   * @param headersCount The number of HTTP headers.
+   * @param headersKeys Array containing the keys of the HTTP headers.
+   * @param headersValues Array containing the values of the HTTP headers.
+   * @return 0 if success, or the error code if failure (this notably happens
+   * if the connection is closed by the client).
+   * @see OrthancPluginSendMultipartItem()
+   * @ingroup REST
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginSendMultipartItem2(
+    OrthancPluginContext*    context,
+    OrthancPluginRestOutput* output,
+    const char*              answer,
+    uint32_t                 answerSize,
+    uint32_t                 headersCount,
+    const char* const*       headersKeys,
+    const char* const*       headersValues)
+  {
+    _OrthancPluginSendMultipartItem2 params;
+    params.output = output;
+    params.answer = answer;
+    params.answerSize = answerSize;
+    params.headersCount = headersCount;
+    params.headersKeys = headersKeys;
+    params.headersValues = headersValues;    
+
+    return context->InvokeService(context, _OrthancPluginService_SendMultipartItem2, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginIncomingHttpRequestFilter callback;
+  } _OrthancPluginIncomingHttpRequestFilter;
+
+  /**
+   * @brief Register a callback to filter incoming HTTP requests.
+   *
+   * This function registers a custom callback to filter incoming HTTP/REST
+   * requests received by the HTTP server of Orthanc.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @return 0 if success, other value if error.
+   * @ingroup Callbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterIncomingHttpRequestFilter(
+    OrthancPluginContext*                   context,
+    OrthancPluginIncomingHttpRequestFilter  callback)
+  {
+    _OrthancPluginIncomingHttpRequestFilter params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterIncomingHttpRequestFilter, &params);
+  }
+  
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  answerBody;
+    OrthancPluginMemoryBuffer*  answerHeaders;
+    uint16_t*                   httpStatus;
+    OrthancPluginHttpMethod     method;
+    const char*                 url;
+    uint32_t                    headersCount;
+    const char* const*          headersKeys;
+    const char* const*          headersValues;
+    const char*                 body;
+    uint32_t                    bodySize;
+    const char*                 username;
+    const char*                 password;
+    uint32_t                    timeout;
+    const char*                 certificateFile;
+    const char*                 certificateKeyFile;
+    const char*                 certificateKeyPassword;
+    uint8_t                     pkcs11;
+  } _OrthancPluginCallHttpClient2;
+
+
+
+  /**
+   * @brief Issue a HTTP call with full flexibility.
+   * 
+   * Make a HTTP call to the given URL. The result to the query is
+   * stored into a newly allocated memory buffer. 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 answerBody The target memory buffer (out argument).
+   *        It must be freed with OrthancPluginFreeMemoryBuffer().
+   * @param answerHeaders The target memory buffer for the HTTP headers in the answers (out argument). 
+   *        The answer headers are formatted as a JSON object (associative array).
+   *        The buffer must be freed with OrthancPluginFreeMemoryBuffer().
+   *        This argument can be set to NULL if the plugin has no interest in the HTTP headers.
+   * @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 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 body The body of the POST request.
+   * @param bodySize The size of the body.
+   * @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.
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpClient(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  answerBody,
+    OrthancPluginMemoryBuffer*  answerHeaders,
+    uint16_t*                   httpStatus,
+    OrthancPluginHttpMethod     method,
+    const char*                 url,
+    uint32_t                    headersCount,
+    const char* const*          headersKeys,
+    const char* const*          headersValues,
+    const char*                 body,
+    uint32_t                    bodySize,
+    const char*                 username,
+    const char*                 password,
+    uint32_t                    timeout,
+    const char*                 certificateFile,
+    const char*                 certificateKeyFile,
+    const char*                 certificateKeyPassword,
+    uint8_t                     pkcs11)
+  {
+    _OrthancPluginCallHttpClient2 params;
+    memset(&params, 0, sizeof(params));
+
+    params.answerBody = answerBody;
+    params.answerHeaders = answerHeaders;
+    params.httpStatus = httpStatus;
+    params.method = method;
+    params.url = url;
+    params.headersCount = headersCount;
+    params.headersKeys = headersKeys;
+    params.headersValues = headersValues;
+    params.body = body;
+    params.bodySize = bodySize;
+    params.username = username;
+    params.password = password;
+    params.timeout = timeout;
+    params.certificateFile = certificateFile;
+    params.certificateKeyFile = certificateKeyFile;
+    params.certificateKeyPassword = certificateKeyPassword;
+    params.pkcs11 = pkcs11;
+
+    return context->InvokeService(context, _OrthancPluginService_CallHttpClient2, &params);
+  }
+
+
+  /**
+   * @brief Generate an UUID.
+   *
+   * Generate a random GUID/UUID (globally unique identifier).
+   * 
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return NULL in the case of an error, or a newly allocated string
+   * containing the UUID. This string must be freed by OrthancPluginFreeString().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE char* OrthancPluginGenerateUuid(
+    OrthancPluginContext*  context)
+  {
+    char* result;
+
+    _OrthancPluginRetrieveDynamicString params;
+    params.result = &result;
+    params.argument = NULL;
+
+    if (context->InvokeService(context, _OrthancPluginService_GenerateUuid, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+
+
+  typedef struct
+  {
+    OrthancPluginFindCallback callback;
+  } _OrthancPluginFindCallback;
+
+  /**
+   * @brief Register a callback to handle C-Find requests.
+   *
+   * This function registers a callback to handle C-Find SCP requests
+   * that are not related to modality worklists.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The callback.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterFindCallback(
+    OrthancPluginContext*      context,
+    OrthancPluginFindCallback  callback)
+  {
+    _OrthancPluginFindCallback params;
+    params.callback = callback;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterFindCallback, &params);
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginFindAnswers      *answers;
+    const OrthancPluginFindQuery  *query;
+    const void                    *dicom;
+    uint32_t                       size;
+    uint32_t                       index;
+    uint32_t                      *resultUint32;
+    uint16_t                      *resultGroup;
+    uint16_t                      *resultElement;
+    char                         **resultString;
+  } _OrthancPluginFindOperation;
+
+  /**
+   * @brief Add one answer to some C-Find request.
+   *
+   * This function adds one answer (encoded as a DICOM file) to the
+   * set of answers corresponding to some C-Find SCP request that is
+   * not related to modality worklists.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param answers The set of answers.
+   * @param dicom The answer to be added, encoded as a DICOM file.
+   * @param size The size of the DICOM file.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   * @see OrthancPluginCreateDicom()
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginFindAddAnswer(
+    OrthancPluginContext*      context,
+    OrthancPluginFindAnswers*  answers,
+    const void*                dicom,
+    uint32_t                   size)
+  {
+    _OrthancPluginFindOperation params;
+    memset(&params, 0, sizeof(params));
+    params.answers = answers;
+    params.dicom = dicom;
+    params.size = size;
+
+    return context->InvokeService(context, _OrthancPluginService_FindAddAnswer, &params);
+  }
+
+
+  /**
+   * @brief Mark the set of C-Find answers as incomplete.
+   *
+   * This function marks as incomplete the set of answers
+   * corresponding to some C-Find SCP request that is not related to
+   * modality worklists. This must be used if canceling the handling
+   * of a request when too many answers are to be returned.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param answers The set of answers.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginFindMarkIncomplete(
+    OrthancPluginContext*      context,
+    OrthancPluginFindAnswers*  answers)
+  {
+    _OrthancPluginFindOperation params;
+    memset(&params, 0, sizeof(params));
+    params.answers = answers;
+
+    return context->InvokeService(context, _OrthancPluginService_FindMarkIncomplete, &params);
+  }
+
+
+
+  /**
+   * @brief Get the number of tags in a C-Find query.
+   *
+   * This function returns the number of tags that are contained in
+   * the given C-Find query.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param query The C-Find query.
+   * @return The number of tags.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t  OrthancPluginGetFindQuerySize(
+    OrthancPluginContext*          context,
+    const OrthancPluginFindQuery*  query)
+  {
+    uint32_t count = 0;
+
+    _OrthancPluginFindOperation params;
+    memset(&params, 0, sizeof(params));
+    params.query = query;
+    params.resultUint32 = &count;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetFindQuerySize, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return 0;
+    }
+    else
+    {
+      return count;
+    }
+  }
+
+
+  /**
+   * @brief Get one tag in a C-Find query.
+   *
+   * This function returns the group and the element of one DICOM tag
+   * in the given C-Find query.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param group The group of the tag (output).
+   * @param element The element of the tag (output).
+   * @param query The C-Find query.
+   * @param index The index of the tag of interest.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginGetFindQueryTag(
+    OrthancPluginContext*          context,
+    uint16_t*                      group,
+    uint16_t*                      element,
+    const OrthancPluginFindQuery*  query,
+    uint32_t                       index)
+  {
+    _OrthancPluginFindOperation params;
+    memset(&params, 0, sizeof(params));
+    params.query = query;
+    params.index = index;
+    params.resultGroup = group;
+    params.resultElement = element;
+
+    return context->InvokeService(context, _OrthancPluginService_GetFindQueryTag, &params);
+  }
+
+
+  /**
+   * @brief Get the symbolic name of one tag in a C-Find query.
+   *
+   * This function returns the symbolic name of one DICOM tag in the
+   * given C-Find query.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param query The C-Find query.
+   * @param index The index of the tag of interest.
+   * @return The NULL value in case of error, or a string containing the name of the tag.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE char*  OrthancPluginGetFindQueryTagName(
+    OrthancPluginContext*          context,
+    const OrthancPluginFindQuery*  query,
+    uint32_t                       index)
+  {
+    char* result;
+
+    _OrthancPluginFindOperation params;
+    memset(&params, 0, sizeof(params));
+    params.query = query;
+    params.index = index;
+    params.resultString = &result;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetFindQueryTagName, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+
+
+  /**
+   * @brief Get the value associated with one tag in a C-Find query.
+   *
+   * This function returns the value associated with one tag in the
+   * given C-Find query.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param query The C-Find query.
+   * @param index The index of the tag of interest.
+   * @return The NULL value in case of error, or a string containing the value of the tag.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE char*  OrthancPluginGetFindQueryValue(
+    OrthancPluginContext*          context,
+    const OrthancPluginFindQuery*  query,
+    uint32_t                       index)
+  {
+    char* result;
+
+    _OrthancPluginFindOperation params;
+    memset(&params, 0, sizeof(params));
+    params.query = query;
+    params.index = index;
+    params.resultString = &result;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetFindQueryValue, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return result;
+    }
+  }
+ 
+
+
+
+  typedef struct
+  {
+    OrthancPluginMoveCallback   callback;
+    OrthancPluginGetMoveSize    getMoveSize;
+    OrthancPluginApplyMove      applyMove;
+    OrthancPluginFreeMove       freeMove;
+  } _OrthancPluginMoveCallback;
+
+  /**
+   * @brief Register a callback to handle C-Move requests.
+   *
+   * This function registers a callback to handle C-Move SCP requests.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param callback The main callback.
+   * @param getMoveSize Callback to read the number of C-Move suboperations.
+   * @param applyMove Callback to apply one C-Move suboperations.
+   * @param freeMove Callback to free the C-Move driver.
+   * @return 0 if success, other value if error.
+   * @ingroup DicomCallbacks
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode OrthancPluginRegisterMoveCallback(
+    OrthancPluginContext*       context,
+    OrthancPluginMoveCallback   callback,
+    OrthancPluginGetMoveSize    getMoveSize,
+    OrthancPluginApplyMove      applyMove,
+    OrthancPluginFreeMove       freeMove)
+  {
+    _OrthancPluginMoveCallback params;
+    params.callback = callback;
+    params.getMoveSize = getMoveSize;
+    params.applyMove = applyMove;
+    params.freeMove = freeMove;
+
+    return context->InvokeService(context, _OrthancPluginService_RegisterMoveCallback, &params);
+  }
+
+
+
+
 #ifdef  __cplusplus
 }
 #endif
diff --git a/Plugin/ChunkedBuffer.cpp b/Plugin/ChunkedBuffer.cpp
deleted file mode 100644
index 76f604d..0000000
--- a/Plugin/ChunkedBuffer.cpp
+++ /dev/null
@@ -1,86 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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 "ChunkedBuffer.h"
-
-#include <cassert>
-#include <string.h>
-
-
-namespace OrthancPlugins
-{
-  void ChunkedBuffer::Clear()
-  {
-    numBytes_ = 0;
-
-    for (Chunks::iterator it = chunks_.begin(); 
-         it != chunks_.end(); ++it)
-    {
-      delete *it;
-    }
-  }
-
-
-  void ChunkedBuffer::AddChunk(const char* chunkData,
-                               size_t chunkSize)
-  {
-    if (chunkSize == 0)
-    {
-      return;
-    }
-
-    assert(chunkData != NULL);
-    chunks_.push_back(new std::string(chunkData, chunkSize));
-    numBytes_ += chunkSize;
-  }
-
-
-  void ChunkedBuffer::AddChunk(const std::string& chunk)
-  {
-    if (chunk.size() > 0)
-    {
-      AddChunk(&chunk[0], chunk.size());
-    }
-  }
-
-
-  void ChunkedBuffer::Flatten(std::string& result)
-  {
-    result.resize(numBytes_);
-
-    size_t pos = 0;
-    for (Chunks::iterator it = chunks_.begin(); 
-         it != chunks_.end(); ++it)
-    {
-      assert(*it != NULL);
-
-      size_t s = (*it)->size();
-      if (s != 0)
-      {
-        memcpy(&result[pos], (*it)->c_str(), s);
-        pos += s;
-      }
-
-      delete *it;
-    }
-
-    chunks_.clear();
-  }
-}
diff --git a/Plugin/ChunkedBuffer.h b/Plugin/ChunkedBuffer.h
deleted file mode 100644
index fbd6a06..0000000
--- a/Plugin/ChunkedBuffer.h
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 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>
-
-namespace OrthancPlugins
-{
-  class ChunkedBuffer
-  {
-  private:
-    typedef std::list<std::string*>  Chunks;
-    size_t numBytes_;
-    Chunks chunks_;
-  
-    void Clear();
-
-  public:
-    ChunkedBuffer() : numBytes_(0)
-    {
-    }
-
-    ~ChunkedBuffer()
-    {
-      Clear();
-    }
-
-    size_t GetNumBytes() const
-    {
-      return numBytes_;
-    }
-
-    void AddChunk(const char* chunkData,
-                  size_t chunkSize);
-
-    void AddChunk(const std::string& chunk);
-
-    void Flatten(std::string& result);
-  };
-}
diff --git a/Plugin/Configuration.cpp b/Plugin/Configuration.cpp
index f165541..950db7f 100644
--- a/Plugin/Configuration.cpp
+++ b/Plugin/Configuration.cpp
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -23,7 +23,10 @@
 #include <fstream>
 #include <json/reader.h>
 #include <boost/regex.hpp>
+#include <boost/lexical_cast.hpp>
 
+#include "Plugin.h"
+#include "DicomWebServers.h"
 #include "../Orthanc/Core/Toolbox.h"
 
 namespace OrthancPlugins
@@ -81,231 +84,206 @@ namespace OrthancPlugins
 
 
   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;
-
-        result.push_back(item);
+        parts.push_back(std::make_pair(start, what[0].first));
       }
 
-      boost::cmatch contentType;
-      if (boost::regex_match(what[0].first, what[0].second, contentType, pattern))
-      {
-        currentType = contentType[1];
-      }
-      else
+      if (*what[2].first == '-')
       {
-        currentType.clear();
+        // This is the last separator (there is a trailing "--")
+        break;
       }
-    
+
       start = what[0].second;
       flags |= boost::match_prev_avail;
     }
-  }
-
 
-  bool RestApiGetString(std::string& result,
-                        OrthancPluginContext* context,
-                        const std::string& uri,
-                        bool applyPlugins)
-  {
-    OrthancPluginMemoryBuffer buffer;
-    int code;
-
-    if (applyPlugins)
+    for (size_t i = 0; i < parts.size(); i++)
     {
-      code = OrthancPluginRestApiGetAfterPlugins(context, &buffer, uri.c_str());
-    }
-    else
-    {
-      code = OrthancPluginRestApiGet(context, &buffer, uri.c_str());
-    }
+      if (boost::regex_match(parts[i].first, parts[i].second, what, encapsulation, boost::match_perl))
+      {
+        size_t dicomSize = what[2].second - what[2].first;
 
-    if (code)
-    {
-      // Error
-      return false;
-    }
+        std::string contentType = "application/octet-stream";
+        std::vector<std::string> headers;
 
-    bool ok = true;
+        {
+          std::string tmp;
+          tmp.assign(what[1].first, what[1].second);
+          Orthanc::Toolbox::TokenizeString(headers, tmp, '\n');
+        }
 
-    try
-    {
-      if (buffer.size)
-      {
-        result.assign(reinterpret_cast<const char*>(buffer.data), buffer.size);
-      }
-      else
-      {
-        result.clear();
-      }
-    }
-    catch (std::bad_alloc&)
-    {
-      ok = false;
-    }
+        bool valid = true;
 
-    OrthancPluginFreeMemoryBuffer(context, &buffer);
+        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;
+              }
+            }
+          }
+        }
 
-    return ok;
+        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");
+        }
+      }      
+    }
   }
 
 
-  bool RestApiGetJson(Json::Value& result,
-                      OrthancPluginContext* context,
-                      const std::string& uri,
-                      bool applyPlugins)
+  void ParseAssociativeArray(std::map<std::string, std::string>& target,
+                             const Json::Value& value,
+                             const std::string& key)
   {
-    std::string content;
-    RestApiGetString(content, context, uri, applyPlugins);
-    
-    Json::Reader reader;
-    return reader.parse(content, result);
-  }
+    if (value.type() != Json::objectValue)
+    {
+      OrthancPlugins::Configuration::LogError("This is not a JSON object");
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat);
+    }
 
+    if (!value.isMember(key))
+    {
+      return;
+    }
 
-  bool RestApiPostString(std::string& result,
-                         OrthancPluginContext* context,
-                         const std::string& uri,
-                         const std::string& body)
-  {
-    OrthancPluginMemoryBuffer buffer;
-    int code = OrthancPluginRestApiPost(context, &buffer, uri.c_str(), body.c_str(), body.size());
+    const Json::Value& tmp = value[key];
 
-    if (code)
+    if (tmp.type() != Json::objectValue)
     {
-      // Error
-      return false;
+      OrthancPlugins::Configuration::LogError("The field \"" + key + "\" of a JSON object is "
+                                              "not a JSON associative array as expected");
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat);
     }
 
-    bool ok = true;
+    Json::Value::Members names = tmp.getMemberNames();
 
-    try
+    for (size_t i = 0; i < names.size(); i++)
     {
-      if (buffer.size)
+      if (tmp[names[i]].type() != Json::stringValue)
       {
-        result.assign(reinterpret_cast<const char*>(buffer.data), buffer.size);
+        OrthancPlugins::Configuration::LogError("Some value in the associative array \"" + key + 
+                                                "\" is not a string as expected");
+        throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat);
       }
       else
       {
-        result.clear();
+        target[names[i]] = tmp[names[i]].asString();
       }
     }
-    catch (std::bad_alloc&)
-    {
-      ok = false;
-    }
-
-    OrthancPluginFreeMemoryBuffer(context, &buffer);
-
-    return ok;
-  }
-
-
-  bool RestApiPostJson(Json::Value& result,
-                       OrthancPluginContext* context,
-                       const std::string& uri,
-                       const std::string& body)
-  {
-    std::string content;
-    RestApiPostString(content, context, uri, body);
-    
-    Json::Reader reader;
-    return reader.parse(content, result);
   }
 
 
   namespace Configuration
   {
-    bool Read(Json::Value& configuration,
-              OrthancPluginContext* context)
-    {
-      std::string s;
+    // Assume Latin-1 encoding by default (as in the Orthanc core)
+    static Orthanc::Encoding defaultEncoding_ = Orthanc::Encoding_Latin1;
+    static OrthancConfiguration configuration_;
 
-      {
-        char* tmp = OrthancPluginGetConfiguration(context);
-        if (tmp == NULL)
-        {
-          OrthancPluginLogError(context, "Error while retrieving the configuration from Orthanc");
-          return false;
-        }
 
-        s.assign(tmp);
-        OrthancPluginFreeString(context, tmp);      
-      }
+    void Initialize(OrthancPluginContext* context)
+    {      
+      OrthancPlugins::OrthancConfiguration global(context);
+      global.GetSection(configuration_, "DicomWeb");
 
-      Json::Reader reader;
-      if (reader.parse(s, configuration))
-      {
-        return true;
-      }
-      else
+      std::string s;
+      if (global.LookupStringValue(s, "DefaultEncoding"))
       {
-        OrthancPluginLogError(context, "Unable to parse the configuration");
-        return false;
+        defaultEncoding_ = Orthanc::StringToEncoding(s.c_str());
       }
+
+      OrthancPlugins::OrthancConfiguration servers;
+      configuration_.GetSection(servers, "Servers");
+      OrthancPlugins::DicomWebServers::GetInstance().Load(servers.GetJson());
     }
 
 
-    std::string GetStringValue(const Json::Value& configuration,
-                               const std::string& key,
+    OrthancPluginContext* GetContext()
+    {
+      return configuration_.GetContext();
+    }
+
+
+    std::string GetStringValue(const std::string& key,
                                const std::string& defaultValue)
     {
-      if (configuration.type() != Json::objectValue ||
-          !configuration.isMember(key) ||
-          configuration[key].type() != Json::stringValue)
-      {
-        return defaultValue;
-      }
-      else
-      {
-        return configuration[key].asString();
-      }
+      return configuration_.GetStringValue(key, defaultValue);
     }
 
 
-    bool GetBoolValue(const Json::Value& configuration,
-                      const std::string& key,
-                      bool defaultValue)
+    bool GetBooleanValue(const std::string& key,
+                         bool defaultValue)
     {
-      if (configuration.type() != Json::objectValue ||
-          !configuration.isMember(key) ||
-          configuration[key].type() != Json::booleanValue)
-      {
-        return defaultValue;
-      }
-      else
-      {
-        return configuration[key].asBool();
-      }
+      return configuration_.GetBooleanValue(key, defaultValue);
+    }
+
+
+    unsigned int GetUnsignedIntegerValue(const std::string& key,
+                                         unsigned int defaultValue)
+    {
+      return configuration_.GetUnsignedIntegerValue(key, defaultValue);
     }
 
 
-    std::string GetRoot(const Json::Value& configuration)
+    std::string GetRoot()
     {
-      std::string root = GetStringValue(configuration, "Root", "/dicom-web/");
+      std::string root = configuration_.GetStringValue("Root", "/dicom-web/");
 
       // Make sure the root URI starts and ends with a slash
       if (root.size() == 0 ||
@@ -323,9 +301,9 @@ namespace OrthancPlugins
     }
 
 
-    std::string GetWadoRoot(const Json::Value& configuration)
+    std::string GetWadoRoot()
     {
-      std::string root = GetStringValue(configuration, "WadoRoot", "/wado/");
+      std::string root = configuration_.GetStringValue("WadoRoot", "/wado/");
 
       // Make sure the root URI starts with a slash
       if (root.size() == 0 ||
@@ -344,11 +322,10 @@ namespace OrthancPlugins
     }
 
 
-    std::string  GetBaseUrl(const Json::Value& configuration,
-                            const OrthancPluginHttpRequest* request)
+    std::string  GetBaseUrl(const OrthancPluginHttpRequest* request)
     {
-      std::string host = GetStringValue(configuration, "Host", "");
-      bool ssl = GetBoolValue(configuration, "Ssl", false);
+      std::string host = configuration_.GetStringValue("Host", "");
+      bool ssl = configuration_.GetBooleanValue("Ssl", false);
 
       if (host.empty() &&
           !LookupHttpHeader(host, request, "host"))
@@ -358,7 +335,52 @@ namespace OrthancPlugins
         host = "localhost:8042";
       }
 
-      return (ssl ? "https://" : "http://") + host + GetRoot(configuration);
+      return (ssl ? "https://" : "http://") + host + GetRoot();
+    }
+
+
+    std::string GetWadoUrl(const std::string& wadoBase,
+                           const std::string& studyInstanceUid,
+                           const std::string& seriesInstanceUid,
+                           const std::string& sopInstanceUid)
+    {
+      if (studyInstanceUid.empty() ||
+          seriesInstanceUid.empty() ||
+          sopInstanceUid.empty())
+      {
+        return "";
+      }
+      else
+      {
+        return (wadoBase + 
+                "studies/" + studyInstanceUid + 
+                "/series/" + seriesInstanceUid + 
+                "/instances/" + sopInstanceUid + "/");
+      }
+    }
+
+
+    void LogError(const std::string& message)
+    {
+      OrthancPluginLogError(GetContext(), message.c_str());
+    }
+
+
+    void LogWarning(const std::string& message)
+    {
+      OrthancPluginLogWarning(GetContext(), message.c_str());
+    }
+
+
+    void LogInfo(const std::string& message)
+    {
+      OrthancPluginLogInfo(GetContext(), message.c_str());
+    }
+
+
+    Orthanc::Encoding GetDefaultEncoding()
+    {
+      return defaultEncoding_;
     }
   }
 }
diff --git a/Plugin/Configuration.h b/Plugin/Configuration.h
index f4fb403..7d2de3a 100644
--- a/Plugin/Configuration.h
+++ b/Plugin/Configuration.h
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -20,6 +20,8 @@
 
 #pragma once
 
+#include "../Orthanc/Core/Enumerations.h"
+
 #include <orthanc/OrthancCPlugin.h>
 #include <json/value.h>
 
@@ -49,48 +51,47 @@ namespace OrthancPlugins
                         const std::string& header);
 
   void ParseMultipartBody(std::vector<MultipartItem>& result,
+                          OrthancPluginContext* context,
                           const char* body,
                           const uint64_t bodySize,
                           const std::string& boundary);
 
-  bool RestApiGetString(std::string& result,
-                        OrthancPluginContext* context,
-                        const std::string& uri,
-                        bool applyPlugins = false);
-
-  bool RestApiGetJson(Json::Value& result,
-                      OrthancPluginContext* context,
-                      const std::string& uri,
-                      bool applyPlugins = false);
-
-  bool RestApiPostString(std::string& result,
-                         OrthancPluginContext* context,
-                         const std::string& uri,
-                         const std::string& body);
-
-  bool RestApiPostJson(Json::Value& result,
-                       OrthancPluginContext* context,
-                       const std::string& uri,
-                       const std::string& body);
+  void ParseAssociativeArray(std::map<std::string, std::string>& target,
+                             const Json::Value& value,
+                             const std::string& key);
 
   namespace Configuration
   {
-    bool Read(Json::Value& configuration,
-              OrthancPluginContext* context);
+    void Initialize(OrthancPluginContext* context);
 
-    std::string GetStringValue(const Json::Value& configuration,
-                               const std::string& key,
-                               const std::string& defaultValue);
+    OrthancPluginContext* GetContext();
     
-    bool GetBoolValue(const Json::Value& configuration,
-                      const std::string& key,
-                      bool defaultValue);
+    std::string GetStringValue(const std::string& key,
+                               const std::string& defaultValue);
+
+    bool GetBooleanValue(const std::string& key,
+                         bool defaultValue);
 
-    std::string GetRoot(const Json::Value& configuration);
+    unsigned int GetUnsignedIntegerValue(const std::string& key,
+                                         unsigned int defaultValue);
 
-    std::string GetWadoRoot(const Json::Value& configuration);
+    std::string GetRoot();
+
+    std::string GetWadoRoot();
       
-    std::string GetBaseUrl(const Json::Value& configuration,
-                           const OrthancPluginHttpRequest* request);
+    std::string GetBaseUrl(const OrthancPluginHttpRequest* request);
+
+    std::string GetWadoUrl(const std::string& wadoBase,
+                           const std::string& studyInstanceUid,
+                           const std::string& seriesInstanceUid,
+                           const std::string& sopInstanceUid);
+
+    void LogError(const std::string& message);
+
+    void LogWarning(const std::string& message);
+
+    void LogInfo(const std::string& message);
+
+    Orthanc::Encoding GetDefaultEncoding();
   }
 }
diff --git a/Plugin/Dicom.cpp b/Plugin/Dicom.cpp
index e4410e4..901de8e 100644
--- a/Plugin/Dicom.cpp
+++ b/Plugin/Dicom.cpp
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -23,7 +23,6 @@
 #include "Plugin.h"
 #include "ChunkedBuffer.h"
 
-#include "../Orthanc/Core/OrthancException.h"
 #include "../Orthanc/Core/Toolbox.h"
 
 #include <gdcmDictEntry.h>
@@ -33,30 +32,6 @@
 
 namespace OrthancPlugins
 {
-  namespace
-  {
-    class ChunkedBufferWriter : public pugi::xml_writer
-    {
-    private:
-      ChunkedBuffer buffer_;
-
-    public:
-      virtual void write(const void *data, size_t size)
-      {
-        if (size > 0)
-        {
-          buffer_.AddChunk(reinterpret_cast<const char*>(data), size);
-        }
-      }
-
-      void Flatten(std::string& s)
-      {
-        buffer_.Flatten(s);
-      }
-    };
-  }
-
-
   static std::string MyStripSpaces(const std::string& source)
   {
     size_t first = 0;
@@ -89,12 +64,12 @@ namespace OrthancPlugins
 
   static const char* GetVRName(bool& isSequence,
                                const gdcm::Dict& dictionary,
-                               const gdcm::DataElement& element)
+                               const gdcm::Tag& tag,
+                               gdcm::VR vr)
   {
-    gdcm::VR vr = element.GetVR();
     if (vr == gdcm::VR::INVALID)
     {
-      const gdcm::DictEntry &entry = dictionary.GetDictEntry(element.GetTag());
+      const gdcm::DictEntry &entry = dictionary.GetDictEntry(tag);
       vr = entry.GetVR();
 
       if (vr == gdcm::VR::OB_OW)
@@ -125,6 +100,21 @@ namespace OrthancPlugins
   }
 
 
+  const char* GetVRName(bool& isSequence,
+                        const gdcm::Dict& dictionary,
+                        const gdcm::Tag& tag)
+  {
+    return GetVRName(isSequence, dictionary, tag, gdcm::VR::INVALID);
+  }
+
+
+  static const char* GetVRName(bool& isSequence,
+                               const gdcm::Dict& dictionary,
+                               const gdcm::DataElement& element)
+  {
+    return GetVRName(isSequence, dictionary, element.GetTag(), element.GetVR());
+  }
+
 
   static bool ConvertDicomStringToUtf8(std::string& result,
                                        const gdcm::Dict& dictionary,
@@ -181,22 +171,32 @@ namespace OrthancPlugins
 
     // Parse the DICOM instance using GDCM
     reader_.SetStream(stream);
+
     if (!reader_.Read())
     {
-      /* "GDCM cannot read this DICOM instance of length " +
-         boost::lexical_cast<std::string>(dicom.size()) */
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      OrthancPlugins::Configuration::LogError("GDCM cannot decode this DICOM instance of length " +
+                                              boost::lexical_cast<std::string>(dicom.size()));
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat);
     }
   }
 
 
   ParsedDicomFile::ParsedDicomFile(const OrthancPlugins::MultipartItem& item)
   {
+    // TODO Avoid this unnecessary memcpy by defining a stream over the MultipartItem
     std::string dicom(item.data_, item.data_ + item.size_);
     Setup(dicom);
   }
 
 
+  ParsedDicomFile::ParsedDicomFile(const OrthancPlugins::MemoryBuffer& buffer)
+  {
+    // TODO Avoid this unnecessary memcpy by defining a stream over the MemoryBuffer
+    std::string dicom(buffer.GetData(), buffer.GetData() + buffer.GetSize());
+    Setup(dicom);
+  }
+
+
   static bool GetRawTag(std::string& result,
                         const gdcm::DataSet& dataset,
                         const gdcm::Tag& tag,
@@ -253,7 +253,7 @@ namespace OrthancPlugins
   {
     if (!GetDataSet().FindDataElement(tag))
     {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InexistentTag);
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InexistentTag);
     }
 
     const gdcm::DataElement& element = GetDataSet().GetDataElement(tag);
@@ -295,7 +295,7 @@ namespace OrthancPlugins
 
 
 
-  static std::string FormatTag(const gdcm::Tag& tag)
+  std::string FormatTag(const gdcm::Tag& tag)
   {
     char tmp[16];
     sprintf(tmp, "%04X%04X", tag.GetGroup(), tag.GetElement());
@@ -303,8 +303,8 @@ namespace OrthancPlugins
   }
 
 
-  static const char* GetKeyword(const gdcm::Dict& dictionary,
-                                const gdcm::Tag& tag)
+  const char* GetKeyword(const gdcm::Dict& dictionary,
+                         const gdcm::Tag& tag)
   {
     const gdcm::DictEntry &entry = dictionary.GetDictEntry(tag);
     const char* keyword = entry.GetKeyword();
@@ -319,7 +319,6 @@ namespace OrthancPlugins
       return "RetrieveURL";
     }
 
-    //throw Orthanc::OrthancException("Unknown keyword for tag: " + FormatTag(tag));
     return NULL;
   }
 
@@ -363,7 +362,7 @@ namespace OrthancPlugins
     }
     else
     {
-      return wadoBase + "studies/" + study + "/series/" + series + "/instances/" + instance + "/";
+      return Configuration::GetWadoUrl(wadoBase, study, series, instance);
     }
   }
 
@@ -381,8 +380,7 @@ namespace OrthancPlugins
     const gdcm::ByteValue* data = element.GetByteValue();
     if (!data)
     {
-      // Assume Latin-1 encoding (TODO add a parameter as in Orthanc)
-      return Orthanc::Encoding_Latin1;
+      return Configuration::GetDefaultEncoding();
     }
 
     std::string tmp(data->GetPointer(), data->GetLength());
@@ -395,8 +393,7 @@ namespace OrthancPlugins
     }
     else
     {
-      // Assume Latin-1 encoding (TODO add a parameter as in Orthanc)
-      return Orthanc::Encoding_Latin1;
+      return Configuration::GetDefaultEncoding();
     }
   }
 
@@ -424,12 +421,6 @@ namespace OrthancPlugins
       pugi::xml_node node = target.append_child("DicomAttribute");
       node.append_attribute("tag").set_value(FormatTag(it->GetTag()).c_str());
 
-      const char* keyword = GetKeyword(dictionary, it->GetTag());
-      if (keyword != NULL)
-      {
-        node.append_attribute("keyword").set_value(keyword);
-      }
-
       bool isSequence = false;
       std::string vr;
       if (it->GetTag() == DICOM_TAG_RETRIEVE_URL)
@@ -444,6 +435,12 @@ namespace OrthancPlugins
 
       node.append_attribute("vr").set_value(vr.c_str());
 
+      const char* keyword = GetKeyword(dictionary, it->GetTag());
+      if (keyword != NULL)
+      {
+        node.append_attribute("keyword").set_value(keyword);
+      }
+
       if (isSequence)
       {
         gdcm::SmartPointer<gdcm::SequenceOfItems> seq = it->GetValueAsSQ();
@@ -486,6 +483,10 @@ namespace OrthancPlugins
         {
           value.append_child(pugi::node_pcdata).set_value(tmp.c_str());
         }
+        else
+        {
+          value.append_child(pugi::node_pcdata).set_value("");
+        }
       }
     }
   }
@@ -542,6 +543,7 @@ namespace OrthancPlugins
 
       node["vr"] = vr.c_str();
 
+      bool ok = true;
       if (isSequence)
       {
         // Deal with sequences
@@ -565,6 +567,8 @@ namespace OrthancPlugins
             node["Value"].append(child);
           }
         }
+
+        ok = true;
       }
       else if (IsBulkData(vr))
       {
@@ -572,6 +576,7 @@ namespace OrthancPlugins
         if (!bulkUri.empty())
         {
           node["BulkDataURI"] = bulkUri + std::string(path);
+          ok = true;
         }
       }
       else
@@ -584,9 +589,18 @@ namespace OrthancPlugins
         {
           node["Value"].append(value.c_str());
         }
+        else
+        {
+          node["Value"].append("");
+        }
+
+        ok = true;
       }
 
-      target[FormatTag(it->GetTag())] = node;
+      if (ok)
+      {
+        target[FormatTag(it->GetTag())] = node;
+      }
     }
   }
 
@@ -654,7 +668,88 @@ namespace OrthancPlugins
 
   std::string ParsedDicomFile::GetWadoUrl(const OrthancPluginHttpRequest* request) const
   {
-    const std::string base = OrthancPlugins::Configuration::GetBaseUrl(configuration_, request);
+    const std::string base = OrthancPlugins::Configuration::GetBaseUrl(request);
     return OrthancPlugins::GetWadoUrl(base, GetDataSet());
   }
+
+
+  static inline uint16_t GetCharValue(char c)
+  {
+    if (c >= '0' && c <= '9')
+      return c - '0';
+    else if (c >= 'a' && c <= 'f')
+      return c - 'a' + 10;
+    else if (c >= 'A' && c <= 'F')
+      return c - 'A' + 10;
+    else
+      return 0;
+  }
+
+  static inline uint16_t GetTagValue(const char* c)
+  {
+    return ((GetCharValue(c[0]) << 12) + 
+            (GetCharValue(c[1]) << 8) + 
+            (GetCharValue(c[2]) << 4) + 
+            GetCharValue(c[3]));
+  }
+
+
+  gdcm::Tag ParseTag(const gdcm::Dict& dictionary,
+                     const std::string& key)
+  {
+    if (key.find('.') != std::string::npos)
+    {
+      OrthancPlugins::Configuration::LogError("This DICOMweb plugin does not support hierarchical queries: " + key);
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NotImplemented);
+    }
+
+    if (key.size() == 8 &&  // This is the DICOMweb convention
+        isxdigit(key[0]) &&
+        isxdigit(key[1]) &&
+        isxdigit(key[2]) &&
+        isxdigit(key[3]) &&
+        isxdigit(key[4]) &&
+        isxdigit(key[5]) &&
+        isxdigit(key[6]) &&
+        isxdigit(key[7]))        
+    {
+      return gdcm::Tag(GetTagValue(key.c_str()),
+                       GetTagValue(key.c_str() + 4));
+    }
+    else if (key.size() == 9 &&  // This is the Orthanc convention
+             isxdigit(key[0]) &&
+             isxdigit(key[1]) &&
+             isxdigit(key[2]) &&
+             isxdigit(key[3]) &&
+             key[4] == ',' &&
+             isxdigit(key[5]) &&
+             isxdigit(key[6]) &&
+             isxdigit(key[7]) &&
+             isxdigit(key[8]))        
+    {
+      return gdcm::Tag(GetTagValue(key.c_str()),
+                       GetTagValue(key.c_str() + 5));
+    }
+    else
+    {
+      gdcm::Tag tag;
+      dictionary.GetDictEntryByKeyword(key.c_str(), tag);
+
+      if (tag.IsIllegal() || tag.IsPrivate())
+      {
+        if (key.find('.') != std::string::npos)
+        {
+          OrthancPlugins::Configuration::LogError("This QIDO-RS implementation does not support search over sequences: " + key);
+          throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NotImplemented);
+        }
+        else
+        {
+          OrthancPlugins::Configuration::LogError("Illegal tag name in QIDO-RS: " + key);
+          throw OrthancPlugins::PluginException(OrthancPluginErrorCode_UnknownDicomTag);
+        }
+      }
+
+      return tag;
+    }
+  }
 }
diff --git a/Plugin/Dicom.h b/Plugin/Dicom.h
index d0f01c1..c9af9fb 100644
--- a/Plugin/Dicom.h
+++ b/Plugin/Dicom.h
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -22,7 +22,9 @@
 
 #include "Configuration.h"
 
+#include "../Orthanc/Core/ChunkedBuffer.h"
 #include "../Orthanc/Core/Enumerations.h"
+#include "../Orthanc/Plugins/Samples/Common/OrthancPluginCppWrapper.h"
 
 #include <gdcmReader.h>
 #include <gdcmDataSet.h>
@@ -61,6 +63,8 @@ namespace OrthancPlugins
   public:
     ParsedDicomFile(const OrthancPlugins::MultipartItem& item);
 
+    ParsedDicomFile(const OrthancPlugins::MemoryBuffer& item);
+
     ParsedDicomFile(const std::string& dicom)
     {
       Setup(dicom);
@@ -99,6 +103,10 @@ namespace OrthancPlugins
   };
 
 
+  const char* GetVRName(bool& isSequence /* out */,
+                        const gdcm::Dict& dictionary,
+                        const gdcm::Tag& tag);
+
   void GenerateSingleDicomAnswer(std::string& result,
                                  const std::string& wadoBase,
                                  const gdcm::Dict& dictionary,
@@ -114,4 +122,32 @@ namespace OrthancPlugins
                    const gdcm::DataSet& dicom,
                    bool isXml,
                    bool isBulkAccessible);
+
+  gdcm::Tag ParseTag(const gdcm::Dict& dictionary,
+                     const std::string& key);
+
+  std::string FormatTag(const gdcm::Tag& tag);
+
+  const char* GetKeyword(const gdcm::Dict& dictionary,
+                         const gdcm::Tag& tag);
+
+  class ChunkedBufferWriter : public pugi::xml_writer
+  {
+  private:
+    Orthanc::ChunkedBuffer buffer_;
+
+  public:
+    virtual void write(const void *data, size_t size)
+    {
+      if (size > 0)
+      {
+        buffer_.AddChunk(reinterpret_cast<const char*>(data), size);
+      }
+    }
+
+    void Flatten(std::string& s)
+    {
+      buffer_.Flatten(s);
+    }
+  };
 }
diff --git a/Plugin/DicomResults.cpp b/Plugin/DicomResults.cpp
index 77834b1..a4ef1d7 100644
--- a/Plugin/DicomResults.cpp
+++ b/Plugin/DicomResults.cpp
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -21,7 +21,11 @@
 #include "DicomResults.h"
 
 #include "Dicom.h"
-#include "../Orthanc/Core/OrthancException.h"
+#include "../Orthanc/Core/Toolbox.h"
+#include "../Orthanc/Plugins/Samples/Common/OrthancPluginCppWrapper.h"
+
+#include <boost/lexical_cast.hpp>
+#include <boost/noncopyable.hpp>
 
 namespace OrthancPlugins
 {
@@ -42,26 +46,22 @@ namespace OrthancPlugins
     if (isXml_ &&
         OrthancPluginStartMultipartAnswer(context_, output_, "related", "application/dicom+xml") != 0)
     {
-      OrthancPluginLogError(context_, "Unable to create a multipart stream of DICOM+XML answers");
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+      OrthancPlugins::Configuration::LogError("Unable to create a multipart stream of DICOM+XML answers");
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol);
     }
 
     jsonWriter_.AddChunk("[\n");
   }
 
 
-  void DicomResults::AddInternal(const gdcm::File* file,
-                                 const gdcm::DataSet& dicom)
+  void DicomResults::AddInternal(const std::string& item)
   {
     if (isXml_)
     {
-      std::string answer;
-      GenerateSingleDicomAnswer(answer, wadoBase_, dictionary_, file, dicom, true, isBulkAccessible_);
-
-      if (OrthancPluginSendMultipartItem(context_, output_, answer.c_str(), answer.size()) != 0)
+      if (OrthancPluginSendMultipartItem(context_, output_, item.c_str(), item.size()) != 0)
       {
-        OrthancPluginLogError(context_, "Unable to create a multipart stream of DICOM+XML answers");
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+        OrthancPlugins::Configuration::LogError("Unable to create a multipart stream of DICOM+XML answers");
+        throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol);
       }
     }
     else
@@ -71,14 +71,342 @@ namespace OrthancPlugins
         jsonWriter_.AddChunk(",\n");
       }
 
-      std::string item;
-      GenerateSingleDicomAnswer(item, wadoBase_, dictionary_, file, dicom, false, isBulkAccessible_);
       jsonWriter_.AddChunk(item);
     }
 
     isFirst_ = false;
   }
 
+
+  void DicomResults::AddInternal(const gdcm::File* file,
+                                 const gdcm::DataSet& dicom)
+  {
+    std::string item;
+
+    if (isXml_)
+    {
+      GenerateSingleDicomAnswer(item, wadoBase_, dictionary_, file, dicom, true, isBulkAccessible_);
+    }
+    else
+    {
+      GenerateSingleDicomAnswer(item, wadoBase_, dictionary_, file, dicom, false, isBulkAccessible_);
+    }
+
+    AddInternal(item);
+
+    isFirst_ = false;
+  }
+
+
+
+  namespace
+  {
+    class ITagVisitor : public boost::noncopyable
+    {
+    public:
+      virtual ~ITagVisitor()
+      {
+      }
+
+      virtual void Visit(const gdcm::Tag& tag,
+                         bool isSequence,
+                         const std::string& vr,
+                         const std::string& type,
+                         const Json::Value& value) = 0;
+
+      static void Apply(ITagVisitor& visitor,
+                        const Json::Value& source,
+                        const gdcm::Dict& dictionary)
+      {
+        if (source.type() != Json::objectValue)
+        {
+          throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError);
+        }
+
+        Json::Value::Members members = source.getMemberNames();
+        for (size_t i = 0; i < members.size(); i++)
+        {
+          if (members[i].size() != 9 ||
+              members[i][4] != ',' ||
+              source[members[i]].type() != Json::objectValue ||
+              !source[members[i]].isMember("Value") ||
+              !source[members[i]].isMember("Type") ||
+              source[members[i]]["Type"].type() != Json::stringValue)
+          {
+            throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError);
+          }        
+
+          const Json::Value& value = source[members[i]]["Value"];
+          const std::string type = source[members[i]]["Type"].asString();
+
+          gdcm::Tag tag(OrthancPlugins::ParseTag(dictionary, members[i]));
+
+          bool isSequence = false;
+          std::string vr = GetVRName(isSequence, dictionary, tag);
+
+          if (tag == DICOM_TAG_RETRIEVE_URL)
+          {
+            // The VR of this attribute has changed from UT to UR.
+            vr = "UR";
+          }
+          else
+          {
+            vr = GetVRName(isSequence, dictionary, tag);
+          }
+
+          visitor.Visit(tag, isSequence, vr, type, value);
+        }
+      }
+    };
+
+
+    class TagVisitorBase : public ITagVisitor
+    {
+    protected:
+      const Json::Value&  source_;
+      const gdcm::Dict&   dictionary_;
+      const std::string&  bulkUri_;
+
+    public:
+      TagVisitorBase(const Json::Value&  source,
+                     const gdcm::Dict&   dictionary,
+                     const std::string&  bulkUri) :
+        source_(source),
+        dictionary_(dictionary),
+        bulkUri_(bulkUri)
+      {
+      }
+    };
+
+
+    class JsonVisitor : public TagVisitorBase
+    {
+    private:
+      Json::Value&   target_;
+
+    public:
+      JsonVisitor(Json::Value&        target,
+                  const Json::Value&  source,
+                  const gdcm::Dict&   dictionary,
+                  const std::string&  bulkUri) :
+        TagVisitorBase(source, dictionary, bulkUri),
+        target_(target)
+      {
+        target_ = Json::objectValue;
+      }
+
+      virtual void Visit(const gdcm::Tag& tag,
+                         bool isSequence,
+                         const std::string& vr,
+                         const std::string& type,
+                         const Json::Value& value)
+      {
+        const std::string formattedTag = OrthancPlugins::FormatTag(tag);
+
+        Json::Value node = Json::objectValue;
+        node["vr"] = vr;
+
+        bool ok = false;
+        if (isSequence)
+        {
+          // Deal with sequences
+          if (type != "Sequence" ||
+              value.type() != Json::arrayValue)
+          {
+            throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError);
+          }
+
+          node["Value"] = Json::arrayValue;
+
+          for (Json::Value::ArrayIndex i = 0; i < value.size(); i++)
+          {
+            if (value[i].type() != Json::objectValue)
+            {
+              throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError);
+            }
+
+            Json::Value child;
+
+            std::string childUri;
+            if (!bulkUri_.empty())
+            {
+              std::string number = boost::lexical_cast<std::string>(i);
+              childUri = bulkUri_ + formattedTag + "/" + number + "/";
+            }
+
+            JsonVisitor visitor(child, value[i], dictionary_, childUri);
+            JsonVisitor::Apply(visitor, value[i], dictionary_);
+
+            node["Value"].append(child);
+          }
+
+          ok = true;
+        }
+        else if (type == "String" &&
+                 value.type() == Json::stringValue)
+        {
+          // Deal with string representations
+          node["Value"] = Json::arrayValue;
+          node["Value"].append(value.asString());
+          ok = true;
+        }
+        else
+        {
+          // Bulk data
+          if (!bulkUri_.empty())
+          {
+            node["BulkDataURI"] = bulkUri_ + formattedTag;
+            ok = true;
+          }
+        }
+
+        if (ok)
+        {
+          target_[formattedTag] = node;
+        }
+      }
+    };
+
+
+    class XmlVisitor : public TagVisitorBase
+    {
+    private:
+      pugi::xml_node&  target_;
+
+    public:
+      XmlVisitor(pugi::xml_node&     target,
+                 const Json::Value&  source,
+                 const gdcm::Dict&   dictionary,
+                 const std::string&  bulkUri) :
+        TagVisitorBase(source, dictionary, bulkUri),
+        target_(target)
+      {
+      }
+
+      virtual void Visit(const gdcm::Tag& tag,
+                         bool isSequence,
+                         const std::string& vr,
+                         const std::string& type,
+                         const Json::Value& value)
+      {
+        const std::string formattedTag = OrthancPlugins::FormatTag(tag);
+
+        pugi::xml_node node = target_.append_child("DicomAttribute");
+        node.append_attribute("tag").set_value(formattedTag.c_str());
+        node.append_attribute("vr").set_value(vr.c_str());
+
+        const char* keyword = GetKeyword(dictionary_, tag);
+        if (keyword != NULL)
+        {
+          node.append_attribute("keyword").set_value(keyword);
+        }
+
+        if (isSequence)
+        {
+          // Deal with sequences
+          if (type != "Sequence" ||
+              value.type() != Json::arrayValue)
+          {
+            throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError);
+          }
+
+          for (Json::Value::ArrayIndex i = 0; i < value.size(); i++)
+          {
+            if (value[i].type() != Json::objectValue)
+            {
+              throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError);
+            }
+
+            pugi::xml_node child = node.append_child("Item");
+            std::string number = boost::lexical_cast<std::string>(i + 1);
+            child.append_attribute("number").set_value(number.c_str());
+
+            std::string childUri;
+            if (!bulkUri_.empty())
+            {
+              childUri = bulkUri_ + formattedTag + "/" + number + "/";
+            }
+
+            XmlVisitor visitor(child, value[i], dictionary_, childUri);
+            XmlVisitor::Apply(visitor, value[i], dictionary_);
+          }
+        }
+        else if (type == "String" &&
+                 value.type() == Json::stringValue)
+        {
+          // Deal with string representations
+          pugi::xml_node item = node.append_child("Value");
+          item.append_attribute("number").set_value("1");
+          item.append_child(pugi::node_pcdata).set_value(value.asCString());
+        }
+        else
+        {
+          // Bulk data
+          if (!bulkUri_.empty())
+          {
+            pugi::xml_node value = node.append_child("BulkData");
+            std::string uri = bulkUri_ + formattedTag;
+            value.append_attribute("uri").set_value(uri.c_str());
+          }
+        }
+      }
+    };
+  }
+
+
+  static void OrthancToDicomWebXml(pugi::xml_document& target,
+                                   const Json::Value& source,
+                                   const gdcm::Dict& dictionary,
+                                   const std::string& bulkUriRoot)
+  {
+    pugi::xml_node root = target.append_child("NativeDicomModel");
+    root.append_attribute("xmlns").set_value("http://dicom.nema.org/PS3.19/models/NativeDICOM");
+    root.append_attribute("xsi:schemaLocation").set_value("http://dicom.nema.org/PS3.19/models/NativeDICOM");
+    root.append_attribute("xmlns:xsi").set_value("http://www.w3.org/2001/XMLSchema-instance");
+
+    XmlVisitor visitor(root, source, dictionary, bulkUriRoot);
+    ITagVisitor::Apply(visitor, source, dictionary);
+
+    pugi::xml_node decl = target.prepend_child(pugi::node_declaration);
+    decl.append_attribute("version").set_value("1.0");
+    decl.append_attribute("encoding").set_value("utf-8");
+  }
+
+
+  void DicomResults::AddFromOrthanc(const Json::Value& dicom,
+                                    const std::string& wadoUrl)
+  { 
+    std::string bulkUriRoot;
+    if (isBulkAccessible_)
+    {
+      bulkUriRoot = wadoUrl + "bulk/";
+    }
+
+    if (isXml_)
+    {
+      pugi::xml_document doc;
+      OrthancToDicomWebXml(doc, dicom, dictionary_, bulkUriRoot);
+    
+      ChunkedBufferWriter writer;
+      doc.save(writer, "  ", pugi::format_default, pugi::encoding_utf8);
+
+      std::string item;
+      writer.Flatten(item);
+
+      AddInternal(item);
+    }
+    else
+    {
+      Json::Value v;
+      JsonVisitor visitor(v, dicom, dictionary_, bulkUriRoot);
+      ITagVisitor::Apply(visitor, dicom, dictionary_);
+
+      Json::FastWriter writer;
+      AddInternal(writer.write(v));
+    }
+  }
+
+
   void DicomResults::Answer()
   {
     if (isXml_)
diff --git a/Plugin/DicomResults.h b/Plugin/DicomResults.h
index 2f6de65..bc7e09a 100644
--- a/Plugin/DicomResults.h
+++ b/Plugin/DicomResults.h
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -26,6 +26,7 @@
 #include <gdcmDataSet.h>
 #include <gdcmDict.h>
 #include <gdcmFile.h>
+#include <json/value.h>
 
 namespace OrthancPlugins
 {
@@ -36,11 +37,13 @@ namespace OrthancPlugins
     OrthancPluginRestOutput*  output_;
     std::string               wadoBase_;
     const gdcm::Dict&         dictionary_;
-    ChunkedBuffer             jsonWriter_;  // Used for JSON output
+    Orthanc::ChunkedBuffer    jsonWriter_;  // Used for JSON output
     bool                      isFirst_; 
     bool                      isXml_;
     bool                      isBulkAccessible_;
 
+    void AddInternal(const std::string& item);
+
     void AddInternal(const gdcm::File* file,
                      const gdcm::DataSet& dicom);
 
@@ -63,6 +66,9 @@ namespace OrthancPlugins
       AddInternal(&file, subset);
     }
 
+    void AddFromOrthanc(const Json::Value& dicom,
+                        const std::string& wadoUrl);
+
     void Answer();
   };
 }
diff --git a/Plugin/DicomWebClient.cpp b/Plugin/DicomWebClient.cpp
new file mode 100644
index 0000000..4259c02
--- /dev/null
+++ b/Plugin/DicomWebClient.cpp
@@ -0,0 +1,616 @@
+/**
+ * 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 "DicomWebClient.h"
+
+#include "Plugin.h"
+#include "DicomWebServers.h"
+
+#include <json/reader.h>
+#include <list>
+#include <set>
+#include <boost/lexical_cast.hpp>
+
+#include "../Orthanc/Core/ChunkedBuffer.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 OrthancPlugins::PluginException(OrthancPluginErrorCode_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& server)
+{
+  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)
+  {
+    OrthancPlugins::Configuration::LogError("The STOW-RS JSON response from DICOMweb server " + server + 
+                                            " does not contain the mandatory tag " + upper);
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol);
+  }
+  else
+  {
+    return false;
+  }
+
+  if (value->type() != Json::objectValue ||
+      !value->isMember("Value") ||
+      (*value) ["Value"].type() != Json::arrayValue)
+  {
+    OrthancPlugins::Configuration::LogError("Unable to parse STOW-RS JSON response from DICOMweb server " + server);
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol);
+  }
+
+  result = (*value) ["Value"].size();
+  return true;
+}
+
+
+
+static void ParseStowRequest(std::list<std::string>& instances /* out */,
+                             std::map<std::string, std::string>& httpHeaders /* out */,
+                             const OrthancPluginHttpRequest* request /* in */)
+{
+  static const char* RESOURCES = "Resources";
+  static const char* HTTP_HEADERS = "HttpHeaders";
+
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
+  Json::Value body;
+  Json::Reader reader;
+  if (!reader.parse(request->body, request->body + request->bodySize, body) ||
+      body.type() != Json::objectValue ||
+      !body.isMember(RESOURCES) ||
+      body[RESOURCES].type() != Json::arrayValue)
+  {
+    OrthancPlugins::Configuration::LogError("A request to the DICOMweb STOW-RS client must provide a JSON object "
+                                            "with the field \"" + std::string(RESOURCES) + 
+                                            "\" containing an array of resources to be sent");
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat);
+  }
+
+  OrthancPlugins::ParseAssociativeArray(httpHeaders, body, HTTP_HEADERS);
+
+  Json::Value& resources = body[RESOURCES];
+
+  // Extract information about all the child instances
+  for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++)
+  {
+    if (resources[i].type() != Json::stringValue)
+    {
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat);
+    }
+
+    std::string resource = resources[i].asString();
+    if (resource.empty())
+    {
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_UnknownResource);
+    }
+
+    // Test whether this resource is an instance
+    Json::Value tmp;
+    if (OrthancPlugins::RestApiGetJson(tmp, context, "/instances/" + resource, false))
+    {
+      AddInstance(instances, tmp);
+    }
+    // This was not an instance, successively try with series/studies/patients
+    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 OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError);
+      }
+
+      for (Json::Value::ArrayIndex j = 0; j < tmp.size(); j++)
+      {
+        AddInstance(instances, tmp[j]);
+      }
+    }
+    else
+    {
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_UnknownResource);
+    }   
+  }
+}
+
+
+static void SendStowChunks(const Orthanc::WebServiceParameters& server,
+                           const std::map<std::string, std::string>& httpHeaders,
+                           const std::string& boundary,
+                           Orthanc::ChunkedBuffer& chunks,
+                           size_t& countInstances,
+                           bool force)
+{
+  unsigned int maxInstances = OrthancPlugins::Configuration::GetUnsignedIntegerValue("StowMaxInstances", 10);
+  size_t maxSize = static_cast<size_t>(OrthancPlugins::Configuration::GetUnsignedIntegerValue("StowMaxSize", 10)) * 1024 * 1024;
+
+  if ((force && countInstances > 0) ||
+      (maxInstances != 0 && countInstances >= maxInstances) ||
+      (maxSize != 0 && chunks.GetNumBytes() >= maxSize))
+  {
+    chunks.AddChunk("\r\n--" + boundary + "--\r\n");
+
+    std::string body;
+    chunks.Flatten(body);
+
+    OrthancPlugins::MemoryBuffer answerBody(OrthancPlugins::Configuration::GetContext());
+    std::map<std::string, std::string> answerHeaders;
+    OrthancPlugins::CallServer(answerBody, answerHeaders, server, OrthancPluginHttpMethod_Post,
+                               httpHeaders, "studies", body);
+
+    Json::Value response;
+    Json::Reader reader;
+    bool success = reader.parse(reinterpret_cast<const char*>((*answerBody)->data),
+                                reinterpret_cast<const char*>((*answerBody)->data) + (*answerBody)->size, response);
+    answerBody.Clear();
+
+    if (!success ||
+        response.type() != Json::objectValue ||
+        !response.isMember("00081199"))
+    {
+      OrthancPlugins::Configuration::LogError("Unable to parse STOW-RS JSON response from DICOMweb server " + server.GetUrl());
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol);
+    }
+
+    size_t size;
+    if (!GetSequenceSize(size, response, "00081199", true, server.GetUrl()) ||
+        size != countInstances)
+    {
+      OrthancPlugins::Configuration::LogError("The STOW-RS server was only able to receive " + 
+                                              boost::lexical_cast<std::string>(size) + " instances out of " +
+                                              boost::lexical_cast<std::string>(countInstances));
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol);
+    }
+
+    if (GetSequenceSize(size, response, "00081198", false, server.GetUrl()) &&
+        size != 0)
+    {
+      OrthancPlugins::Configuration::LogError("The response from the STOW-RS server contains " + 
+                                              boost::lexical_cast<std::string>(size) + 
+                                              " items in its Failed SOP Sequence (0008,1198) tag");
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol);    
+    }
+
+    if (GetSequenceSize(size, response, "0008119A", false, server.GetUrl()) &&
+        size != 0)
+    {
+      OrthancPlugins::Configuration::LogError("The response from the STOW-RS server contains " + 
+                                              boost::lexical_cast<std::string>(size) + 
+                                              " items in its Other Failures Sequence (0008,119A) tag");
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol);    
+    }
+
+    countInstances = 0;
+  }
+}
+
+
+void StowClient(OrthancPluginRestOutput* output,
+                const char* /*url*/,
+                const OrthancPluginHttpRequest* request)
+{
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
+  if (request->groupsCount != 1)
+  {
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadRequest);
+  }
+
+  if (request->method != OrthancPluginHttpMethod_Post)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "POST");
+    return;
+  }
+
+  Orthanc::WebServiceParameters server(OrthancPlugins::DicomWebServers::GetInstance().GetServer(request->groups[0]));
+
+  std::string boundary;
+
+  {
+    char* uuid = OrthancPluginGenerateUuid(context);
+    try
+    {
+      boundary.assign(uuid);
+    }
+    catch (...)
+    {
+      OrthancPluginFreeString(context, uuid);
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NotEnoughMemory);
+    }
+
+    OrthancPluginFreeString(context, uuid);
+  }
+
+  std::string mime = "multipart/related; type=application/dicom; boundary=" + boundary;
+
+  std::map<std::string, std::string> httpHeaders;
+  httpHeaders["Accept"] = "application/json";
+  httpHeaders["Expect"] = "";
+  httpHeaders["Content-Type"] = mime;
+
+  std::list<std::string> instances;
+  ParseStowRequest(instances, httpHeaders, request);
+
+  OrthancPlugins::Configuration::LogInfo("Sending " + boost::lexical_cast<std::string>(instances.size()) + 
+                                         " instances using STOW-RS to DICOMweb server: " + server.GetUrl());
+
+  Orthanc::ChunkedBuffer chunks;
+  size_t countInstances = 0;
+
+  for (std::list<std::string>::const_iterator it = instances.begin(); it != instances.end(); it++)
+  {
+    OrthancPlugins::MemoryBuffer dicom(context);
+    if (dicom.RestApiGet("/instances/" + *it + "/file", false))
+    {
+      chunks.AddChunk("\r\n--" + boundary + "\r\n" +
+                      "Content-Type: application/dicom\r\n" +
+                      "Content-Length: " + boost::lexical_cast<std::string>(dicom.GetSize()) +
+                      "\r\n\r\n");
+      chunks.AddChunk(dicom.GetData(), dicom.GetSize());
+      countInstances ++;
+
+      SendStowChunks(server, httpHeaders, boundary, chunks, countInstances, false);
+    }
+  }
+
+  SendStowChunks(server, httpHeaders, boundary, chunks, countInstances, true);
+
+  std::string answer = "{}\n";
+  OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json");
+}
+
+
+static bool GetStringValue(std::string& target,
+                           const Json::Value& json,
+                           const std::string& key)
+{
+  if (json.type() != Json::objectValue)
+  {
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat);
+  }
+  else if (!json.isMember(key))
+  {
+    target.clear();
+    return false;
+  }
+  else if (json[key].type() != Json::stringValue)
+  {
+    OrthancPlugins::Configuration::LogError("The field \"" + key + "\" in a JSON object should be a string");
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat);
+  }
+  else
+  {
+    target = json[key].asString();
+    return true;
+  }
+}
+
+
+void GetFromServer(OrthancPluginRestOutput* output,
+                   const char* /*url*/,
+                   const OrthancPluginHttpRequest* request)
+{
+  static const char* URI = "Uri";
+  static const char* HTTP_HEADERS = "HttpHeaders";
+  static const char* GET_ARGUMENTS = "Arguments";
+
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
+  if (request->method != OrthancPluginHttpMethod_Post)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "POST");
+    return;
+  }
+
+  Orthanc::WebServiceParameters server(OrthancPlugins::DicomWebServers::GetInstance().GetServer(request->groups[0]));
+
+  std::string tmp;
+  Json::Value body;
+  Json::Reader reader;
+  if (!reader.parse(request->body, request->body + request->bodySize, body) ||
+      body.type() != Json::objectValue ||
+      !GetStringValue(tmp, body, URI))
+  {
+    OrthancPlugins::Configuration::LogError("A request to the DICOMweb STOW-RS client must provide a JSON object "
+                                            "with the field \"Uri\" containing the URI of interest");
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat);
+  }
+
+  std::map<std::string, std::string> getArguments;
+  OrthancPlugins::ParseAssociativeArray(getArguments, body, GET_ARGUMENTS);
+
+  std::string uri;
+  OrthancPlugins::UriEncode(uri, tmp, getArguments);
+
+  std::map<std::string, std::string> httpHeaders;
+  OrthancPlugins::ParseAssociativeArray(httpHeaders, body, HTTP_HEADERS);
+
+  OrthancPlugins::MemoryBuffer answerBody(context);
+  std::map<std::string, std::string> answerHeaders;
+  OrthancPlugins::CallServer(answerBody, answerHeaders, server, OrthancPluginHttpMethod_Get, httpHeaders, uri, "");
+
+  std::string contentType = "application/octet-stream";
+
+  for (std::map<std::string, std::string>::const_iterator
+         it = answerHeaders.begin(); it != answerHeaders.end(); ++it)
+  {
+    std::string key = it->first;
+    Orthanc::Toolbox::ToLowerCase(key);
+
+    if (key == "content-type")
+    {
+      contentType = it->second;
+    }
+    else if (key == "transfer-encoding")
+    {
+      // Do not forward this header
+    }
+    else
+    {
+      OrthancPluginSetHttpHeader(context, output, it->first.c_str(), it->second.c_str());
+    }
+  }
+
+  OrthancPluginAnswerBuffer(context, output, 
+                            reinterpret_cast<const char*>(answerBody.GetData()),
+                            answerBody.GetSize(), contentType.c_str());
+}
+
+
+
+static void RetrieveFromServerInternal(std::set<std::string>& instances,
+                                       const Orthanc::WebServiceParameters& server,
+                                       const std::map<std::string, std::string>& httpHeaders,
+                                       const Json::Value& resource)
+{
+  static const std::string STUDY = "Study";
+  static const std::string SERIES = "Series";
+  static const std::string INSTANCE = "Instance";
+  static const std::string MULTIPART_RELATED = "multipart/related";
+  static const std::string APPLICATION_DICOM = "application/dicom";
+
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
+  if (resource.type() != Json::objectValue)
+  {
+    OrthancPlugins::Configuration::LogError("Resources of interest for the DICOMweb WADO-RS Retrieve client "
+                                            "must be provided as a JSON object");
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat);
+  }
+
+  std::string study, series, instance;
+  if (!GetStringValue(study, resource, STUDY) ||
+      study.empty())
+  {
+    OrthancPlugins::Configuration::LogError("A non-empty \"" + STUDY + "\" field is mandatory for the "
+                                            "DICOMweb WADO-RS Retrieve client");
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat);
+  }
+
+  GetStringValue(series, resource, SERIES);
+  GetStringValue(instance, resource, INSTANCE);
+
+  if (series.empty() && 
+      !instance.empty())
+  {
+    OrthancPlugins::Configuration::LogError("When specifying a \"" + INSTANCE + "\" field in a call to DICOMweb "
+                                            "WADO-RS Retrieve client, the \"" + SERIES + "\" field is mandatory");
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat);
+  }
+
+  std::string uri = "studies/" + study;
+  if (!series.empty())
+  {
+    uri += "/series/" + series;
+    if (!instance.empty())
+    {
+      uri += "/instances/" + instance;
+    }
+  }
+
+  OrthancPlugins::MemoryBuffer answerBody(context);
+  std::map<std::string, std::string> answerHeaders;
+  OrthancPlugins::CallServer(answerBody, answerHeaders, server, OrthancPluginHttpMethod_Get, httpHeaders, uri, "");
+
+  std::vector<std::string> contentType;
+  for (std::map<std::string, std::string>::const_iterator 
+         it = answerHeaders.begin(); it != answerHeaders.end(); ++it)
+  {
+    std::string s = Orthanc::Toolbox::StripSpaces(it->first);
+    Orthanc::Toolbox::ToLowerCase(s);
+    if (s == "content-type")
+    {
+      Orthanc::Toolbox::TokenizeString(contentType, it->second, ';');
+      break;
+    }
+  }
+
+  if (contentType.empty())
+  {
+    OrthancPlugins::Configuration::LogError("No Content-Type provided by the remote WADO-RS server");
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol);
+  }
+
+  Orthanc::Toolbox::ToLowerCase(contentType[0]);
+  if (Orthanc::Toolbox::StripSpaces(contentType[0]) != MULTIPART_RELATED)
+  {
+    OrthancPlugins::Configuration::LogError("The remote WADO-RS server answers with a \"" + contentType[0] +
+                                            "\" Content-Type, but \"" + MULTIPART_RELATED + "\" is expected");
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol);
+  }
+
+  std::string type, boundary;
+  for (size_t i = 1; i < contentType.size(); i++)
+  {
+    std::vector<std::string> tokens;
+    Orthanc::Toolbox::TokenizeString(tokens, contentType[i], '=');
+
+    if (tokens.size() == 2)
+    {
+      std::string s = Orthanc::Toolbox::StripSpaces(tokens[0]);
+      Orthanc::Toolbox::ToLowerCase(s);
+
+      if (s == "type")
+      {
+        type = Orthanc::Toolbox::StripSpaces(tokens[1]);
+        Orthanc::Toolbox::ToLowerCase(type);
+      }
+      else if (s == "boundary")
+      {
+        boundary = Orthanc::Toolbox::StripSpaces(tokens[1]);
+      }
+    }
+  }
+
+  if (type != APPLICATION_DICOM)
+  {
+    OrthancPlugins::Configuration::LogError("The remote WADO-RS server answers with a \"" + type +
+                                            "\" multipart Content-Type, but \"" + APPLICATION_DICOM + "\" is expected");
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol);
+  }
+
+  if (boundary.empty())
+  {
+    OrthancPlugins::Configuration::LogError("The remote WADO-RS server does not provide a boundary for its multipart answer");
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol);
+  }
+
+  std::vector<OrthancPlugins::MultipartItem> parts;
+  OrthancPlugins::ParseMultipartBody(parts, context, 
+                                     reinterpret_cast<const char*>(answerBody.GetData()),
+                                     answerBody.GetSize(), boundary);
+
+  OrthancPlugins::Configuration::LogInfo("The remote WADO-RS server has provided " +
+                                         boost::lexical_cast<std::string>(parts.size()) + 
+                                         " DICOM instances");
+
+  for (size_t i = 0; i < parts.size(); i++)
+  {
+    if (parts[i].contentType_ != APPLICATION_DICOM)
+    {
+      OrthancPlugins::Configuration::LogError("The remote WADO-RS server has provided a non-DICOM file in its multipart answer");
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol);      
+    }
+
+    OrthancPlugins::MemoryBuffer tmp(context);
+    tmp.RestApiPost("/instances", parts[i].data_, parts[i].size_, false);
+
+    Json::Value result;
+    tmp.ToJson(result);
+
+    if (result.type() != Json::objectValue ||
+        !result.isMember("ID") ||
+        result["ID"].type() != Json::stringValue)
+    {
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError);      
+    }
+    else
+    {
+      instances.insert(result["ID"].asString());
+    }
+  }
+}
+
+
+
+void RetrieveFromServer(OrthancPluginRestOutput* output,
+                        const char* /*url*/,
+                        const OrthancPluginHttpRequest* request)
+{
+  static const std::string RESOURCES("Resources");
+  static const char* HTTP_HEADERS = "HttpHeaders";
+
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
+  if (request->method != OrthancPluginHttpMethod_Post)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "POST");
+    return;
+  }
+
+  Orthanc::WebServiceParameters server(OrthancPlugins::DicomWebServers::GetInstance().GetServer(request->groups[0]));
+
+  Json::Value body;
+  Json::Reader reader;
+  if (!reader.parse(request->body, request->body + request->bodySize, body) ||
+      body.type() != Json::objectValue ||
+      !body.isMember(RESOURCES) ||
+      body[RESOURCES].type() != Json::arrayValue)
+  {
+    OrthancPlugins::Configuration::LogError("A request to the DICOMweb WADO-RS Retrieve client must provide a JSON object "
+                                            "with the field \"" + RESOURCES + "\" containing an array of resources");
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat);
+  }
+
+  std::map<std::string, std::string> httpHeaders;
+  OrthancPlugins::ParseAssociativeArray(httpHeaders, body, HTTP_HEADERS);
+
+  std::set<std::string> instances;
+  for (Json::Value::ArrayIndex i = 0; i < body[RESOURCES].size(); i++)
+  {
+    RetrieveFromServerInternal(instances, server, httpHeaders, body[RESOURCES][i]);
+  }
+
+  Json::Value status = Json::objectValue;
+  status["Instances"] = Json::arrayValue;
+  
+  for (std::set<std::string>::const_iterator
+         it = instances.begin(); it != instances.end(); ++it)
+  {
+    status["Instances"].append(*it);
+  }
+
+  std::string s = status.toStyledString();
+  OrthancPluginAnswerBuffer(context, output, s.c_str(), s.size(), "application/json");
+}
diff --git a/Plugin/QidoRs.h b/Plugin/DicomWebClient.h
similarity index 65%
copy from Plugin/QidoRs.h
copy to Plugin/DicomWebClient.h
index 5e63d86..79922db 100644
--- a/Plugin/QidoRs.h
+++ b/Plugin/DicomWebClient.h
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -23,14 +23,14 @@
 #include "Configuration.h"
 
 
-void SearchForStudies(OrthancPluginRestOutput* output,
-                      const char* url,
-                      const OrthancPluginHttpRequest* request);
+void StowClient(OrthancPluginRestOutput* output,
+                const char* url,
+                const OrthancPluginHttpRequest* request);
 
-void SearchForSeries(OrthancPluginRestOutput* output,
-                     const char* url,
-                     const OrthancPluginHttpRequest* request);
+void GetFromServer(OrthancPluginRestOutput* output,
+                   const char* /*url*/,
+                   const OrthancPluginHttpRequest* request);
 
-void SearchForInstances(OrthancPluginRestOutput* output,
-                        const char* url,
+void RetrieveFromServer(OrthancPluginRestOutput* output,
+                        const char* /*url*/,
                         const OrthancPluginHttpRequest* request);
diff --git a/Plugin/DicomWebServers.cpp b/Plugin/DicomWebServers.cpp
new file mode 100644
index 0000000..bbc8112
--- /dev/null
+++ b/Plugin/DicomWebServers.cpp
@@ -0,0 +1,279 @@
+/**
+ * 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 "DicomWebServers.h"
+
+#include "Configuration.h"
+#include "../Orthanc/Core/Toolbox.h"
+
+namespace OrthancPlugins
+{
+  void DicomWebServers::Clear()
+  {
+    for (Servers::iterator it = servers_.begin(); it != servers_.end(); ++it)
+    {
+      delete it->second;
+    }
+  }
+
+
+  void DicomWebServers::Load(const Json::Value& servers)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Clear();
+
+    bool ok = true;
+
+    try
+    {
+      if (servers.type() != Json::objectValue)
+      {
+        ok = false;
+      }
+      else
+      {
+        Json::Value::Members members = servers.getMemberNames();
+
+        for (size_t i = 0; i < members.size(); i++)
+        {
+          std::auto_ptr<Orthanc::WebServiceParameters> parameters(new Orthanc::WebServiceParameters);
+          parameters->FromJson(servers[members[i]]);
+
+          servers_[members[i]] = parameters.release();
+        }
+      }
+    }
+    catch (Orthanc::OrthancException& e)
+    {
+      OrthancPlugins::Configuration::LogError("Exception while parsing the \"DicomWeb.Servers\" section "
+                                              "of the configuration file: " + std::string(e.What()));
+      throw;
+    }
+
+    if (!ok)
+    {
+      OrthancPlugins::Configuration::LogError("Cannot parse the \"DicomWeb.Servers\" section of the configuration file");
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat);
+    }
+  }
+
+
+  DicomWebServers& DicomWebServers::GetInstance()
+  {
+    static DicomWebServers singleton;
+    return singleton;
+  }
+
+
+  Orthanc::WebServiceParameters DicomWebServers::GetServer(const std::string& name)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    Servers::const_iterator server = servers_.find(name);
+
+    if (server == servers_.end() ||
+        server->second == NULL)
+    {
+      OrthancPlugins::Configuration::LogError("Inexistent server: " + name);
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InexistentItem);
+    }
+    else
+    {
+      return *server->second;
+    }
+  }
+
+
+  void DicomWebServers::ListServers(std::list<std::string>& servers)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    servers.clear();
+    for (Servers::const_iterator it = servers_.begin(); it != servers_.end(); ++it)
+    {
+      servers.push_back(it->first);
+    }
+  }
+
+
+  static const char* ConvertToCString(const std::string& s)
+  {
+    if (s.empty())
+    {
+      return NULL;
+    }
+    else
+    {
+      return s.c_str();
+    }
+  }
+
+
+
+  void CallServer(OrthancPlugins::MemoryBuffer& answerBody /* out */,
+                  std::map<std::string, std::string>& answerHeaders /* out */,
+                  const Orthanc::WebServiceParameters& server,
+                  OrthancPluginHttpMethod method,
+                  const std::map<std::string, std::string>& httpHeaders,
+                  const std::string& uri,
+                  const std::string& body)
+  {
+    answerBody.Clear();
+    answerHeaders.clear();
+
+    std::string url = server.GetUrl();
+    assert(!url.empty() && url[url.size() - 1] == '/');
+
+    // Remove the leading "/" in the URI if need be
+    std::string tmp;
+    if (!uri.empty() &&
+        uri[0] == '/')
+    {
+      url += uri.substr(1);
+    }
+    else
+    {
+      url += uri;
+    }
+
+    std::vector<const char*> httpHeadersKeys(httpHeaders.size());
+    std::vector<const char*> httpHeadersValues(httpHeaders.size());
+
+    {
+      size_t pos = 0;
+      for (std::map<std::string, std::string>::const_iterator
+             it = httpHeaders.begin(); it != httpHeaders.end(); ++it)
+      {
+        httpHeadersKeys[pos] = it->first.c_str();
+        httpHeadersValues[pos] = it->second.c_str();
+        pos += 1;
+      }
+    }
+
+    const char* bodyContent = NULL;
+    size_t bodySize = 0;
+
+    if ((method == OrthancPluginHttpMethod_Put ||
+         method == OrthancPluginHttpMethod_Post) &&
+        !body.empty())
+    {
+      bodyContent = body.c_str();
+      bodySize = body.size();
+    }
+
+    OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
+    uint16_t status = 0;
+    MemoryBuffer answerHeadersTmp(context);
+    OrthancPluginErrorCode code = OrthancPluginHttpClient(
+      context, 
+      /* Outputs */
+      *answerBody, *answerHeadersTmp, &status, 
+      method,
+      url.c_str(), 
+      /* HTTP headers*/
+      httpHeaders.size(),
+      httpHeadersKeys.empty() ? NULL : &httpHeadersKeys[0],
+      httpHeadersValues.empty() ? NULL : &httpHeadersValues[0],
+      bodyContent, bodySize,
+      ConvertToCString(server.GetUsername()), /* Authentication */
+      ConvertToCString(server.GetPassword()), 
+      0,                                      /* Timeout */
+      ConvertToCString(server.GetCertificateFile()),
+      ConvertToCString(server.GetCertificateKeyFile()),
+      ConvertToCString(server.GetCertificateKeyPassword()),
+      server.IsPkcs11Enabled() ? 1 : 0);
+
+    if (code != OrthancPluginErrorCode_Success ||
+        (status < 200 || status >= 300))
+    {
+      OrthancPlugins::Configuration::LogError("Cannot issue an HTTP query to " + url + 
+                                              " (HTTP status: " + boost::lexical_cast<std::string>(status) + ")");
+      throw PluginException(code);
+    }
+
+    Json::Value json;
+    answerHeadersTmp.ToJson(json);
+    answerHeadersTmp.Clear();
+
+    if (json.type() != Json::objectValue)
+    {
+      throw PluginException(OrthancPluginErrorCode_InternalError);
+    }
+
+    Json::Value::Members members = json.getMemberNames();
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const std::string& key = members[i];
+
+      if (json[key].type() != Json::stringValue)
+      {
+        throw PluginException(OrthancPluginErrorCode_InternalError);
+      }
+      else
+      {
+        answerHeaders[key] = json[key].asString();        
+      }
+    }
+  }
+
+
+  void UriEncode(std::string& uri,
+                 const std::string& resource,
+                 const std::map<std::string, std::string>& getArguments)
+  {
+    if (resource.find('?') != std::string::npos)
+    {
+      OrthancPlugins::Configuration::LogError("The GET arguments must be provided in a separate field "
+                                              "(explicit \"?\" is disallowed): " + resource);
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat);
+    }
+
+    uri = resource;
+
+    bool isFirst = true;
+    for (std::map<std::string, std::string>::const_iterator
+           it = getArguments.begin(); it != getArguments.end(); ++it)
+    {
+      if (isFirst)
+      {
+        uri += '?';
+        isFirst = false;
+      }
+      else
+      {
+        uri += '&';
+      }
+
+      std::string key, value;
+      Orthanc::Toolbox::UriEncode(key, it->first);
+      Orthanc::Toolbox::UriEncode(value, it->second);
+
+      if (value.empty())
+      {
+        uri += key;
+      }
+      else
+      {
+        uri += key + "=" + value;
+      }
+    }
+  }
+}
diff --git a/Plugin/DicomWebServers.h b/Plugin/DicomWebServers.h
new file mode 100644
index 0000000..df3aded
--- /dev/null
+++ b/Plugin/DicomWebServers.h
@@ -0,0 +1,73 @@
+/**
+ * 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 "../Orthanc/Core/WebServiceParameters.h"
+#include "../Orthanc/Plugins/Samples/Common/OrthancPluginCppWrapper.h"
+
+#include <list>
+#include <string>
+#include <boost/thread/mutex.hpp>
+#include <json/value.h>
+
+namespace OrthancPlugins
+{
+  class DicomWebServers
+  {
+  private:
+    typedef std::map<std::string, Orthanc::WebServiceParameters*>  Servers;
+
+    boost::mutex  mutex_;
+    Servers       servers_;
+
+    void Clear();
+
+    DicomWebServers()  // Forbidden (singleton pattern)
+    {
+    }
+
+  public:
+    void Load(const Json::Value& configuration);
+
+    ~DicomWebServers()
+    {
+      Clear();
+    }
+
+    static DicomWebServers& GetInstance();
+
+    Orthanc::WebServiceParameters GetServer(const std::string& name);
+
+    void ListServers(std::list<std::string>& servers);
+  };
+
+
+  void CallServer(OrthancPlugins::MemoryBuffer& answerBody /* out */,
+                  std::map<std::string, std::string>& answerHeaders /* out */,
+                  const Orthanc::WebServiceParameters& server,
+                  OrthancPluginHttpMethod method,
+                  const std::map<std::string, std::string>& httpHeaders,
+                  const std::string& uri,
+                  const std::string& body);
+
+  void UriEncode(std::string& uri,
+                 const std::string& resource,
+                 const std::map<std::string, std::string>& getArguments);
+}
diff --git a/Plugin/Plugin.cpp b/Plugin/Plugin.cpp
index a4ac203..0bf076e 100644
--- a/Plugin/Plugin.cpp
+++ b/Plugin/Plugin.cpp
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -22,10 +22,14 @@
 
 #include "QidoRs.h"
 #include "StowRs.h"
+#include "DicomWebClient.h"
 #include "WadoRs.h"
-#include "Wado.h"
+#include "WadoUri.h"
 #include "Configuration.h"
+#include "DicomWebServers.h"
 
+#include "../Orthanc/Plugins/Samples/Common/OrthancPluginCppWrapper.h"
+#include "../Orthanc/Core/Toolbox.h"
 
 #include <gdcmDictEntry.h>
 #include <gdcmDict.h>
@@ -34,47 +38,8 @@
 
 
 // Global state
-OrthancPluginContext* context_ = NULL;
-Json::Value configuration_;
 const gdcm::Dict* dictionary_ = NULL;
 
-#include "../Orthanc/Core/OrthancException.h"
-#include <boost/lexical_cast.hpp>
-
-
-typedef void (*RestCallback) (OrthancPluginRestOutput* output,
-                              const char* url,
-                              const OrthancPluginHttpRequest* request);
-
-
-template <RestCallback Callback>
-OrthancPluginErrorCode Protect(OrthancPluginRestOutput* output,
-                               const char* url,
-                               const OrthancPluginHttpRequest* request)
-{
-  try
-  {
-    Callback(output, url, request);
-    return OrthancPluginErrorCode_Success;
-  }
-  catch (Orthanc::OrthancException& e)
-  {
-    OrthancPluginLogError(context_, e.What());
-    return OrthancPluginErrorCode_Plugin;
-  }
-  catch (boost::bad_lexical_cast& e)
-  {
-    OrthancPluginLogError(context_, e.what());
-    return OrthancPluginErrorCode_Plugin;
-  }
-  catch (std::runtime_error& e)
-  {
-    OrthancPluginLogError(context_, e.what());
-    return OrthancPluginErrorCode_Plugin;
-  }
-}
-
-
 
 void SwitchStudies(OrthancPluginRestOutput* output,
                    const char* url,
@@ -93,7 +58,7 @@ void SwitchStudies(OrthancPluginRestOutput* output,
       break;
 
     default:
-      OrthancPluginSendMethodNotAllowed(context_, output, "GET,POST");
+      OrthancPluginSendMethodNotAllowed(OrthancPlugins::Configuration::GetContext(), output, "GET,POST");
       break;
   }
 }
@@ -116,117 +81,156 @@ void SwitchStudy(OrthancPluginRestOutput* output,
       break;
 
     default:
-      OrthancPluginSendMethodNotAllowed(context_, output, "GET,POST");
+      OrthancPluginSendMethodNotAllowed(OrthancPlugins::Configuration::GetContext(), output, "GET,POST");
       break;
   }
 }
 
 
-static void Register(const std::string& root,
-                     const std::string& uri,
-                     OrthancPluginRestCallback callback)
+void ListServers(OrthancPluginRestOutput* output,
+                 const char* url,
+                 const OrthancPluginHttpRequest* request)
 {
-  assert(!uri.empty() && uri[0] != '/');
-  std::string s = root + uri;
-  OrthancPluginRegisterRestCallback(context_, s.c_str(), callback);
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+  }
+  else
+  {
+    std::list<std::string> servers;
+    OrthancPlugins::DicomWebServers::GetInstance().ListServers(servers);
+
+    Json::Value json = Json::arrayValue;
+    for (std::list<std::string>::const_iterator it = servers.begin(); it != servers.end(); ++it)
+    {
+      json.append(*it);
+    }
+
+    std::string answer = json.toStyledString(); 
+    OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json");
+  }
 }
 
 
+void ListServerOperations(OrthancPluginRestOutput* output,
+                          const char* /*url*/,
+                          const OrthancPluginHttpRequest* request)
+{
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
+  }
+  else
+  {
+    // Make sure the server does exist
+    OrthancPlugins::DicomWebServers::GetInstance().GetServer(request->groups[0]);
+
+    Json::Value json = Json::arrayValue;
+    json.append("get");
+    json.append("retrieve");
+    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)
   {
-    context_ = context;
-
     /* Check the version of the Orthanc core */
-    if (OrthancPluginCheckVersion(context_) == 0)
+    if (OrthancPluginCheckVersion(context) == 0)
     {
       char info[1024];
       sprintf(info, "Your version of Orthanc (%s) must be above %d.%d.%d to run this plugin",
-              context_->orthancVersion,
+              context->orthancVersion,
               ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER,
               ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER,
               ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER);
-      OrthancPluginLogError(context_, info);
+      OrthancPluginLogError(context, info);
       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 DICOMweb (QIDO-RS, STOW-RS and WADO-RS) and WADO-URI.");
 
+    try
+    {
+      // Read the configuration
+      OrthancPlugins::Configuration::Initialize(context);
 
-    OrthancPluginSetDescription(context_, "Implementation of DICOM Web (QIDO-RS, STOW-RS and WADO-RS) and WADO.");
-
-    // Read the configuration
-    dictionary_ = &gdcm::Global::GetInstance().GetDicts().GetPublicDict();
-
-    configuration_ = Json::objectValue;
+      // Initialize GDCM
+      dictionary_ = &gdcm::Global::GetInstance().GetDicts().GetPublicDict();
 
-    {
-      Json::Value tmp;
-      if (!OrthancPlugins::Configuration::Read(tmp, context) ||
-          tmp.type() != Json::objectValue)
+      // Configure the DICOMweb callbacks
+      if (OrthancPlugins::Configuration::GetBooleanValue("Enable", true))
       {
-        OrthancPluginLogError(context_, "Unable to read the configuration file");
-        return -1;
+        std::string root = OrthancPlugins::Configuration::GetRoot();
+        assert(!root.empty() && root[root.size() - 1] == '/');
+
+        OrthancPlugins::Configuration::LogWarning("URI to the DICOMweb REST API: " + root);
+
+        OrthancPlugins::RegisterRestCallback<SearchForInstances>(context, root + "instances", true);
+        OrthancPlugins::RegisterRestCallback<SearchForSeries>(context, root + "series", true);    
+        OrthancPlugins::RegisterRestCallback<SwitchStudies>(context, root + "studies", true);
+        OrthancPlugins::RegisterRestCallback<SwitchStudy>(context, root + "studies/([^/]*)", true);
+        OrthancPlugins::RegisterRestCallback<SearchForInstances>(context, root + "studies/([^/]*)/instances", true);    
+        OrthancPlugins::RegisterRestCallback<RetrieveStudyMetadata>(context, root + "studies/([^/]*)/metadata", true);
+        OrthancPlugins::RegisterRestCallback<SearchForSeries>(context, root + "studies/([^/]*)/series", true);    
+        OrthancPlugins::RegisterRestCallback<RetrieveDicomSeries>(context, root + "studies/([^/]*)/series/([^/]*)", true);
+        OrthancPlugins::RegisterRestCallback<SearchForInstances>(context, root + "studies/([^/]*)/series/([^/]*)/instances", true);    
+        OrthancPlugins::RegisterRestCallback<RetrieveDicomInstance>(context, root + "studies/([^/]*)/series/([^/]*)/instances/([^/]*)", true);
+        OrthancPlugins::RegisterRestCallback<RetrieveBulkData>(context, root + "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/bulk/(.*)", true);
+        OrthancPlugins::RegisterRestCallback<RetrieveInstanceMetadata>(context, root + "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/metadata", true);
+        OrthancPlugins::RegisterRestCallback<RetrieveSeriesMetadata>(context, root + "studies/([^/]*)/series/([^/]*)/metadata", true);
+        OrthancPlugins::RegisterRestCallback<RetrieveFrames>(context, root + "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/frames", true);
+        OrthancPlugins::RegisterRestCallback<RetrieveFrames>(context, root + "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/frames/([^/]*)", true);
+
+        OrthancPlugins::RegisterRestCallback<ListServers>(context, root + "servers", true);
+        OrthancPlugins::RegisterRestCallback<ListServerOperations>(context, root + "servers/([^/]*)", true);
+        OrthancPlugins::RegisterRestCallback<StowClient>(context, root + "servers/([^/]*)/stow", true);
+        OrthancPlugins::RegisterRestCallback<GetFromServer>(context, root + "servers/([^/]*)/get", true);
+        OrthancPlugins::RegisterRestCallback<RetrieveFromServer>(context, root + "servers/([^/]*)/retrieve", true);
       }
-
-      if (tmp.isMember("DicomWeb") &&
-          tmp["DicomWeb"].type() == Json::objectValue)
+      else
       {
-        configuration_ = tmp["DicomWeb"];
+        OrthancPlugins::Configuration::LogWarning("DICOMweb support is disabled");
       }
-    }
 
-    // Configure the DICOMweb callbacks
-    if (OrthancPlugins::Configuration::GetBoolValue(configuration_, "Enable", true))
-    {
-      std::string root = OrthancPlugins::Configuration::GetRoot(configuration_);
-
-      std::string message = "URI to the DICOMweb REST API: " + root;
-      OrthancPluginLogWarning(context_, message.c_str());
-
-      Register(root, "instances", Protect<SearchForInstances>);
-      Register(root, "series", Protect<SearchForSeries>);    
-      Register(root, "studies", Protect<SwitchStudies>);
-      Register(root, "studies/([^/]*)", Protect<SwitchStudy>);
-      Register(root, "studies/([^/]*)/instances", Protect<SearchForInstances>);    
-      Register(root, "studies/([^/]*)/metadata", Protect<RetrieveStudyMetadata>);
-      Register(root, "studies/([^/]*)/series", Protect<SearchForSeries>);    
-      Register(root, "studies/([^/]*)/series/([^/]*)", Protect<RetrieveDicomSeries>);
-      Register(root, "studies/([^/]*)/series/([^/]*)/instances", Protect<SearchForInstances>);    
-      Register(root, "studies/([^/]*)/series/([^/]*)/instances/([^/]*)", Protect<RetrieveDicomInstance>);
-      Register(root, "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/bulk/(.*)", Protect<RetrieveBulkData>);
-      Register(root, "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/metadata", Protect<RetrieveInstanceMetadata>);
-      Register(root, "studies/([^/]*)/series/([^/]*)/metadata", Protect<RetrieveSeriesMetadata>);
-      Register(root, "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/frames", Protect<RetrieveFrames>);
-      Register(root, "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/frames/([^/]*)", Protect<RetrieveFrames>);
+      // Configure the WADO callback
+      if (OrthancPlugins::Configuration::GetBooleanValue("EnableWado", true))
+      {
+        std::string wado = OrthancPlugins::Configuration::GetWadoRoot();
+        OrthancPlugins::Configuration::LogWarning("URI to the WADO-URI API: " + wado);
+
+        OrthancPlugins::RegisterRestCallback<WadoUriCallback>(context, wado, true);
+      }
+      else
+      {
+        OrthancPlugins::Configuration::LogWarning("WADO-URI support is disabled");
+      }
     }
-    else
+    catch (OrthancPlugins::PluginException& e)
     {
-      OrthancPluginLogWarning(context_, "DICOMweb support is disabled");
+      OrthancPlugins::Configuration::LogError("Exception while initializing the DICOMweb plugin: " + 
+                                              std::string(e.GetErrorDescription(context)));
+      return -1;
     }
-
-    // Configure the WADO callback
-    if (OrthancPlugins::Configuration::GetBoolValue(configuration_, "EnableWado", true))
+    catch (Orthanc::OrthancException& e)
     {
-      std::string wado = OrthancPlugins::Configuration::GetWadoRoot(configuration_);
-
-      std::string message = "URI to the WADO API: " + wado;
-      OrthancPluginLogWarning(context_, message.c_str());
-
-      OrthancPluginRegisterRestCallback(context_, wado.c_str(), Protect<WadoCallback>);
+      OrthancPlugins::Configuration::LogError("Exception while initializing the DICOMweb plugin: " + 
+                                              std::string(e.What()));
+      return -1;
     }
-    else
+    catch (...)
     {
-      OrthancPluginLogWarning(context_, "WADO support is disabled");
+      OrthancPlugins::Configuration::LogError("Exception while initializing the DICOMweb plugin");
+      return -1;
     }
 
     return 0;
diff --git a/Plugin/Plugin.h b/Plugin/Plugin.h
index 1516966..e3a7721 100644
--- a/Plugin/Plugin.h
+++ b/Plugin/Plugin.h
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -20,11 +20,11 @@
 
 #pragma once
 
-#include <orthanc/OrthancCPlugin.h>
-#include <json/value.h>
+#include "../Orthanc/Plugins/Samples/Common/OrthancPluginCppWrapper.h"
+
 #include <gdcmDict.h>
 
+// TODO Remove this file
+
 // Global state
-extern OrthancPluginContext* context_;
-extern Json::Value configuration_;
 extern const gdcm::Dict* dictionary_;
diff --git a/Plugin/QidoRs.cpp b/Plugin/QidoRs.cpp
index 55ef655..7a69482 100644
--- a/Plugin/QidoRs.cpp
+++ b/Plugin/QidoRs.cpp
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -26,7 +26,6 @@
 #include "DicomResults.h"
 #include "Configuration.h"
 #include "../Orthanc/Core/Toolbox.h"
-#include "../Orthanc/Core/OrthancException.h"
 
 #include <gdcmTag.h>
 #include <list>
@@ -42,6 +41,35 @@
 
 namespace
 {
+  static std::string FormatOrthancTag(const gdcm::Tag& tag)
+  {
+    char b[16];
+    sprintf(b, "%04x,%04x", tag.GetGroup(), tag.GetElement());
+    return std::string(b);
+  }
+
+
+  static std::string GetOrthancTag(const Json::Value& source,
+                                   const gdcm::Tag& tag,
+                                   const std::string& defaultValue)
+  {
+    std::string s = FormatOrthancTag(tag);
+      
+    if (source.isMember(s) &&
+        source[s].type() == Json::objectValue &&
+        source[s].isMember("Value") &&
+        source[s].isMember("Type") &&
+        source[s]["Type"] == "String" &&
+        source[s]["Value"].type() == Json::stringValue)
+    {
+      return source[s]["Value"].asString();
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
 
   enum QueryLevel
   {
@@ -53,10 +81,10 @@ namespace
 
   class ModuleMatcher
   {
-  private:
+  public:
     typedef std::map<gdcm::Tag, std::string>  Filters;
 
-    const gdcm::Dict&     dictionary_;
+  private:
     bool                  fuzzy_;
     unsigned int          offset_;
     unsigned int          limit_;
@@ -65,84 +93,6 @@ namespace
     Filters               filters_;
 
 
-
-    static inline uint16_t GetCharValue(char c)
-    {
-      if (c >= '0' && c <= '9')
-        return c - '0';
-      else if (c >= 'a' && c <= 'f')
-        return c - 'a' + 10;
-      else if (c >= 'A' && c <= 'F')
-        return c - 'A' + 10;
-      else
-        return 0;
-    }
-
-    static inline uint16_t GetTagValue(const char* c)
-    {
-      return ((GetCharValue(c[0]) << 12) + 
-              (GetCharValue(c[1]) << 8) + 
-              (GetCharValue(c[2]) << 4) + 
-              GetCharValue(c[3]));
-    }
-
-
-    static std::string Format(const gdcm::Tag& tag)
-    {
-      char b[16];
-      sprintf(b, "%04x,%04x", tag.GetGroup(), tag.GetElement());
-      return std::string(b);
-    }
-
-
-    gdcm::Tag  ParseTag(const std::string& key) const
-    {
-      if (key.find('.') != std::string::npos)
-      {
-        std::string s = "This DICOMweb plugin does not support hierarchical queries: " + key;
-        OrthancPluginLogError(context_, s.c_str());
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-      }
-
-      if (key.size() == 8 &&
-          isxdigit(key[0]) &&
-          isxdigit(key[1]) &&
-          isxdigit(key[2]) &&
-          isxdigit(key[3]) &&
-          isxdigit(key[4]) &&
-          isxdigit(key[5]) &&
-          isxdigit(key[6]) &&
-          isxdigit(key[7]))        
-      {
-        return gdcm::Tag(GetTagValue(key.c_str()),
-                         GetTagValue(key.c_str() + 4));
-      }
-      else
-      {
-        gdcm::Tag tag;
-        dictionary_.GetDictEntryByKeyword(key.c_str(), tag);
-
-        if (tag.IsIllegal() || tag.IsPrivate())
-        {
-          if (key.find('.') != std::string::npos)
-          {
-            std::string s = "This QIDO-RS implementation does not support search over sequences: " + key;
-            OrthancPluginLogError(context_, s.c_str());
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-          }
-          else
-          {
-            std::string s = "Illegal tag name in QIDO-RS: " + key;
-            OrthancPluginLogError(context_, s.c_str());
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownDicomTag);
-          }
-        }
-
-        return tag;
-      }
-    }
-
-
     static void AddResultAttributesForLevel(std::list<gdcm::Tag>& result,
                                             QueryLevel level)
     {
@@ -155,7 +105,7 @@ namespace
           result.push_back(gdcm::Tag(0x0008, 0x0030));  // Study Time
           result.push_back(gdcm::Tag(0x0008, 0x0050));  // Accession Number
           result.push_back(gdcm::Tag(0x0008, 0x0056));  // Instance Availability
-          result.push_back(gdcm::Tag(0x0008, 0x0061));  // Modalities in Study
+          //result.push_back(gdcm::Tag(0x0008, 0x0061));  // Modalities in Study  => SPECIAL CASE
           result.push_back(gdcm::Tag(0x0008, 0x0090));  // Referring Physician's Name
           result.push_back(gdcm::Tag(0x0008, 0x0201));  // Timezone Offset From UTC
           //result.push_back(gdcm::Tag(0x0008, 0x1190));  // Retrieve URL  => SPECIAL CASE
@@ -165,20 +115,20 @@ namespace
           result.push_back(gdcm::Tag(0x0010, 0x0040));  // Patient's Sex
           result.push_back(gdcm::Tag(0x0020, 0x000D));  // Study Instance UID
           result.push_back(gdcm::Tag(0x0020, 0x0010));  // Study ID
-          result.push_back(gdcm::Tag(0x0020, 0x1206));  // Number of Study Related Series
-          result.push_back(gdcm::Tag(0x0020, 0x1208));  // Number of Study Related Instances
+          //result.push_back(gdcm::Tag(0x0020, 0x1206));  // Number of Study Related Series  => SPECIAL CASE
+          //result.push_back(gdcm::Tag(0x0020, 0x1208));  // Number of Study Related Instances  => SPECIAL CASE
           break;
 
         case QueryLevel_Series:
           // http://medical.nema.org/medical/dicom/current/output/html/part18.html#table_6.7.1-2a
           result.push_back(gdcm::Tag(0x0008, 0x0005));  // Specific Character Set
-          result.push_back(gdcm::Tag(0x0008, 0x0056));  // Modality
+          result.push_back(gdcm::Tag(0x0008, 0x0060));  // Modality
           result.push_back(gdcm::Tag(0x0008, 0x0201));  // Timezone Offset From UTC
           result.push_back(gdcm::Tag(0x0008, 0x103E));  // Series Description
           //result.push_back(gdcm::Tag(0x0008, 0x1190));  // Retrieve URL  => SPECIAL CASE
           result.push_back(gdcm::Tag(0x0020, 0x000E));  // Series Instance UID
           result.push_back(gdcm::Tag(0x0020, 0x0011));  // Series Number
-          result.push_back(gdcm::Tag(0x0020, 0x1209));  // Number of Series Related Instances
+          //result.push_back(gdcm::Tag(0x0020, 0x1209));  // Number of Series Related Instances  => SPECIAL CASE
           result.push_back(gdcm::Tag(0x0040, 0x0244));  // Performed Procedure Step Start Date
           result.push_back(gdcm::Tag(0x0040, 0x0245));  // Performed Procedure Step Start Time
           result.push_back(gdcm::Tag(0x0040, 0x0275));  // Request Attribute Sequence
@@ -200,15 +150,13 @@ namespace
           break;
 
         default:
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError);
       }
     }
 
 
-
   public:
     ModuleMatcher(const OrthancPluginHttpRequest* request) :
-    dictionary_(gdcm::Global::GetInstance().GetDicts().GetPublicDict()),
     fuzzy_(false),
     offset_(0),
     limit_(0),
@@ -239,9 +187,8 @@ namespace
           }
           else
           {
-            std::string s = "Not a proper value for fuzzy matching (true or false): " + value;
-            OrthancPluginLogError(context_, s.c_str());
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest);
+            OrthancPlugins::Configuration::LogError("Not a proper value for fuzzy matching (true or false): " + value);
+            throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadRequest);
           }
         }
         else if (key == "includefield")
@@ -257,13 +204,13 @@ namespace
             Orthanc::Toolbox::TokenizeString(tags, value, ',');
             for (size_t i = 0; i < tags.size(); i++)
             {
-              includeFields_.push_back(ParseTag(tags[i]));
+              includeFields_.push_back(OrthancPlugins::ParseTag(*dictionary_, tags[i]));
             }
           }
         }
         else
         {
-          filters_[ParseTag(key)] = value;
+          filters_[OrthancPlugins::ParseTag(*dictionary_, key)] = value;
         }
       }
     }
@@ -313,7 +260,7 @@ namespace
           break;
 
         default:
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError);
       }
 
       result["Expand"] = false;
@@ -323,11 +270,90 @@ namespace
       for (Filters::const_iterator it = filters_.begin(); 
            it != filters_.end(); ++it)
       {
-        result["Query"][Format(it->first)] = it->second;
+        result["Query"][FormatOrthancTag(it->first)] = it->second;
       }
     }
 
 
+    void ComputeDerivedTags(Filters& target,
+                            QueryLevel level,
+                            const std::string& resource) const
+    {
+      OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
+      target.clear();
+
+      switch (level)
+      {
+        case QueryLevel_Study:
+        {
+          Json::Value series, instances;
+          if (OrthancPlugins::RestApiGetJson(series, context, "/studies/" + resource + "/series?expand", false) &&
+              OrthancPlugins::RestApiGetJson(instances, context, "/studies/" + resource + "/instances", false))
+          {
+            // Number of Study Related Series
+            target[gdcm::Tag(0x0020, 0x1206)] = boost::lexical_cast<std::string>(series.size());
+
+            // Number of Study Related Instances
+            target[gdcm::Tag(0x0020, 0x1208)] = boost::lexical_cast<std::string>(instances.size());
+
+            // Collect the Modality of all the child series
+            std::set<std::string> modalities;
+            for (Json::Value::ArrayIndex i = 0; i < series.size(); i++)
+            {
+              if (series[i].isMember("MainDicomTags") &&
+                  series[i]["MainDicomTags"].isMember("Modality"))
+              {
+                modalities.insert(series[i]["MainDicomTags"]["Modality"].asString());
+              }
+            }
+
+            std::string s;
+            for (std::set<std::string>::const_iterator 
+                   it = modalities.begin(); it != modalities.end(); ++it)
+            {
+              if (!s.empty())
+              {
+                s += "\\";
+              }
+
+              s += *it;
+            }
+
+            target[gdcm::Tag(0x0008, 0x0061)] = s;  // Modalities in Study
+          }
+          else
+          {
+            target[gdcm::Tag(0x0008, 0x0061)] = "";   // Modalities in Study
+            target[gdcm::Tag(0x0020, 0x1206)] = "0";  // Number of Study Related Series
+            target[gdcm::Tag(0x0020, 0x1208)] = "0";  // Number of Study Related Instances
+          }
+
+          break;
+        }
+
+        case QueryLevel_Series:
+        {
+          Json::Value instances;
+          if (OrthancPlugins::RestApiGetJson(instances, context, "/series/" + resource + "/instances", false))
+          {
+            // Number of Series Related Instances
+            target[gdcm::Tag(0x0020, 0x1209)] = boost::lexical_cast<std::string>(instances.size());
+          }
+          else
+          {
+            target[gdcm::Tag(0x0020, 0x1209)] = "0";  // Number of Series Related Instances
+          }
+
+          break;
+        }
+
+        default:
+          break;
+      }
+    }                              
+
+
     void ExtractFields(gdcm::DataSet& result,
                        const OrthancPlugins::ParsedDicomFile& dicom,
                        const std::string& wadoBase,
@@ -365,7 +391,7 @@ namespace
 
       // Copy all the required fields to the target
       for (std::list<gdcm::Tag>::const_iterator
-             it = fields.begin(); it != fields.end(); it++)
+             it = fields.begin(); it != fields.end(); ++it)
       {
         if (dicom.GetDataSet().FindDataElement(*it))
         {
@@ -392,6 +418,76 @@ namespace
       element.SetByteValue(url.c_str(), url.size());
       result.Replace(element);
     }
+
+
+    void ExtractFields(Json::Value& result,
+                       const Json::Value& source,
+                       const std::string& wadoBase,
+                       QueryLevel level) const
+    {
+      result = Json::objectValue;
+      std::list<gdcm::Tag> fields = includeFields_;
+
+      // The list of attributes for this query level
+      AddResultAttributesForLevel(fields, level);
+
+      // All other attributes passed as query keys
+      for (Filters::const_iterator it = filters_.begin();
+           it != filters_.end(); ++it)
+      {
+        fields.push_back(it->first);
+      }
+
+      // For instances and series, add all Study-level attributes if
+      // {StudyInstanceUID} is not specified.
+      if ((level == QueryLevel_Instance  || level == QueryLevel_Series) 
+          && filters_.find(OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID) == filters_.end()
+        )
+      {
+        AddResultAttributesForLevel(fields, QueryLevel_Study);
+      }
+
+      // For instances, add all Series-level attributes if
+      // {SeriesInstanceUID} is not specified.
+      if (level == QueryLevel_Instance
+          && filters_.find(OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID) == filters_.end()
+        )
+      {
+        AddResultAttributesForLevel(fields, QueryLevel_Series);
+      }
+
+      // Copy all the required fields to the target
+      for (std::list<gdcm::Tag>::const_iterator
+             it = fields.begin(); it != fields.end(); ++it)
+      {
+        std::string tag = FormatOrthancTag(*it);
+        if (source.isMember(tag))
+        {
+          result[tag] = source[tag];
+        }
+      }
+
+      // Set the retrieve URL for WADO-RS
+      std::string url = (wadoBase + "studies/" + 
+                         GetOrthancTag(source, OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID, ""));
+
+      if (level == QueryLevel_Series || level == QueryLevel_Instance)
+      {
+        url += "/series/" + GetOrthancTag(source, OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID, "");
+      }
+
+      if (level == QueryLevel_Instance)
+      {
+        url += "/instances/" + GetOrthancTag(source, OrthancPlugins::DICOM_TAG_SOP_INSTANCE_UID, "");
+      }
+    
+      Json::Value tmp = Json::objectValue;
+      tmp["Name"] = "RetrieveURL";
+      tmp["Type"] = "String";
+      tmp["Value"] = url;
+
+      result[FormatOrthancTag(OrthancPlugins::DICOM_TAG_RETRIEVE_URL)] = tmp;
+    }
   };
 }
 
@@ -402,6 +498,8 @@ static void ApplyMatcher(OrthancPluginRestOutput* output,
                          const ModuleMatcher& matcher,
                          QueryLevel level)
 {
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
   Json::Value find;
   matcher.ConvertToOrthanc(find, level);
 
@@ -409,53 +507,114 @@ static void ApplyMatcher(OrthancPluginRestOutput* output,
   std::string body = writer.write(find);
   
   Json::Value resources;
-  if (!OrthancPlugins::RestApiPostJson(resources, context_, "/tools/find", body) ||
+  if (!OrthancPlugins::RestApiPostJson(resources, context, "/tools/find", body, false) ||
       resources.type() != Json::arrayValue)
   {
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError);
   }
 
-  std::list<std::string> instances;
+  typedef std::list< std::pair<std::string, std::string> > ResourcesAndInstances;
+
+  ResourcesAndInstances resourcesAndInstances;
   std::string root = (level == QueryLevel_Study ? "/studies/" : "/series/");
     
   for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++)
   {
+    const std::string resource = resources[i].asString();
+
     if (level == QueryLevel_Study ||
         level == QueryLevel_Series)
     {
       // Find one child instance of this resource
       Json::Value tmp;
-      if (OrthancPlugins::RestApiGetJson(tmp, context_, root + resources[i].asString() + "/instances") &&
+      if (OrthancPlugins::RestApiGetJson(tmp, context, root + resource + "/instances", false) &&
           tmp.type() == Json::arrayValue &&
           tmp.size() > 0)
       {
-        instances.push_back(tmp[0]["ID"].asString());
+        resourcesAndInstances.push_back(std::make_pair(resource, tmp[0]["ID"].asString()));
       }
     }
     else
     {
-      instances.push_back(resources[i].asString());
+      resourcesAndInstances.push_back(std::make_pair(resource, resource));
     }
   }
   
-  std::string wadoBase = OrthancPlugins::Configuration::GetBaseUrl(configuration_, request);
+  std::string wadoBase = OrthancPlugins::Configuration::GetBaseUrl(request);
 
-  OrthancPlugins::DicomResults results(context_, output, wadoBase, *dictionary_, IsXmlExpected(request), true);
+  OrthancPlugins::DicomResults results(context, output, wadoBase, *dictionary_, IsXmlExpected(request), true);
 
-  for (std::list<std::string>::const_iterator
-         it = instances.begin(); it != instances.end(); it++)
+#if 0
+  // Implementation up to version 0.2 of the plugin. Each instance is
+  // downloaded and decoded using GDCM, which slows down things
+  // wrt. the new implementation below that directly uses the Orthanc
+  // pre-computed JSON summary.
+  for (ResourcesAndInstances::const_iterator
+         it = resourcesAndInstances.begin(); it != resourcesAndInstances.end(); ++it)
   {
+    ModuleMatcher::Filters derivedTags;
+    matcher.ComputeDerivedTags(derivedTags, level, it->first);
+
     std::string file;
-    if (OrthancPlugins::RestApiGetString(file, context_, "/instances/" + *it + "/file"))
+    if (OrthancPlugins::RestApiGetString(file, context, "/instances/" + it->second + "/file", false))
     {
       OrthancPlugins::ParsedDicomFile dicom(file);
 
       std::auto_ptr<gdcm::DataSet> result(new gdcm::DataSet);
       matcher.ExtractFields(*result, dicom, wadoBase, level);
+
+      // Inject the derived tags
+      ModuleMatcher::Filters derivedTags;
+      matcher.ComputeDerivedTags(derivedTags, level, it->first);
+
+      for (ModuleMatcher::Filters::const_iterator
+             tag = derivedTags.begin(); tag != derivedTags.end(); ++tag)
+      {
+        gdcm::DataElement element(tag->first);
+        element.SetByteValue(tag->second.c_str(), tag->second.size());
+        result->Replace(element);
+      }
+
       results.Add(dicom.GetFile(), *result);
     }
   }
 
+#else
+  // Fix of issue #13
+  for (ResourcesAndInstances::const_iterator
+         it = resourcesAndInstances.begin(); it != resourcesAndInstances.end(); ++it)
+  {
+    Json::Value tags;
+    if (OrthancPlugins::RestApiGetJson(tags, context, "/instances/" + it->second + "/tags", false))
+    {
+      std::string wadoUrl = OrthancPlugins::Configuration::GetWadoUrl(
+        wadoBase, 
+        GetOrthancTag(tags, OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID, ""),
+        GetOrthancTag(tags, OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID, ""),
+        GetOrthancTag(tags, OrthancPlugins::DICOM_TAG_SOP_INSTANCE_UID, ""));
+
+      Json::Value result;
+      matcher.ExtractFields(result, tags, wadoBase, level);
+
+      // Inject the derived tags
+      ModuleMatcher::Filters derivedTags;
+      matcher.ComputeDerivedTags(derivedTags, level, it->first);
+
+      for (ModuleMatcher::Filters::const_iterator
+             tag = derivedTags.begin(); tag != derivedTags.end(); ++tag)
+      {
+        Json::Value tmp = Json::objectValue;
+        tmp["Name"] = OrthancPlugins::GetKeyword(*dictionary_, tag->first);
+        tmp["Type"] = "String";
+        tmp["Value"] = tag->second;
+        result[FormatOrthancTag(tag->first)] = tmp;
+      }
+
+      results.AddFromOrthanc(result, wadoUrl);
+    }
+  }
+#endif
+
   results.Answer();
 }
 
@@ -467,7 +626,7 @@ void SearchForStudies(OrthancPluginRestOutput* output,
 {
   if (request->method != OrthancPluginHttpMethod_Get)
   {
-    OrthancPluginSendMethodNotAllowed(context_, output, "GET");
+    OrthancPluginSendMethodNotAllowed(OrthancPlugins::Configuration::GetContext(), output, "GET");
   }
   else
   {
@@ -483,7 +642,7 @@ void SearchForSeries(OrthancPluginRestOutput* output,
 {
   if (request->method != OrthancPluginHttpMethod_Get)
   {
-    OrthancPluginSendMethodNotAllowed(context_, output, "GET");
+    OrthancPluginSendMethodNotAllowed(OrthancPlugins::Configuration::GetContext(), output, "GET");
   }
   else
   {
@@ -506,7 +665,7 @@ void SearchForInstances(OrthancPluginRestOutput* output,
 {
   if (request->method != OrthancPluginHttpMethod_Get)
   {
-    OrthancPluginSendMethodNotAllowed(context_, output, "GET");
+    OrthancPluginSendMethodNotAllowed(OrthancPlugins::Configuration::GetContext(), output, "GET");
   }
   else
   {
diff --git a/Plugin/QidoRs.h b/Plugin/QidoRs.h
index 5e63d86..77a5c80 100644
--- a/Plugin/QidoRs.h
+++ b/Plugin/QidoRs.h
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
diff --git a/Plugin/StowRs.cpp b/Plugin/StowRs.cpp
index d808758..a8b1c55 100644
--- a/Plugin/StowRs.cpp
+++ b/Plugin/StowRs.cpp
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -24,7 +24,6 @@
 #include "Configuration.h"
 #include "Dicom.h"
 #include "../Orthanc/Core/Toolbox.h"
-#include "../Orthanc/Core/OrthancException.h"
 
 #include <stdexcept>
 
@@ -75,8 +74,7 @@ bool IsXmlExpected(const OrthancPluginHttpRequest* request)
       accept != "text/xml" &&
       accept != "*/*")
   {
-    std::string s = "Unsupported return MIME type: " + accept + ", will return XML";
-    OrthancPluginLogError(context_, s.c_str());
+    OrthancPlugins::Configuration::LogError("Unsupported return MIME type: " + accept + ", will return XML");
   }
 
   return true;
@@ -88,11 +86,13 @@ void StowCallback(OrthancPluginRestOutput* output,
                   const char* url,
                   const OrthancPluginHttpRequest* request)
 {
-  const std::string wadoBase = OrthancPlugins::Configuration::GetBaseUrl(configuration_, request);
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
+  const std::string wadoBase = OrthancPlugins::Configuration::GetBaseUrl(request);
 
   if (request->method != OrthancPluginHttpMethod_Post)
   {
-    OrthancPluginSendMethodNotAllowed(context_, output, "POST");
+    OrthancPluginSendMethodNotAllowed(context, output, "POST");
     return;
   }
 
@@ -104,12 +104,11 @@ void StowCallback(OrthancPluginRestOutput* output,
 
   if (expectedStudy.empty())
   {
-    OrthancPluginLogInfo(context_, "STOW-RS request without study");
+    OrthancPlugins::Configuration::LogInfo("STOW-RS request without study");
   }
   else
   {
-    std::string s = "STOW-RS request restricted to study UID " + expectedStudy;
-    OrthancPluginLogInfo(context_, s.c_str());
+    OrthancPlugins::Configuration::LogInfo("STOW-RS request restricted to study UID " + expectedStudy);
   }
 
   bool isXml = IsXmlExpected(request);
@@ -117,8 +116,8 @@ void StowCallback(OrthancPluginRestOutput* output,
   std::string header;
   if (!OrthancPlugins::LookupHttpHeader(header, request, "content-type"))
   {
-    OrthancPluginLogError(context_, "No content type in the HTTP header of a STOW-RS request");
-    OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */);
+    OrthancPlugins::Configuration::LogError("No content type in the HTTP header of a STOW-RS request");
+    OrthancPluginSendHttpStatusCode(context, output, 400 /* Bad request */);
     return;
   }
 
@@ -130,9 +129,8 @@ void StowCallback(OrthancPluginRestOutput* output,
       attributes.find("type") == attributes.end() ||
       attributes.find("boundary") == attributes.end())
   {
-    std::string s = "Unable to parse the content type of a STOW-RS request (" + application + ")";
-    OrthancPluginLogError(context_, s.c_str());
-    OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */);
+    OrthancPlugins::Configuration::LogError("Unable to parse the content type of a STOW-RS request (" + application + ")");
+    OrthancPluginSendHttpStatusCode(context, output, 400 /* Bad request */);
     return;
   }
 
@@ -141,28 +139,29 @@ void StowCallback(OrthancPluginRestOutput* output,
 
   if (attributes["type"] != "application/dicom")
   {
-    OrthancPluginLogError(context_, "The STOW-RS plugin currently only supports application/dicom");
-    OrthancPluginSendHttpStatusCode(context_, output, 415 /* Unsupported media type */);
+    OrthancPlugins::Configuration::LogError("The STOW-RS plugin currently only supports application/dicom");
+    OrthancPluginSendHttpStatusCode(context, output, 415 /* Unsupported media type */);
     return;
   }
 
 
-
   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() &&
         items[i].contentType_ != "application/dicom")
     {
-      std::string s = "The STOW-RS request contains a part that is not application/dicom (it is: \"" + items[i].contentType_ + "\")";
-      OrthancPluginLogError(context_, s.c_str());
-      OrthancPluginSendHttpStatusCode(context_, output, 415 /* Unsupported media type */);
+      OrthancPlugins::Configuration::LogError("The STOW-RS request contains a part that is not "
+                                              "\"application/dicom\" (it is: \"" + items[i].contentType_ + "\")");
+      OrthancPluginSendHttpStatusCode(context, output, 415 /* Unsupported media type */);
       return;
     }
 
@@ -182,9 +181,8 @@ void StowCallback(OrthancPluginRestOutput* output,
     if (!expectedStudy.empty() &&
         studyInstanceUid != expectedStudy)
     {
-      std::string s = ("STOW-RS request restricted to study [" + expectedStudy + 
-                       "]: Ignoring instance from study [" + studyInstanceUid + "]");
-      OrthancPluginLogInfo(context_, s.c_str());
+      OrthancPlugins::Configuration::LogInfo("STOW-RS request restricted to study [" + expectedStudy + 
+                                             "]: Ignoring instance from study [" + studyInstanceUid + "]");
 
       SetTag(status, OrthancPlugins::DICOM_TAG_WARNING_REASON, gdcm::VR::US, "B006");  // Elements discarded
       success->AddItem(item);      
@@ -198,9 +196,9 @@ void StowCallback(OrthancPluginRestOutput* output,
         isFirst = false;
       }
 
-      OrthancPluginMemoryBuffer result;
-      bool ok = OrthancPluginRestApiPost(context_, &result, "/instances", items[i].data_, items[i].size_) == 0;
-      OrthancPluginFreeMemoryBuffer(context_, &result);
+      OrthancPlugins::MemoryBuffer tmp(context);
+      bool ok = tmp.RestApiPost("/instances", items[i].data_, items[i].size_, false);
+      tmp.Clear();
 
       if (ok)
       {
@@ -214,7 +212,7 @@ void StowCallback(OrthancPluginRestOutput* output,
       }
       else
       {
-        OrthancPluginLogError(context_, "Orthanc was unable to store instance through STOW-RS request");
+        OrthancPlugins::Configuration::LogError("Orthanc was unable to store instance through STOW-RS request");
         SetTag(status, OrthancPlugins::DICOM_TAG_FAILURE_REASON, gdcm::VR::US, "0110");  // Processing failure
         failed->AddItem(item);
       }
@@ -224,5 +222,5 @@ void StowCallback(OrthancPluginRestOutput* output,
   SetSequenceTag(result, OrthancPlugins::DICOM_TAG_FAILED_SOP_SEQUENCE, failed);
   SetSequenceTag(result, OrthancPlugins::DICOM_TAG_REFERENCED_SOP_SEQUENCE, success);
 
-  OrthancPlugins::AnswerDicom(context_, output, wadoBase, *dictionary_, result, isXml, false);
+  OrthancPlugins::AnswerDicom(context, output, wadoBase, *dictionary_, result, isXml, false);
 }
diff --git a/Plugin/StowRs.h b/Plugin/StowRs.h
index 24d2cc6..9e57321 100644
--- a/Plugin/StowRs.h
+++ b/Plugin/StowRs.h
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
diff --git a/Plugin/WadoRs.cpp b/Plugin/WadoRs.cpp
index 0f4517d..173071a 100644
--- a/Plugin/WadoRs.cpp
+++ b/Plugin/WadoRs.cpp
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -24,9 +24,7 @@
 #include "Dicom.h"
 #include "DicomResults.h"
 #include "../Orthanc/Core/Toolbox.h"
-#include "../Orthanc/Core/OrthancException.h"
 
-#include <boost/lexical_cast.hpp>
 #include <memory>
 
 static bool AcceptMultipartDicom(const OrthancPluginHttpRequest* request)
@@ -45,8 +43,7 @@ static bool AcceptMultipartDicom(const OrthancPluginHttpRequest* request)
   if (application != "multipart/related" &&
       application != "*/*")
   {
-    std::string s = "This WADO-RS plugin cannot generate the following content type: " + accept;
-    OrthancPluginLogError(context_, s.c_str());
+    OrthancPlugins::Configuration::LogError("This WADO-RS plugin cannot generate the following content type: " + accept);
     return false;
   }
 
@@ -56,16 +53,16 @@ static bool AcceptMultipartDicom(const OrthancPluginHttpRequest* request)
     Orthanc::Toolbox::ToLowerCase(s);
     if (s != "application/dicom")
     {
-      std::string s = "This WADO-RS plugin only supports application/dicom return type for DICOM retrieval (" + accept + ")";
-      OrthancPluginLogError(context_, s.c_str());
+      OrthancPlugins::Configuration::LogError("This WADO-RS plugin only supports application/dicom "
+                                              "return type for DICOM retrieval (" + accept + ")");
       return false;
     }
   }
 
   if (attributes.find("transfer-syntax") != attributes.end())
   {
-    std::string s = "This WADO-RS plugin cannot change the transfer syntax to " + attributes["transfer-syntax"];
-    OrthancPluginLogError(context_, s.c_str());
+    OrthancPlugins::Configuration::LogError("This WADO-RS plugin cannot change the transfer syntax to " + 
+                                            attributes["transfer-syntax"]);
     return false;
   }
 
@@ -100,8 +97,7 @@ static bool AcceptMetadata(const OrthancPluginHttpRequest* request,
   if (application != "multipart/related" &&
       application != "*/*")
   {
-    std::string s = "This WADO-RS plugin cannot generate the following content type: " + accept;
-    OrthancPluginLogError(context_, s.c_str());
+    OrthancPlugins::Configuration::LogError("This WADO-RS plugin cannot generate the following content type: " + accept);
     return false;
   }
 
@@ -111,16 +107,16 @@ static bool AcceptMetadata(const OrthancPluginHttpRequest* request,
     Orthanc::Toolbox::ToLowerCase(s);
     if (s != "application/dicom+xml")
     {
-      std::string s = "This WADO-RS plugin only supports application/json or application/dicom+xml return types for metadata (" + accept + ")";
-      OrthancPluginLogError(context_, s.c_str());
+      OrthancPlugins::Configuration::LogError("This WADO-RS plugin only supports application/json or "
+                                              "application/dicom+xml return types for metadata (" + accept + ")");
       return false;
     }
   }
 
   if (attributes.find("transfer-syntax") != attributes.end())
   {
-    std::string s = "This WADO-RS plugin cannot change the transfer syntax to " + attributes["transfer-syntax"];
-    OrthancPluginLogError(context_, s.c_str());
+    OrthancPlugins::Configuration::LogError("This WADO-RS plugin cannot change the transfer syntax to " + 
+                                            attributes["transfer-syntax"]);
     return false;
   }
 
@@ -145,8 +141,7 @@ static bool AcceptBulkData(const OrthancPluginHttpRequest* request)
   if (application != "multipart/related" &&
       application != "*/*")
   {
-    std::string s = "This WADO-RS plugin cannot generate the following bulk data type: " + accept;
-    OrthancPluginLogError(context_, s.c_str());
+    OrthancPlugins::Configuration::LogError("This WADO-RS plugin cannot generate the following bulk data type: " + accept);
     return false;
   }
 
@@ -156,16 +151,16 @@ static bool AcceptBulkData(const OrthancPluginHttpRequest* request)
     Orthanc::Toolbox::ToLowerCase(s);
     if (s != "application/octet-stream")
     {
-      std::string s = "This WADO-RS plugin only supports application/octet-stream return type for bulk data retrieval (" + accept + ")";
-      OrthancPluginLogError(context_, s.c_str());
+      OrthancPlugins::Configuration::LogError("This WADO-RS plugin only supports application/octet-stream "
+                                              "return type for bulk data retrieval (" + accept + ")");
       return false;
     }
   }
 
   if (attributes.find("ra,ge") != attributes.end())
   {
-    std::string s = "This WADO-RS plugin does not support Range retrieval, it can only return entire bulk data object";
-    OrthancPluginLogError(context_, s.c_str());
+    OrthancPlugins::Configuration::LogError("This WADO-RS plugin does not support Range retrieval, "
+                                            "it can only return entire bulk data object");
     return false;
   }
 
@@ -176,27 +171,30 @@ static bool AcceptBulkData(const OrthancPluginHttpRequest* request)
 static void AnswerListOfDicomInstances(OrthancPluginRestOutput* output,
                                        const std::string& resource)
 {
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
   Json::Value instances;
-  if (!OrthancPlugins::RestApiGetJson(instances, context_, resource + "/instances"))
+  if (!OrthancPlugins::RestApiGetJson(instances, context, resource + "/instances", false))
   {
     // Internal error
-    OrthancPluginSendHttpStatusCode(context_, output, 400);
+    OrthancPluginSendHttpStatusCode(context, output, 400);
     return;
   }
 
-  if (OrthancPluginStartMultipartAnswer(context_, output, "related", "application/dicom"))
+  if (OrthancPluginStartMultipartAnswer(context, output, "related", "application/dicom"))
   {
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol);
   }
   
   for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++)
   {
     std::string uri = "/instances/" + instances[i]["ID"].asString() + "/file";
-    std::string dicom;
-    if (OrthancPlugins::RestApiGetString(dicom, context_, uri) &&
-        OrthancPluginSendMultipartItem(context_, output, dicom.c_str(), dicom.size()) != 0)
+
+    OrthancPlugins::MemoryBuffer dicom(context);
+    if (dicom.RestApiGet(uri, false) &&
+        OrthancPluginSendMultipartItem(context, output, dicom.GetData(), dicom.GetSize()) != 0)
     {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError);
     }
   }
 }
@@ -209,6 +207,8 @@ static void AnswerMetadata(OrthancPluginRestOutput* output,
                            bool isInstance,
                            bool isXml)
 {
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
   std::list<std::string> files;
   if (isInstance)
   {
@@ -217,10 +217,10 @@ static void AnswerMetadata(OrthancPluginRestOutput* output,
   else
   {
     Json::Value instances;
-    if (!OrthancPlugins::RestApiGetJson(instances, context_, resource + "/instances"))
+    if (!OrthancPlugins::RestApiGetJson(instances, context, resource + "/instances", false))
     {
       // Internal error
-      OrthancPluginSendHttpStatusCode(context_, output, 400);
+      OrthancPluginSendHttpStatusCode(context, output, 400);
       return;
     }
 
@@ -230,14 +230,14 @@ static void AnswerMetadata(OrthancPluginRestOutput* output,
     }
   }
 
-  const std::string wadoBase = OrthancPlugins::Configuration::GetBaseUrl(configuration_, request);
-  OrthancPlugins::DicomResults results(context_, output, wadoBase, *dictionary_, isXml, true);
+  const std::string wadoBase = OrthancPlugins::Configuration::GetBaseUrl(request);
+  OrthancPlugins::DicomResults results(context, output, wadoBase, *dictionary_, isXml, true);
   
   for (std::list<std::string>::const_iterator
          it = files.begin(); it != files.end(); ++it)
   {
-    std::string content; 
-    if (OrthancPlugins::RestApiGetString(content, context_, *it))
+    OrthancPlugins::MemoryBuffer content(context);
+    if (content.RestApiGet(*it, false))
     {
       OrthancPlugins::ParsedDicomFile dicom(content);
       results.Add(dicom.GetFile());
@@ -254,26 +254,27 @@ static bool LocateStudy(OrthancPluginRestOutput* output,
                         std::string& uri,
                         const OrthancPluginHttpRequest* request)
 {
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
   if (request->method != OrthancPluginHttpMethod_Get)
   {
-    OrthancPluginSendMethodNotAllowed(context_, output, "GET");
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
     return false;
   }
 
   std::string id;
 
   {
-    char* tmp = OrthancPluginLookupStudy(context_, request->groups[0]);
+    char* tmp = OrthancPluginLookupStudy(context, request->groups[0]);
     if (tmp == NULL)
     {
-      std::string s = "Accessing an inexistent study with WADO-RS: " + std::string(request->groups[0]);
-      OrthancPluginLogError(context_, s.c_str());
-      OrthancPluginSendHttpStatusCode(context_, output, 404);
+      OrthancPlugins::Configuration::LogError("Accessing an inexistent study with WADO-RS: " + std::string(request->groups[0]));
+      OrthancPluginSendHttpStatusCode(context, output, 404);
       return false;
     }
 
     id.assign(tmp);
-    OrthancPluginFreeString(context_, tmp);
+    OrthancPluginFreeString(context, tmp);
   }
   
   uri = "/studies/" + id;
@@ -285,40 +286,41 @@ static bool LocateSeries(OrthancPluginRestOutput* output,
                          std::string& uri,
                          const OrthancPluginHttpRequest* request)
 {
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
   if (request->method != OrthancPluginHttpMethod_Get)
   {
-    OrthancPluginSendMethodNotAllowed(context_, output, "GET");
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
     return false;
   }
 
   std::string id;
 
   {
-    char* tmp = OrthancPluginLookupSeries(context_, request->groups[1]);
+    char* tmp = OrthancPluginLookupSeries(context, request->groups[1]);
     if (tmp == NULL)
     {
-      std::string s = "Accessing an inexistent series with WADO-RS: " + std::string(request->groups[1]);
-      OrthancPluginLogError(context_, s.c_str());
-      OrthancPluginSendHttpStatusCode(context_, output, 404);
+      OrthancPlugins::Configuration::LogError("Accessing an inexistent series with WADO-RS: " + std::string(request->groups[1]));
+      OrthancPluginSendHttpStatusCode(context, output, 404);
       return false;
     }
 
     id.assign(tmp);
-    OrthancPluginFreeString(context_, tmp);
+    OrthancPluginFreeString(context, tmp);
   }
   
   Json::Value study;
-  if (!OrthancPlugins::RestApiGetJson(study, context_, "/series/" + id + "/study"))
+  if (!OrthancPlugins::RestApiGetJson(study, context, "/series/" + id + "/study", false))
   {
-    OrthancPluginSendHttpStatusCode(context_, output, 404);
+    OrthancPluginSendHttpStatusCode(context, output, 404);
     return false;
   }
 
   if (study["MainDicomTags"]["StudyInstanceUID"].asString() != std::string(request->groups[0]))
   {
-    std::string s = "No series " + std::string(request->groups[1]) + " in study " + std::string(request->groups[0]);
-    OrthancPluginLogError(context_, s.c_str());
-    OrthancPluginSendHttpStatusCode(context_, output, 404);
+    OrthancPlugins::Configuration::LogError("No series " + std::string(request->groups[1]) + 
+                                            " in study " + std::string(request->groups[0]));
+    OrthancPluginSendHttpStatusCode(context, output, 404);
     return false;
   }
   
@@ -331,44 +333,45 @@ bool LocateInstance(OrthancPluginRestOutput* output,
                     std::string& uri,
                     const OrthancPluginHttpRequest* request)
 {
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
   if (request->method != OrthancPluginHttpMethod_Get)
   {
-    OrthancPluginSendMethodNotAllowed(context_, output, "GET");
+    OrthancPluginSendMethodNotAllowed(context, output, "GET");
     return false;
   }
 
   std::string id;
 
   {
-    char* tmp = OrthancPluginLookupInstance(context_, request->groups[2]);
+    char* tmp = OrthancPluginLookupInstance(context, request->groups[2]);
     if (tmp == NULL)
     {
-      std::string s = "Accessing an inexistent instance with WADO-RS: " + std::string(request->groups[2]);
-      OrthancPluginLogError(context_, s.c_str());
-      OrthancPluginSendHttpStatusCode(context_, output, 404);
+      OrthancPlugins::Configuration::LogError("Accessing an inexistent instance with WADO-RS: " + 
+                                              std::string(request->groups[2]));
+      OrthancPluginSendHttpStatusCode(context, output, 404);
       return false;
     }
 
     id.assign(tmp);
-    OrthancPluginFreeString(context_, tmp);
+    OrthancPluginFreeString(context, tmp);
   }
   
   Json::Value study, series;
-  if (!OrthancPlugins::RestApiGetJson(series, context_, "/instances/" + id + "/series") ||
-      !OrthancPlugins::RestApiGetJson(study, context_, "/instances/" + id + "/study"))
+  if (!OrthancPlugins::RestApiGetJson(series, context, "/instances/" + id + "/series", false) ||
+      !OrthancPlugins::RestApiGetJson(study, context, "/instances/" + id + "/study", false))
   {
-    OrthancPluginSendHttpStatusCode(context_, output, 404);
+    OrthancPluginSendHttpStatusCode(context, output, 404);
     return false;
   }
 
   if (study["MainDicomTags"]["StudyInstanceUID"].asString() != std::string(request->groups[0]) ||
       series["MainDicomTags"]["SeriesInstanceUID"].asString() != std::string(request->groups[1]))
   {
-    std::string s = ("No instance " + std::string(request->groups[2]) + 
-                     " in study " + std::string(request->groups[0]) + " or " +
-                     " in series " + std::string(request->groups[1]));
-    OrthancPluginLogError(context_, s.c_str());
-    OrthancPluginSendHttpStatusCode(context_, output, 404);
+    OrthancPlugins::Configuration::LogError("No instance " + std::string(request->groups[2]) + 
+                                            " in study " + std::string(request->groups[0]) + " or " +
+                                            " in series " + std::string(request->groups[1]));
+    OrthancPluginSendHttpStatusCode(context, output, 404);
     return false;
   }
 
@@ -383,7 +386,7 @@ void RetrieveDicomStudy(OrthancPluginRestOutput* output,
 {
   if (!AcceptMultipartDicom(request))
   {
-    OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */);
+    OrthancPluginSendHttpStatusCode(OrthancPlugins::Configuration::GetContext(), output, 400 /* Bad request */);
   }
   else
   {
@@ -402,7 +405,7 @@ void RetrieveDicomSeries(OrthancPluginRestOutput* output,
 {
   if (!AcceptMultipartDicom(request))
   {
-    OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */);
+    OrthancPluginSendHttpStatusCode(OrthancPlugins::Configuration::GetContext(), output, 400 /* Bad request */);
   }
   else
   {
@@ -420,25 +423,27 @@ void RetrieveDicomInstance(OrthancPluginRestOutput* output,
                            const char* url,
                            const OrthancPluginHttpRequest* request)
 {
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
   if (!AcceptMultipartDicom(request))
   {
-    OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */);
+    OrthancPluginSendHttpStatusCode(context, output, 400 /* Bad request */);
   }
   else
   {
     std::string uri;
     if (LocateInstance(output, uri, request))
     {
-      if (OrthancPluginStartMultipartAnswer(context_, output, "related", "application/dicom"))
+      if (OrthancPluginStartMultipartAnswer(context, output, "related", "application/dicom"))
       {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+        throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol);
       }
-  
-      std::string dicom;
-      if (OrthancPlugins::RestApiGetString(dicom, context_, uri + "/file") &&
-          OrthancPluginSendMultipartItem(context_, output, dicom.c_str(), dicom.size()) != 0)
+
+      OrthancPlugins::MemoryBuffer dicom(context);
+      if (dicom.RestApiGet(uri + "/file", false) &&
+          OrthancPluginSendMultipartItem(context, output, dicom.GetData(), dicom.GetSize()) != 0)
       {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
+        throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol);
       }
     }
   }
@@ -453,7 +458,7 @@ void RetrieveStudyMetadata(OrthancPluginRestOutput* output,
   bool isXml;
   if (!AcceptMetadata(request, isXml))
   {
-    OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */);
+    OrthancPluginSendHttpStatusCode(OrthancPlugins::Configuration::GetContext(), output, 400 /* Bad request */);
   }
   else
   {
@@ -473,7 +478,7 @@ void RetrieveSeriesMetadata(OrthancPluginRestOutput* output,
   bool isXml;
   if (!AcceptMetadata(request, isXml))
   {
-    OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */);
+    OrthancPluginSendHttpStatusCode(OrthancPlugins::Configuration::GetContext(), output, 400 /* Bad request */);
   }
   else
   {
@@ -493,7 +498,7 @@ void RetrieveInstanceMetadata(OrthancPluginRestOutput* output,
   bool isXml;
   if (!AcceptMetadata(request, isXml))
   {
-    OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */);
+    OrthancPluginSendHttpStatusCode(OrthancPlugins::Configuration::GetContext(), output, 400 /* Bad request */);
   }
   else
   {
@@ -582,15 +587,18 @@ void RetrieveBulkData(OrthancPluginRestOutput* output,
                       const char* url,
                       const OrthancPluginHttpRequest* request)
 {
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
   if (!AcceptBulkData(request))
   {
-    OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */);
+    OrthancPluginSendHttpStatusCode(context, output, 400 /* Bad request */);
     return;
   }
 
-  std::string uri, content;
+  std::string uri;
+  OrthancPlugins::MemoryBuffer content(context);
   if (LocateInstance(output, uri, request) &&
-      OrthancPlugins::RestApiGetString(content, context_, uri + "/file"))
+      content.RestApiGet(uri + "/file", false))
   {
     OrthancPlugins::ParsedDicomFile dicom(content);
 
@@ -601,15 +609,15 @@ void RetrieveBulkData(OrthancPluginRestOutput* output,
     if (path.size() % 2 == 1 &&
         ExploreBulkData(result, path, 0, dicom.GetDataSet()))
     {
-      if (OrthancPluginStartMultipartAnswer(context_, output, "related", "application/octet-stream") != 0 ||
-          OrthancPluginSendMultipartItem(context_, output, result.c_str(), result.size()) != 0)
+      if (OrthancPluginStartMultipartAnswer(context, output, "related", "application/octet-stream") != 0 ||
+          OrthancPluginSendMultipartItem(context, output, result.c_str(), result.size()) != 0)
       {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_Plugin);
+        throw OrthancPlugins::PluginException(OrthancPluginErrorCode_Plugin);
       }
     }
     else
     {
-      OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */);
+      OrthancPluginSendHttpStatusCode(context, output, 400 /* Bad request */);
     }      
   }
 }
diff --git a/Plugin/WadoRs.h b/Plugin/WadoRs.h
index 34fcab9..bbbd302 100644
--- a/Plugin/WadoRs.h
+++ b/Plugin/WadoRs.h
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
diff --git a/Plugin/WadoRsRetrieveFrames.cpp b/Plugin/WadoRsRetrieveFrames.cpp
index cc14c12..7ad13fa 100644
--- a/Plugin/WadoRsRetrieveFrames.cpp
+++ b/Plugin/WadoRsRetrieveFrames.cpp
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -21,7 +21,6 @@
 #include "WadoRs.h"
 
 #include "../Orthanc/Core/Toolbox.h"
-#include "../Orthanc/Core/OrthancException.h"
 #include "Dicom.h"
 #include "Plugin.h"
 
@@ -69,7 +68,7 @@ static gdcm::TransferSyntax ParseTransferSyntax(const OrthancPluginHttpRequest*
 
       if (tokens[0] != "multipart/related")
       {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        throw OrthancPlugins::PluginException(OrthancPluginErrorCode_ParameterOutOfRange);
       }
 
       std::string type("application/octet-stream");
@@ -82,7 +81,7 @@ static gdcm::TransferSyntax ParseTransferSyntax(const OrthancPluginHttpRequest*
 
         if (parsed.size() != 2)
         {
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest);
+          throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadRequest);
         }
 
         if (parsed[0] == "type")
@@ -104,10 +103,9 @@ static gdcm::TransferSyntax ParseTransferSyntax(const OrthancPluginHttpRequest*
         }
         else
         {
-          std::string s = ("DICOMweb RetrieveFrames: Cannot specify a transfer syntax (" + 
-                           transferSyntax + ") for default Little Endian uncompressed pixel data");
-          OrthancPluginLogError(context_, s.c_str());
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest);
+          OrthancPlugins::Configuration::LogError("DICOMweb RetrieveFrames: Cannot specify a transfer syntax (" + 
+                                                  transferSyntax + ") for default Little Endian uncompressed pixel data");
+          throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadRequest);
         }
       }
       else
@@ -164,10 +162,9 @@ static gdcm::TransferSyntax ParseTransferSyntax(const OrthancPluginHttpRequest*
         }
         else
         {
-          std::string s = ("DICOMweb RetrieveFrames: Transfer syntax \"" + 
-                           transferSyntax + "\" is incompatible with media type \"" + type + "\"");
-          OrthancPluginLogError(context_, s.c_str());
-          throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest);
+          OrthancPlugins::Configuration::LogError("DICOMweb RetrieveFrames: Transfer syntax \"" + 
+                                                  transferSyntax + "\" is incompatible with media type \"" + type + "\"");
+          throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadRequest);
         }
       }
     }
@@ -201,9 +198,8 @@ static void ParseFrameList(std::list<unsigned int>& frames,
     int frame = boost::lexical_cast<int>(tokens[i]);
     if (frame <= 0)
     {
-      std::string s = "Invalid frame number (must be > 0): " + tokens[i];
-      OrthancPluginLogError(context_, s.c_str());
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+      OrthancPlugins::Configuration::LogError("Invalid frame number (must be > 0): " + tokens[i]);
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_ParameterOutOfRange);
     }
 
     frames.push_back(static_cast<unsigned int>(frame - 1));
@@ -253,7 +249,7 @@ static const char* GetMimeType(const gdcm::TransferSyntax& syntax)
       return "image/dicom+jpx; transfer-syntax=1.2.840.10008.1.2.4.93";
 
     default:
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError);
   }
 }
 
@@ -272,14 +268,14 @@ static void AnswerSingleFrame(OrthancPluginRestOutput* output,
   std::string location = dicom.GetWadoUrl(request) + "frames/" + boost::lexical_cast<std::string>(frameIndex + 1);
   const char *keys[] = { "Content-Location" };
   const char *values[] = { location.c_str() };
-  error = OrthancPluginSendMultipartItem2(context_, output, frame, size, 1, keys, values);
+  error = OrthancPluginSendMultipartItem2(OrthancPlugins::Configuration::GetContext(), output, frame, size, 1, keys, values);
 #else
-  error = OrthancPluginSendMultipartItem(context_, output, frame, size);
+  error = OrthancPluginSendMultipartItem(OrthancPlugins::Configuration::GetContext(), output, frame, size);
 #endif
 
   if (error != OrthancPluginErrorCode_Success)
   {
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);      
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NetworkProtocol);      
   }
 }
 
@@ -293,13 +289,14 @@ static bool AnswerFrames(OrthancPluginRestOutput* output,
 {
   if (!dicom.GetDataSet().FindDataElement(OrthancPlugins::DICOM_TAG_PIXEL_DATA))
   {
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_IncompatibleImageFormat);
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_IncompatibleImageFormat);
   }
 
   const gdcm::DataElement& pixelData = dicom.GetDataSet().GetDataElement(OrthancPlugins::DICOM_TAG_PIXEL_DATA);
   const gdcm::SequenceOfFragments* fragments = pixelData.GetSequenceOfFragments();
 
-  if (OrthancPluginStartMultipartAnswer(context_, output, "related", GetMimeType(syntax)) != OrthancPluginErrorCode_Success)
+  if (OrthancPluginStartMultipartAnswer(OrthancPlugins::Configuration::GetContext(), 
+                                        output, "related", GetMimeType(syntax)) != OrthancPluginErrorCode_Success)
   {
     return false;
   }
@@ -310,8 +307,8 @@ static bool AnswerFrames(OrthancPluginRestOutput* output,
 
     if (pixelData.GetByteValue() == NULL)
     {
-      OrthancPluginLogError(context_, "Image was not properly decoded");
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);      
+      OrthancPlugins::Configuration::LogError("Image was not properly decoded");
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError);      
     }
 
     int width, height, bits;
@@ -320,14 +317,14 @@ static bool AnswerFrames(OrthancPluginRestOutput* output,
         !dicom.GetIntegerTag(width, *dictionary_, OrthancPlugins::DICOM_TAG_COLUMNS) ||
         !dicom.GetIntegerTag(bits, *dictionary_, OrthancPlugins::DICOM_TAG_BITS_ALLOCATED))
     {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat);
     }
 
     size_t frameSize = height * width * bits / 8;
     
     if (pixelData.GetByteValue()->GetLength() % frameSize != 0)
     {
-      throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);      
+      throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError);      
     }
 
     size_t framesCount = pixelData.GetByteValue()->GetLength() / frameSize;
@@ -349,10 +346,9 @@ static bool AnswerFrames(OrthancPluginRestOutput* output,
     {
       if (*frame >= framesCount)
       {
-        std::string s = ("Trying to access frame number " + boost::lexical_cast<std::string>(*frame + 1) + 
-                         " of an image with " + boost::lexical_cast<std::string>(framesCount) + " frames");
-        OrthancPluginLogError(context_, s.c_str());
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        OrthancPlugins::Configuration::LogError("Trying to access frame number " + boost::lexical_cast<std::string>(*frame + 1) + 
+                                                " of an image with " + boost::lexical_cast<std::string>(framesCount) + " frames");
+        throw OrthancPlugins::PluginException(OrthancPluginErrorCode_ParameterOutOfRange);
       }
       else
       {
@@ -379,10 +375,13 @@ static bool AnswerFrames(OrthancPluginRestOutput* output,
     {
       if (*frame >= fragments->GetNumberOfFragments())
       {
-        std::string s = ("Trying to access frame number " + boost::lexical_cast<std::string>(*frame + 1) + 
-                         " of an image with " + boost::lexical_cast<std::string>(fragments->GetNumberOfFragments()) + " frames");
-        OrthancPluginLogError(context_, s.c_str());
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
+        // TODO A frame is not a fragment, looks like a bug
+        OrthancPlugins::Configuration::LogError("Trying to access frame number " + 
+                                                boost::lexical_cast<std::string>(*frame + 1) + 
+                                                " of an image with " + 
+                                                boost::lexical_cast<std::string>(fragments->GetNumberOfFragments()) + 
+                                                " frames");
+        throw OrthancPlugins::PluginException(OrthancPluginErrorCode_ParameterOutOfRange);
       }
       else
       {
@@ -402,16 +401,19 @@ void RetrieveFrames(OrthancPluginRestOutput* output,
                     const char* url,
                     const OrthancPluginHttpRequest* request)
 {
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
   gdcm::TransferSyntax targetSyntax(ParseTransferSyntax(request));
 
   std::list<unsigned int> frames;
   ParseFrameList(frames, request);
 
   Json::Value header;
-  std::string uri, content;
+  std::string uri;
+  OrthancPlugins::MemoryBuffer content(context);
   if (LocateInstance(output, uri, request) &&
-      OrthancPlugins::RestApiGetString(content, context_, uri + "/file") &&
-      OrthancPlugins::RestApiGetJson(header, context_, uri + "/header?simplify"))
+      content.RestApiGet(uri + "/file", false) &&
+      OrthancPlugins::RestApiGetJson(header, context, uri + "/header?simplify", false))
   {
     {
       std::string s = "DICOMweb RetrieveFrames on " + uri + ", frames: ";
@@ -420,7 +422,8 @@ void RetrieveFrames(OrthancPluginRestOutput* output,
       {
         s += boost::lexical_cast<std::string>(*frame + 1) + " ";
       }
-      OrthancPluginLogInfo(context_, s.c_str());
+
+      OrthancPlugins::Configuration::LogInfo(s);
     }
 
     std::auto_ptr<OrthancPlugins::ParsedDicomFile> source;
@@ -456,29 +459,31 @@ void RetrieveFrames(OrthancPluginRestOutput* output,
       // Need to convert the transfer syntax
 
       {
-        std::string s = ("DICOMweb RetrieveFrames: Transcoding " + uri + " from transfer syntax " + 
-                         std::string(sourceSyntax.GetString()) + " to " + std::string(targetSyntax.GetString()));
-        OrthancPluginLogInfo(context_, s.c_str());
+        OrthancPlugins::Configuration::LogInfo("DICOMweb RetrieveFrames: Transcoding " + uri + 
+                                               " from transfer syntax " + std::string(sourceSyntax.GetString()) + 
+                                               " to " + std::string(targetSyntax.GetString()));
       }
 
       gdcm::ImageChangeTransferSyntax change;
       change.SetTransferSyntax(targetSyntax);
 
-      std::stringstream stream(content);
+      // TODO Avoid this unnecessary memcpy by defining a stream over the MemoryBuffer
+      std::string dicom(content.GetData(), content.GetData() + content.GetSize());
+      std::stringstream stream(dicom);
 
       gdcm::ImageReader reader;
       reader.SetStream(stream);
       if (!reader.Read())
       {
-        OrthancPluginLogError(context_, "Cannot decode the image");
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat);
+        OrthancPlugins::Configuration::LogError("Cannot decode the image");
+        throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadFileFormat);
       }
 
       change.SetInput(reader.GetImage());
       if (!change.Change())
       {
-        OrthancPluginLogError(context_, "Cannot change the transfer syntax of the image");
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        OrthancPlugins::Configuration::LogError("Cannot change the transfer syntax of the image");
+        throw OrthancPlugins::PluginException(OrthancPluginErrorCode_InternalError);
       }
 
       gdcm::ImageWriter writer;
@@ -489,7 +494,7 @@ void RetrieveFrames(OrthancPluginRestOutput* output,
       writer.SetStream(ss);
       if (!writer.Write())
       {
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotEnoughMemory);
+        throw OrthancPlugins::PluginException(OrthancPluginErrorCode_NotEnoughMemory);
       }
 
       OrthancPlugins::ParsedDicomFile transcoded(ss.str());
diff --git a/Plugin/Wado.cpp b/Plugin/WadoUri.cpp
similarity index 56%
rename from Plugin/Wado.cpp
rename to Plugin/WadoUri.cpp
index e332e6e..3eb13d4 100644
--- a/Plugin/Wado.cpp
+++ b/Plugin/WadoUri.cpp
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -18,24 +18,26 @@
  **/
 
 
-#include "Wado.h"
+#include "WadoUri.h"
 #include "Plugin.h"
 
-#include "../Orthanc/Core/OrthancException.h"
 #include "Configuration.h"
 
 #include <string>
 
+
 static bool MapWadoToOrthancIdentifier(std::string& orthanc,
                                        char* (*func) (OrthancPluginContext*, const char*),
                                        const std::string& dicom)
 {
-  char* tmp = func(context_, dicom.c_str());
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
+  char* tmp = func(context, dicom.c_str());
 
   if (tmp)
   {
     orthanc = tmp;
-    OrthancPluginFreeString(context_, tmp);
+    OrthancPluginFreeString(context, tmp);
     return true;
   }
   else
@@ -49,6 +51,8 @@ static bool LocateInstance(std::string& instance,
                            std::string& contentType,
                            const OrthancPluginHttpRequest* request)
 {
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
   std::string requestType, studyUid, seriesUid, objectUid;
 
   for (uint32_t i = 0; i < request->getCount; i++)
@@ -64,7 +68,7 @@ static bool LocateInstance(std::string& instance,
     {
       seriesUid = value;
     }
-    else if (key == "objectUID")  // In WADO, "objectUID" corresponds to "SOPInstanceUID"
+    else if (key == "objectUID")  // In WADO-URI, "objectUID" corresponds to "SOPInstanceUID"
     {
       objectUid = value;
     }
@@ -80,21 +84,19 @@ static bool LocateInstance(std::string& instance,
 
   if (requestType != "WADO")
   {
-    std::string msg = "WADO: Invalid requestType: \"" + requestType + "\"";
-    OrthancPluginLogError(context_, msg.c_str());
+    OrthancPlugins::Configuration::LogError("WADO-URI: Invalid requestType: \"" + requestType + "\"");
     return false;
   }
 
   if (objectUid.empty())
   {
-    OrthancPluginLogError(context_, "WADO: No SOPInstanceUID provided");
+    OrthancPlugins::Configuration::LogError("WADO-URI: No SOPInstanceUID provided");
     return false;
   }
 
   if (!MapWadoToOrthancIdentifier(instance, OrthancPluginLookupInstance, objectUid))
   {
-    std::string msg = "WADO: No such SOPInstanceUID in Orthanc: \"" + objectUid + "\"";
-    OrthancPluginLogError(context_, msg.c_str());
+    OrthancPlugins::Configuration::LogError("WADO-URI: No such SOPInstanceUID in Orthanc: \"" + objectUid + "\"");
     return false;
   }
 
@@ -108,18 +110,16 @@ static bool LocateInstance(std::string& instance,
     std::string series;
     if (!MapWadoToOrthancIdentifier(series, OrthancPluginLookupSeries, seriesUid))
     {
-      std::string msg = "WADO: No such SeriesInstanceUID in Orthanc: \"" + seriesUid + "\"";
-      OrthancPluginLogError(context_, msg.c_str());
+      OrthancPlugins::Configuration::LogError("WADO-URI: No such SeriesInstanceUID in Orthanc: \"" + seriesUid + "\"");
       return false;
     }
     else
     {
       Json::Value info;
-      if (!OrthancPlugins::RestApiGetJson(info, context_, "/instances/" + instance + "/series") ||
+      if (!OrthancPlugins::RestApiGetJson(info, context, "/instances/" + instance + "/series", false) ||
           info["MainDicomTags"]["SeriesInstanceUID"] != seriesUid)
       {
-        std::string msg = "WADO: Instance " + objectUid + " does not belong to series " + seriesUid;
-        OrthancPluginLogError(context_, msg.c_str());
+        OrthancPlugins::Configuration::LogError("WADO-URI: Instance " + objectUid + " does not belong to series " + seriesUid);
         return false;
       }
     }
@@ -130,18 +130,16 @@ static bool LocateInstance(std::string& instance,
     std::string study;
     if (!MapWadoToOrthancIdentifier(study, OrthancPluginLookupStudy, studyUid))
     {
-      std::string msg = "WADO: No such StudyInstanceUID in Orthanc: \"" + studyUid + "\"";
-      OrthancPluginLogError(context_, msg.c_str());
+      OrthancPlugins::Configuration::LogError("WADO-URI: No such StudyInstanceUID in Orthanc: \"" + studyUid + "\"");
       return false;
     }
     else
     {
       Json::Value info;
-      if (!OrthancPlugins::RestApiGetJson(info, context_, "/instances/" + instance + "/study") ||
+      if (!OrthancPlugins::RestApiGetJson(info, context, "/instances/" + instance + "/study", false) ||
           info["MainDicomTags"]["StudyInstanceUID"] != studyUid)
       {
-        std::string msg = "WADO: Instance " + objectUid + " does not belong to study " + studyUid;
-        OrthancPluginLogError(context_, msg.c_str());
+        OrthancPlugins::Configuration::LogError("WADO-URI: Instance " + objectUid + " does not belong to study " + studyUid);
         return false;
       }
     }
@@ -154,35 +152,36 @@ static bool LocateInstance(std::string& instance,
 static void AnswerDicom(OrthancPluginRestOutput* output,
                         const std::string& instance)
 {
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
   std::string uri = "/instances/" + instance + "/file";
 
-  std::string dicom;
-  if (OrthancPlugins::RestApiGetString(dicom, context_, uri))
+  OrthancPlugins::MemoryBuffer dicom(context);
+  if (dicom.RestApiGet(uri, false))
   {
-    OrthancPluginAnswerBuffer(context_, output, dicom.c_str(), dicom.size(), "application/dicom");
+    OrthancPluginAnswerBuffer(context, output, 
+                              dicom.GetData(), dicom.GetSize(), "application/dicom");
   }
   else
   {
-    std::string msg = "WADO: Unable to retrieve DICOM file from " + uri;
-    OrthancPluginLogError(context_, msg.c_str());
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_Plugin);
+    OrthancPlugins::Configuration::LogError("WADO-URI: Unable to retrieve DICOM file from " + uri);
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_Plugin);
   }
 }
 
 
-static bool RetrievePngPreview(std::string& png,
+static bool RetrievePngPreview(OrthancPlugins::MemoryBuffer& png,
                                const std::string& instance)
 {
   std::string uri = "/instances/" + instance + "/preview";
 
-  if (OrthancPlugins::RestApiGetString(png, context_, uri, true))
+  if (png.RestApiGet(uri, true))
   {
     return true;
   }
   else
   {
-    std::string msg = "WADO: Unable to generate a preview image for " + uri;
-    OrthancPluginLogError(context_, msg.c_str());
+    OrthancPlugins::Configuration::LogError("WADO-URI: Unable to generate a preview image for " + uri);
     return false;
   }
 }
@@ -191,14 +190,17 @@ static bool RetrievePngPreview(std::string& png,
 static void AnswerPngPreview(OrthancPluginRestOutput* output,
                              const std::string& instance)
 {
-  std::string png;
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
+  OrthancPlugins::MemoryBuffer png(context);
   if (RetrievePngPreview(png, instance))
   {
-    OrthancPluginAnswerBuffer(context_, output, png.c_str(), png.size(), "image/png");
+    OrthancPluginAnswerBuffer(context, output, 
+                              png.GetData(), png.GetSize(), "image/png");
   }
   else
   {
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_Plugin);
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_Plugin);
   }
 }
 
@@ -206,38 +208,28 @@ static void AnswerPngPreview(OrthancPluginRestOutput* output,
 static void AnswerJpegPreview(OrthancPluginRestOutput* output,
                               const std::string& instance)
 {
+  OrthancPluginContext* context = OrthancPlugins::Configuration::GetContext();
+
   // Retrieve the preview in the PNG format
-  std::string png;
+  OrthancPlugins::MemoryBuffer png(context);
   if (!RetrievePngPreview(png, instance))
   {
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_Plugin);
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_Plugin);
   }
-
-  // Decode the PNG file
-  OrthancPluginImage* image = OrthancPluginUncompressImage(
-    context_, png.c_str(), png.size(), OrthancPluginImageFormat_Png);
-
-  // Convert to JPEG
-  OrthancPluginCompressAndAnswerJpegImage(
-    context_, output, 
-    OrthancPluginGetImagePixelFormat(context_, image),
-    OrthancPluginGetImageWidth(context_, image),
-    OrthancPluginGetImageHeight(context_, image),
-    OrthancPluginGetImagePitch(context_, image),
-    OrthancPluginGetImageBuffer(context_, image), 
-    90 /*quality*/);
-
-  OrthancPluginFreeImage(context_, image);
+  
+  OrthancPlugins::OrthancImage image(context);
+  image.UncompressPngImage(png.GetData(), png.GetSize());
+  image.AnswerJpegImage(output, 90 /* quality */);
 }
 
 
-void WadoCallback(OrthancPluginRestOutput* output,
-                  const char* url,
-                  const OrthancPluginHttpRequest* request)
+void WadoUriCallback(OrthancPluginRestOutput* output,
+                     const char* url,
+                     const OrthancPluginHttpRequest* request)
 {
   if (request->method != OrthancPluginHttpMethod_Get)
   {
-    OrthancPluginSendMethodNotAllowed(context_, output, "GET");
+    OrthancPluginSendMethodNotAllowed(OrthancPlugins::Configuration::GetContext(), output, "GET");
     return;
   }
 
@@ -245,7 +237,7 @@ void WadoCallback(OrthancPluginRestOutput* output,
   std::string contentType = "image/jpg";  // By default, JPEG image will be returned
   if (!LocateInstance(instance, contentType, request))
   {
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownResource);
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_UnknownResource);
   }
 
   if (contentType == "application/dicom")
@@ -263,8 +255,7 @@ void WadoCallback(OrthancPluginRestOutput* output,
   }
   else
   {
-    std::string msg = "WADO: Unsupported content type: \"" + contentType + "\"";
-    OrthancPluginLogError(context_, msg.c_str());
-    throw Orthanc::OrthancException(Orthanc::ErrorCode_BadRequest);
+    OrthancPlugins::Configuration::LogError("WADO-URI: Unsupported content type: \"" + contentType + "\"");
+    throw OrthancPlugins::PluginException(OrthancPluginErrorCode_BadRequest);
   }
 }
diff --git a/Plugin/Wado.h b/Plugin/WadoUri.h
similarity index 79%
rename from Plugin/Wado.h
rename to Plugin/WadoUri.h
index c79acd9..1784723 100644
--- a/Plugin/Wado.h
+++ b/Plugin/WadoUri.h
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -22,6 +22,6 @@
 
 #include "Configuration.h"
 
-void WadoCallback(OrthancPluginRestOutput* output,
-                  const char* url,
-                  const OrthancPluginHttpRequest* request);
+void WadoUriCallback(OrthancPluginRestOutput* output,
+                     const char* url,
+                     const OrthancPluginHttpRequest* request);
diff --git a/README b/README
index 78aad90..347fb6d 100644
--- a/README
+++ b/README
@@ -1,5 +1,5 @@
-DICOM Web plugin for Orthanc
-============================
+DICOMweb plugin for Orthanc
+===========================
 
 
 General Information
@@ -10,8 +10,8 @@ lightweight Vendor Neutral Archive for medical imaging. This plugin
 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:
 
@@ -32,23 +32,18 @@ The full status about the support of these protocols can be found in
 the "./Status.txt" file.
 
 
-Install
--------
+Installation and usage
+----------------------
 
 Build instructions can be found in "./Resources/BuildInstructions.txt".
 
-
-Samples
--------
-
-Python samples to call the DICOM Web services can be found in the
-"./Samples" folder.
+Usage instructions can be found in "./Usage.txt".
 
 
 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.
 
diff --git a/Resources/CMake/GdcmConfiguration.cmake b/Resources/CMake/GdcmConfiguration.cmake
index 8c8eb8d..402e470 100644
--- a/Resources/CMake/GdcmConfiguration.cmake
+++ b/Resources/CMake/GdcmConfiguration.cmake
@@ -1,5 +1,5 @@
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# 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
diff --git a/Resources/Samples/JavaScript/index.html b/Resources/Samples/JavaScript/index.html
index 4269fa1..83f99d6 100644
--- a/Resources/Samples/JavaScript/index.html
+++ b/Resources/Samples/JavaScript/index.html
@@ -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">
diff --git a/Resources/Samples/JavaScript/qido-rs.js b/Resources/Samples/JavaScript/qido-rs.js
index a336b83..ab7e387 100644
--- a/Resources/Samples/JavaScript/qido-rs.js
+++ b/Resources/Samples/JavaScript/qido-rs.js
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
diff --git a/Resources/Samples/JavaScript/stow-rs.js b/Resources/Samples/JavaScript/stow-rs.js
index 2c28a8a..cc37f84 100644
--- a/Resources/Samples/JavaScript/stow-rs.js
+++ b/Resources/Samples/JavaScript/stow-rs.js
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
diff --git a/Resources/Samples/SendStow.py b/Resources/Samples/Python/SendStow.py
similarity index 57%
rename from Resources/Samples/SendStow.py
rename to Resources/Samples/Python/SendStow.py
index 0769629..98c5b18 100755
--- a/Resources/Samples/SendStow.py
+++ b/Resources/Samples/Python/SendStow.py
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# 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
@@ -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 @@ if len(sys.argv) < 2:
 
 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()
-
-# Discard the header
-body = body.split('\n\n', 1)[1]
+# Closing boundary
+body += bytearray('--%s--' % boundary, 'ascii')
 
-headers['Content-Type'] = 'multipart/related; type=application/dicom; boundary=%s' % related.get_boundary()
-headers['Accept'] = 'application/json'
+# 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',
+})
 
-r = requests.post(URL, data=body, headers=headers)
 j = json.loads(r.text)
 
 # Loop over the successful instances
@@ -64,4 +71,7 @@ for instance in j['00081199']['Value']:
         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!')
diff --git a/Resources/Samples/WadoRetrieveStudy.py b/Resources/Samples/Python/WadoRetrieveStudy.py
similarity index 95%
rename from Resources/Samples/WadoRetrieveStudy.py
rename to Resources/Samples/Python/WadoRetrieveStudy.py
index b6b2084..25ed702 100755
--- a/Resources/Samples/WadoRetrieveStudy.py
+++ b/Resources/Samples/Python/WadoRetrieveStudy.py
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 
 # Orthanc - A Lightweight, RESTful DICOM Store
-# Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+# 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
diff --git a/Resources/SyncOrthancFolder.py b/Resources/SyncOrthancFolder.py
index 05018e0..5ed3021 100755
--- a/Resources/SyncOrthancFolder.py
+++ b/Resources/SyncOrthancFolder.py
@@ -9,9 +9,10 @@ import multiprocessing
 import os
 import stat
 import urllib2
+import uuid
 
 TARGET = os.path.join(os.path.dirname(__file__), '..', 'Orthanc')
-PLUGIN_SDK_VERSION = '0.9.5'
+PLUGIN_SDK_VERSION = '1.1.0'
 REPOSITORY = 'https://bitbucket.org/sjodogne/orthanc/raw'
 
 FILES = [
@@ -24,7 +25,11 @@ FILES = [
     'Core/PrecompiledHeaders.h',
     'Core/Toolbox.cpp',
     'Core/Toolbox.h',
+    'Core/WebServiceParameters.cpp',
+    'Core/WebServiceParameters.h',
     'Plugins/Samples/Common/ExportedSymbols.list',
+    'Plugins/Samples/Common/OrthancPluginCppWrapper.h',
+    'Plugins/Samples/Common/OrthancPluginCppWrapper.cpp',
     'Plugins/Samples/Common/VersionScript.map',
     'Resources/CMake/BoostConfiguration.cmake',
     'Resources/CMake/Compiler.cmake',
@@ -63,10 +68,14 @@ def Download(x):
     except:
         pass
 
-    url = '%s/%s/%s' % (REPOSITORY, branch, source)
+    url = '%s/%s/%s?force=%s' % (REPOSITORY, branch, source, uuid.uuid4())
 
     with open(target, 'w') as f:
-        f.write(urllib2.urlopen(url).read())
+        try:
+            f.write(urllib2.urlopen(url).read())
+        except:
+            print('Cannot download %s' % url)
+            raise
 
 
 commands = []
@@ -75,7 +84,12 @@ for f in FILES:
     commands.append([ 'default', f, f ])
 
 for f in SDK:
-    commands.append([ 'Orthanc-%s' % PLUGIN_SDK_VERSION, 
+    if PLUGIN_SDK_VERSION == 'mainline':
+        branch = 'default'
+    else:
+        branch = 'Orthanc-%s' % PLUGIN_SDK_VERSION
+
+    commands.append([ branch, 
                       'Plugins/Include/%s' % f,
                       'Sdk-%s/%s' % (PLUGIN_SDK_VERSION, f) ])
 
diff --git a/UnitTestsSources/UnitTestsMain.cpp b/UnitTestsSources/UnitTestsMain.cpp
index 25838b0..bd26c3e 100644
--- a/UnitTestsSources/UnitTestsMain.cpp
+++ b/UnitTestsSources/UnitTestsMain.cpp
@@ -1,6 +1,6 @@
 /**
  * Orthanc - A Lightweight, RESTful DICOM Store
- * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * 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
@@ -26,7 +26,7 @@
 
 using namespace OrthancPlugins;
 
-Json::Value configuration_ = Json::objectValue;
+OrthancPluginContext* context_ = NULL;
 
 
 TEST(ContentType, Parse)
diff --git a/Usage.txt b/Usage.txt
new file mode 100644
index 0000000..0f78fe8
--- /dev/null
+++ b/Usage.txt
@@ -0,0 +1,284 @@
+=============
+Configuration
+=============
+
+(1) You must change the Orthanc configuration file to tell Orthanc
+    where it can find the DICOMweb plugin. This is done by properly
+    modifying the "Plugins" configuration option of Orthanc. For
+    instance, in Linux:
+
+{
+  ...
+  "Plugins" : [
+    "/home/user/OrthancDicomWeb/Build/libOrthancDicomWeb.so"
+  ]
+  ...
+}
+
+    Or in Windows:
+
+{
+  ...
+  "Plugins" : [
+    "c:/Temp/OrthancDicomWeb.dll"
+  ]
+  ...
+}
+
+    Note that the DICOMweb server will share all the parameters of the
+    Orthanc HTTP server, notably wrt. authentication and HTTPS
+    encryption. For this reason, you will most probably have to enable
+    the remote access to the Orthanc HTTP server:
+
+{
+  ...
+  "RemoteAccessEnabled" : true
+  ...
+}
+
+
+(2) There are several configuration options that can be set to
+    fine-tune the Orthanc DICOMweb server. Here is the full list of
+    the available options, all of them must be grouped inside the
+    "DicomWeb" section of the Orthanc configuration file:
+
+{
+  ...
+  "DicomWeb" : {
+    "Enable" : true,          // Whether DICOMweb support is enabled
+    "Root" : "/dicom-web/",   // Root URI of the DICOMweb API (for QIDO-RS, STOW-RS and WADO-RS)
+    "EnableWado" : true,      // Whether WADO-URI (previously known as WADO) support is enabled
+    "WadoRoot" : "/wado",     // Root URI of the WADO-URI (aka. WADO) API
+    "Host" : "localhost",     // Hard-codes the name of the host for subsequent WADO-RS requests
+    "Ssl" : false,            // Whether HTTPS should be used for subsequent WADO-RS requests
+    "StowMaxInstances" : 10,  // For STOW-RS client, the maximum number of instances in one single HTTP query (0 = no limit)
+    "StowMaxSize" : 10        // For STOW-RS client, the maximum size of the body in one single HTTP query (in MB, 0 = no limit)
+  }
+  ...
+}
+
+
+(3) If you want to connect Orthanc as a client to remote DICOMweb
+    servers (cf. below), you need to modify the configuration file so
+    as to define each of them in the option "DicomWeb.Servers".  The
+    syntax is identical to the "OrthancPeers" parameters.
+
+    In the most simple case, here is how to instruct Orthanc about the
+    existence of a password-less DICOMweb server that will be refered
+    to as "sample" in Orthanc:
+
+{
+  ...
+  "DicomWeb" : {
+    "Servers" : {
+      "sample" : [ "http://192.168.1.1/dicom-web/" ]
+    }
+  }
+  ...
+}
+
+    You are of course free to add as many DICOMweb servers as you
+    need. If the DICOMweb server is protected by a password (with HTTP
+    Basic access authentication):
+
+{
+  ...
+  "DicomWeb" : {
+    "Servers" : {
+      "sample" : [ "http://192.168.1.1/dicom-web/", "username", "password" ]
+    }
+  }
+  ...
+}
+
+    If the DICOMweb server is protected with HTTPS client
+    authentication, you must provide your client certificate (in the
+    PEM format), your client private key (in the PEM format), together
+    with the password protecting the private key:
+
+{
+  ...
+  "DicomWeb" : {
+    "Servers" : {
+      "sample" : {
+        "Url" : "http://192.168.1.1/dicom-web/", 
+        "CertificateFile" : "client.crt",
+        "CertificateKeyFile" : "client.key",
+        "CertificateKeyPassword" : "password"
+      }
+    }
+  }
+  ...
+}
+
+    Finally, it is also possible to use client authentication with
+    hardware security modules and smart cards through PKCS#11 (this
+    feature is only available is the core of Orthanc was compiled with
+    the "-DENABLE_PKCS11=ON" option in CMake, and if the Orthanc
+    configuration file has a proper "Pkcs11" section):
+
+{
+  ...
+  "DicomWeb" : {
+    "Servers" : {
+      "sample" : {
+        "Url" : "http://192.168.1.1/dicom-web/", 
+        "Pkcs11" : true
+      }
+    }
+  }
+  ...
+}
+
+    Important remark: When querying a DICOMweb server, Orthanc will
+    automatically use the global configuration options "HttpProxy",
+    "HttpTimeout", "HttpsVerifyPeers", "HttpsCACertificates", and
+    "Pkcs11". Make sure to adapt them if need be.
+
+
+
+=================================
+Querying a remote DICOMweb server
+=================================
+
+Listing the available servers
+-----------------------------
+
+The list of the remote DICOMweb servers that are known to the DICOMweb
+plugin can be obtained as follows:
+
+# curl http://localhost:8042/dicom-web/servers/
+[ "sample" ]
+
+Here, a single server called "sample" is configured.
+
+
+Making a call to QIDO-RS or WADO-RS
+-----------------------------------
+
+In Orthanc, the URI "/{dicom-web}/servers/{name}/get" allows to make a
+HTTP GET call against a DICOMweb server. This can be used to issue a
+QIDO-RS or WADO-RS command. Orthanc will take care of properly
+encoding the URL and authenticating the client.
+
+For instance, here is a sample QIDO-RS search to query all the
+studies (using a bash command-line):
+
+# curl http://localhost:8042/dicom-web/servers/sample/get -d @- << EOF
+{
+  "Uri" : "/studies"
+}
+EOF
+
+You do not have to specify the base URL of the remote DICOMweb server,
+as it is encoded in the configuration file.
+
+The result of the command above is a multipart "application/dicom+xml"
+document.  It is possible to request a more human-friendly JSON answer
+by adding the "Accept" HTTP header. Here is how to search for a given
+patient name, while requesting a JSON answer and pretty-printing
+through the "json_pp" command-line tool:
+
+# curl http://localhost:8042/dicom-web/servers/sample/get -d @- << EOF | json_pp 
+{
+  "Uri" : "/studies",
+  "HttpHeaders" : {
+    "Accept" : "application/json"
+  },
+  "Arguments" : {
+    "00100010" : "*JODOGNE*"
+  }
+}
+EOF
+
+Note how all the GET arguments must be specified in the "Arguments"
+field. Orthanc will take care of properly encoding it to a URL.
+
+An user-friendly reference of the features available in QIDO-RS and
+WADO-RS can be found at http://dicomweb.hcintegrations.ca/#/home
+
+
+Sending DICOM resources to a STOW-RS server
+-------------------------------------------
+
+STOW-RS allows to send local DICOM resources to a remote DICOMweb
+server. In Orthanc, the STOW-RS client primitive is available at URI
+"/{dicom-web}/servers/{name}/stow". Here is a sample call:
+
+# curl http://localhost:8042/dicom-web/servers/sample/stow -X POST -d @- << EOF
+{
+  "Resources" : [
+    "6ca4c9f3-5e895cb3-4d82c6da-09e060fe-9c59f228"
+  ]
+}
+EOF
+
+Note that this primitive takes as its input a list of Orthanc
+identifiers corresponding to the resources (patients, studies, series
+and/or instances) to be exported:
+https://orthanc.chu.ulg.ac.be/book/faq/orthanc-ids.html
+
+Remark 1: Additional HTTP headers can be added with an optional
+"HttpHeaders" argument, as for QIDO-RS and WADO-RS. This might be
+useful e.g. for cookie-based session management.
+
+Remark 2: One call to this "/stow" primitive will possibly result in
+several HTTP requests to the DICOMweb server, in order to limit the
+size of the HTTP messages. The configuration options
+"DicomWeb.StowMaxInstances" and "DicomWeb.StowMaxSize" can be used to
+tune this behavior (set both options to 0 to send one single request).
+
+
+Retrieving DICOM resources from a WADO-RS server
+------------------------------------------------
+
+Once DICOM resources of interest have been identified through a
+QIDO-RS call to a remote DICOMweb server (cf. above), it is
+interesting to download them locally with a WADO-RS call. You could do
+it manually with a second call to the
+"/{dicom-web}/servers/{name}/get" URI, but Orthanc provides another
+primitive "/retrieve" to automate this process.
+
+Here is how you would download one study, one series and one instance
+whose StudyInstanceUID (0020,000d), SeriesInstanceUID (0020,000e) are
+SOPInstanceUID (0008,0018) have been identified through a former
+QIDO-RS call:
+
+# curl http://localhost:8042/dicom-web/servers/sample/retrieve -X POST -d @- << EOF
+{
+  "Resources" : [
+    {
+      "Study" : "1.3.51.0.1.1.192.168.29.133.1688840.1688819"
+    },
+    {
+      "Study" : "1.3.51.0.1.1.192.168.29.133.1681753.1681732",
+      "Series" : "1.3.12.2.1107.5.2.33.37097.2012041613040617636372171.0.0.0"
+    },
+    {
+      "Study" : "1.3.51.0.1.1.192.168.29.133.1681753.1681732",
+      "Series" : "1.3.12.2.1107.5.2.33.37097.2012041612474981424569674.0.0.0",
+      "Instance" : "1.3.12.2.1107.5.2.33.37097.2012041612485540185869716"
+    }
+  ]
+}
+EOF
+
+Orthanc will reply with the list of the Orthanc identifiers of all the
+DICOM instances that were downloaded from the remote server.
+
+Remark 1: Contrarily to the "/stow" URI that uses Orthanc identifiers,
+the "/retrieve" URI uses DICOM identifiers.
+
+Remark 2: The "HttpArguments" is also available.
+
+
+
+=======
+Samples
+=======
+
+Samples of how to call DICOMweb services from standalone applications
+can be found in the following folders:
+
+- In Python: see ./Resources/Samples/Python/
+- In JavaScript: see ./Resources/Samples/Python/

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-med/orthanc-dicomweb.git



More information about the debian-med-commit mailing list