changeset 2799:6e3a60b85da6

New primitives to access Orthanc peers from plugins
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 22 Aug 2018 16:55:07 +0200
parents 5c83e5cf9d79
children dc7330089736
files NEWS OrthancServer/LuaScripting.cpp OrthancServer/OrthancInitialization.cpp OrthancServer/OrthancInitialization.h OrthancServer/OrthancRestApi/OrthancRestModalities.cpp Plugins/Engine/OrthancPlugins.cpp Plugins/Engine/OrthancPlugins.h Plugins/Include/orthanc/OrthancCPlugin.h
diffstat 8 files changed, 567 insertions(+), 45 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Tue Aug 21 10:42:16 2018 +0200
+++ b/NEWS	Wed Aug 22 16:55:07 2018 +0200
@@ -1,6 +1,16 @@
 Pending changes in the mainline
 ===============================
 
+
+Plugins
+-------
+
+* New primitives to access Orthanc peers from plugins
+
+
+Maintenance
+-----------
+
 * New configuration option: "HttpVerbose" to debug outgoing HTTP connections
 * Fix incoming DICOM C-Store filtering for JPEG-LS transfer syntaxes
 * Fix OrthancPluginHttpClient() to return the HTTP status on errors
--- a/OrthancServer/LuaScripting.cpp	Tue Aug 21 10:42:16 2018 +0200
+++ b/OrthancServer/LuaScripting.cpp	Wed Aug 22 16:55:07 2018 +0200
@@ -456,9 +456,15 @@
       std::string name = parameters["Peer"].asString();
 
       WebServiceParameters peer;
-      Configuration::GetOrthancPeer(peer, name);
-
-      return lock.AddStorePeerOperation(peer);
+      if (Configuration::GetOrthancPeer(peer, name))
+      {
+        return lock.AddStorePeerOperation(peer);
+      }
+      else
+      {
+        LOG(ERROR) << "No peer with symbolic name: " << name;
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
     }
 
     if (operation == "modify")
--- a/OrthancServer/OrthancInitialization.cpp	Tue Aug 21 10:42:16 2018 +0200
+++ b/OrthancServer/OrthancInitialization.cpp	Wed Aug 22 16:55:07 2018 +0200
@@ -608,16 +608,14 @@
   }
 
 
-
-  void Configuration::GetOrthancPeer(WebServiceParameters& peer,
+  bool Configuration::GetOrthancPeer(WebServiceParameters& peer,
                                      const std::string& name)
   {
     boost::recursive_mutex::scoped_lock lock(globalMutex_);
 
     if (!configuration_.isMember("OrthancPeers"))
     {
-      LOG(ERROR) << "No peer with symbolic name: " << name;
-      throw OrthancException(ErrorCode_InexistentItem);
+      return false;
     }
 
     try
@@ -626,11 +624,13 @@
       if (modalities.type() != Json::objectValue ||
           !modalities.isMember(name))
       {
-        LOG(ERROR) << "No peer with symbolic name: " << name;
-        throw OrthancException(ErrorCode_InexistentItem);
+        return false;
       }
-
-      peer.FromJson(modalities[name]);
+      else
+      {
+        peer.FromJson(modalities[name]);
+        return true;
+      }
     }
     catch (OrthancException&)
     {
--- a/OrthancServer/OrthancInitialization.h	Tue Aug 21 10:42:16 2018 +0200
+++ b/OrthancServer/OrthancInitialization.h	Wed Aug 22 16:55:07 2018 +0200
@@ -78,7 +78,7 @@
     static bool LookupDicomModalityUsingAETitle(RemoteModalityParameters& modality,
                                                 const std::string& aet);
 
-    static void GetOrthancPeer(WebServiceParameters& peer,
+    static bool GetOrthancPeer(WebServiceParameters& peer,
                                const std::string& name);
 
     static void GetListOfDicomModalities(std::set<std::string>& target);
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Tue Aug 21 10:42:16 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Wed Aug 22 16:55:07 2018 +0200
@@ -836,16 +836,18 @@
              it = peers.begin(); it != peers.end(); ++it)
       {
         WebServiceParameters peer;
-        Configuration::GetOrthancPeer(peer, *it);
-
-        Json::Value jsonPeer = Json::objectValue;
-        // only return the minimum information to identify the destination, do not include "security" information like passwords
-        jsonPeer["Url"] = peer.GetUrl();
-        if (!peer.GetUsername().empty())
+        
+        if (Configuration::GetOrthancPeer(peer, *it))
         {
-          jsonPeer["Username"] = peer.GetUsername();
+          Json::Value jsonPeer = Json::objectValue;
+          // only return the minimum information to identify the destination, do not include "security" information like passwords
+          jsonPeer["Url"] = peer.GetUrl();
+          if (!peer.GetUsername().empty())
+          {
+            jsonPeer["Username"] = peer.GetUsername();
+          }
+          result[*it] = jsonPeer;
         }
-        result[*it] = jsonPeer;
       }
       call.GetOutput().AnswerJson(result);
     }
@@ -886,12 +888,17 @@
     if (GetInstancesToExport(request, *job, remote, call))
     {
       WebServiceParameters peer;
-      Configuration::GetOrthancPeer(peer, remote);
-      
-      job->SetDescription("REST API");
-      job->SetPeer(peer);    
-      
-      SubmitJob(call, request, job.release());
+      if (Configuration::GetOrthancPeer(peer, remote))
+      {
+        job->SetDescription("REST API");
+        job->SetPeer(peer);    
+        SubmitJob(call, request, job.release());
+      }
+      else
+      {
+        LOG(ERROR) << "No peer with symbolic name: " << remote;
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
     }
   }
 
--- a/Plugins/Engine/OrthancPlugins.cpp	Tue Aug 21 10:42:16 2018 +0200
+++ b/Plugins/Engine/OrthancPlugins.cpp	Wed Aug 22 16:55:07 2018 +0200
@@ -248,6 +248,61 @@
         return new PluginStorageArea(callbacks_, errorDictionary_);
       }
     };
+
+
+    class OrthancPeers : public boost::noncopyable
+    {
+    private:
+      std::vector<std::string>           names_;
+      std::vector<WebServiceParameters>  parameters_;
+
+      void CheckIndex(size_t i) const
+      {
+        assert(names_.size() == parameters_.size());
+        if (i >= names_.size())
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }
+      }
+      
+    public:
+      OrthancPeers()
+      {
+        std::set<std::string> peers;
+        Configuration::GetListOfOrthancPeers(peers);
+
+        names_.reserve(peers.size());
+        parameters_.reserve(peers.size());
+        
+        for (std::set<std::string>::const_iterator
+               it = peers.begin(); it != peers.end(); ++it)
+        {
+          WebServiceParameters peer;
+          if (Configuration::GetOrthancPeer(peer, *it))
+          {
+            names_.push_back(*it);
+            parameters_.push_back(peer);
+          }
+        }
+      }
+
+      size_t GetPeersCount() const
+      {
+        return names_.size();
+      }
+
+      const std::string& GetPeerName(size_t i) const
+      {
+        CheckIndex(i);
+        return names_[i];
+      }
+
+      const WebServiceParameters& GetPeerParameters(size_t i) const
+      {
+        CheckIndex(i);
+        return parameters_[i];
+      }
+    };
   }
 
 
@@ -1832,7 +1887,7 @@
       if (p.headersKeys[i] == NULL ||
           p.headersValues[i] == NULL)
       {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
+        throw OrthancException(ErrorCode_NullPointer);
       }
 
       client.AddHeader(p.headersKeys[i], p.headersValues[i]);
@@ -1898,6 +1953,90 @@
   }
 
 
+  void OrthancPlugins::CallPeerApi(const void* parameters)
+  {
+    const _OrthancPluginCallPeerApi& p = *reinterpret_cast<const _OrthancPluginCallPeerApi*>(parameters);
+    const OrthancPeers& peers = *reinterpret_cast<const OrthancPeers*>(p.peers);
+
+    HttpClient client(peers.GetPeerParameters(p.peerIndex), p.uri);
+    client.SetConvertHeadersToLowerCase(false);
+
+    if (p.timeout != 0)
+    {
+      client.SetTimeout(p.timeout);
+    }
+
+    for (uint32_t i = 0; i < p.additionalHeadersCount; i++)
+    {
+      if (p.additionalHeadersKeys[i] == NULL ||
+          p.additionalHeadersValues[i] == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+
+      client.AddHeader(p.additionalHeadersKeys[i], p.additionalHeadersValues[i]);
+    }
+
+    switch (p.method)
+    {
+      case OrthancPluginHttpMethod_Get:
+        client.SetMethod(HttpMethod_Get);
+        break;
+
+      case OrthancPluginHttpMethod_Post:
+        client.SetMethod(HttpMethod_Post);
+        client.GetBody().assign(p.body, p.bodySize);
+        break;
+
+      case OrthancPluginHttpMethod_Put:
+        client.SetMethod(HttpMethod_Put);
+        client.GetBody().assign(p.body, p.bodySize);
+        break;
+
+      case OrthancPluginHttpMethod_Delete:
+        client.SetMethod(HttpMethod_Delete);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    std::string body;
+    HttpClient::HttpHeaders headers;
+
+    bool success = client.Apply(body, headers);
+
+    // The HTTP request has succeeded
+    *p.httpStatus = static_cast<uint16_t>(client.GetLastStatus());
+
+    if (!success)
+    {
+      HttpClient::ThrowException(client.GetLastStatus());
+    }
+
+    // Copy the HTTP headers of the answer, if the plugin requested them
+    if (p.answerHeaders != NULL)
+    {
+      Json::Value json = Json::objectValue;
+
+      for (HttpClient::HttpHeaders::const_iterator 
+             it = headers.begin(); it != headers.end(); ++it)
+      {
+        json[it->first] = it->second;
+      }
+        
+      std::string s = json.toStyledString();
+      CopyToMemoryBuffer(*p.answerHeaders, s);
+    }
+
+    // Copy the body of the answer if it makes sense
+    if (p.method != OrthancPluginHttpMethod_Delete)
+    {
+      CopyToMemoryBuffer(*p.answerBody, body);
+    }
+  }
+
+
   void OrthancPlugins::ConvertPixelFormat(const void* parameters)
   {
     const _OrthancPluginConvertPixelFormat& p = *reinterpret_cast<const _OrthancPluginConvertPixelFormat*>(parameters);
@@ -1959,7 +2098,7 @@
     {
       if (p.instanceId == NULL)
       {
-        throw OrthancException(ErrorCode_ParameterOutOfRange);
+        throw OrthancException(ErrorCode_NullPointer);
       }
 
       std::string content;
@@ -2127,7 +2266,6 @@
   }
 
 
-
   namespace
   {
     class DictionaryReadLocker
@@ -2179,7 +2317,6 @@
   }
 
 
-
   bool OrthancPlugins::InvokeSafeService(SharedLibrary& plugin,
                                          _OrthancPluginService service,
                                          const void* parameters)
@@ -2444,15 +2581,13 @@
       case _OrthancPluginService_FreeImage:
       {
         const _OrthancPluginFreeImage& p = *reinterpret_cast<const _OrthancPluginFreeImage*>(parameters);
-        if (p.image == NULL)
-        {
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-        }
-        else
+
+        if (p.image != NULL)
         {
           delete reinterpret_cast<ImageAccessor*>(p.image);
-          return true;
         }
+
+        return true;
       }
 
       case _OrthancPluginService_UncompressImage:
@@ -2624,15 +2759,12 @@
         const _OrthancPluginFreeFindMatcher& p =
           *reinterpret_cast<const _OrthancPluginFreeFindMatcher*>(parameters);
 
-        if (p.matcher == NULL)
-        {
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-        }
-        else
+        if (p.matcher != NULL)
         {
           delete reinterpret_cast<HierarchicalMatcher*>(p.matcher);
-          return true;
         }
+
+        return true;
       }
 
       case _OrthancPluginService_FindMatcherIsMatch:
@@ -2642,7 +2774,7 @@
 
         if (p.matcher == NULL)
         {
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
+          throw OrthancException(ErrorCode_NullPointer);
         }
         else
         {
@@ -2652,6 +2784,79 @@
         }
       }
 
+      case _OrthancPluginService_GetPeers:
+      {
+        const _OrthancPluginGetPeers& p =
+          *reinterpret_cast<const _OrthancPluginGetPeers*>(parameters);
+        *(p.peers) = reinterpret_cast<OrthancPluginPeers*>(new OrthancPeers);
+        return true;
+      }
+
+      case _OrthancPluginService_FreePeers:
+      {
+        const _OrthancPluginFreePeers& p =
+          *reinterpret_cast<const _OrthancPluginFreePeers*>(parameters);
+
+        if (p.peers != NULL)
+        {
+          delete reinterpret_cast<OrthancPeers*>(p.peers);
+        }
+        
+        return true;
+      }
+
+      case _OrthancPluginService_GetPeersCount:
+      {
+        const _OrthancPluginGetPeersCount& p =
+          *reinterpret_cast<const _OrthancPluginGetPeersCount*>(parameters);
+
+        if (p.peers == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
+        else
+        {
+          *(p.target) = reinterpret_cast<const OrthancPeers*>(p.peers)->GetPeersCount();
+          return true;
+        }
+      }
+
+      case _OrthancPluginService_GetPeerName:
+      {
+        const _OrthancPluginGetPeerProperty& p =
+          *reinterpret_cast<const _OrthancPluginGetPeerProperty*>(parameters);
+
+        if (p.peers == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
+        else
+        {
+          *(p.target) = reinterpret_cast<const OrthancPeers*>(p.peers)->GetPeerName(p.peerIndex).c_str();
+          return true;
+        }
+      }
+
+      case _OrthancPluginService_GetPeerUrl:
+      {
+        const _OrthancPluginGetPeerProperty& p =
+          *reinterpret_cast<const _OrthancPluginGetPeerProperty*>(parameters);
+
+        if (p.peers == NULL)
+        {
+          throw OrthancException(ErrorCode_NullPointer);
+        }
+        else
+        {
+          *(p.target) = reinterpret_cast<const OrthancPeers*>(p.peers)->GetPeerParameters(p.peerIndex).GetUrl().c_str();
+          return true;
+        }
+      }
+
+      case _OrthancPluginService_CallPeerApi:
+        CallPeerApi(parameters);
+        return true;
+
       default:
         return false;
     }
--- a/Plugins/Engine/OrthancPlugins.h	Tue Aug 21 10:42:16 2018 +0200
+++ b/Plugins/Engine/OrthancPlugins.h	Wed Aug 22 16:55:07 2018 +0200
@@ -157,6 +157,8 @@
 
     void CallHttpClient2(const void* parameters);
 
+    void CallPeerApi(const void* parameters);
+  
     void GetFontInfo(const void* parameters);
 
     void DrawText(const void* parameters);
--- a/Plugins/Include/orthanc/OrthancCPlugin.h	Tue Aug 21 10:42:16 2018 +0200
+++ b/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Aug 22 16:55:07 2018 +0200
@@ -118,7 +118,7 @@
 
 #define ORTHANC_PLUGINS_MINIMAL_MAJOR_NUMBER     1
 #define ORTHANC_PLUGINS_MINIMAL_MINOR_NUMBER     4
-#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  0
+#define ORTHANC_PLUGINS_MINIMAL_REVISION_NUMBER  2
 
 
 #if !defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)
@@ -519,6 +519,14 @@
     _OrthancPluginService_FreeFindMatcher = 7011,
     _OrthancPluginService_FindMatcherIsMatch = 7012,
 
+    /* Primitives for accessing Orthanc Peers (new in 1.4.2) */
+    _OrthancPluginService_GetPeers = 8000,
+    _OrthancPluginService_FreePeers = 8001,
+    _OrthancPluginService_GetPeersCount = 8003,
+    _OrthancPluginService_GetPeerName = 8004,
+    _OrthancPluginService_GetPeerUrl = 8005,
+    _OrthancPluginService_CallPeerApi = 8006,
+    
     _OrthancPluginService_INTERNAL = 0x7fffffff
   } _OrthancPluginService;
 
@@ -921,6 +929,14 @@
   typedef struct _OrthancPluginFindAnswers_t OrthancPluginFindMatcher;
 
 
+  
+  /**
+   * @brief Opaque structure to the set of remote Orthanc Peers that are known to the local Orthanc server.
+   * @ingroup Toolbox
+   **/
+  typedef struct _OrthancPluginPeers_t OrthancPluginPeers;
+
+
 
   /**
    * @brief Signature of a callback function that answers to a REST request.
@@ -1284,6 +1300,9 @@
    * Orthanc.
    * 
    * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param expectedMajor Expected major version.
+   * @param expectedMinor Expected minor version.
+   * @param expectedRevision Expected revision.
    * @return 1 if and only if the versions are compatible. If the
    * result is 0, the initialization of the plugin should fail.
    * @see OrthancPluginCheckVersion
@@ -5177,7 +5196,7 @@
    * @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 body The HTTP body for a POST or PUT 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
@@ -5188,6 +5207,7 @@
    * (can be <tt>NULL</tt> if no client certificate or if not using HTTPS).
    * @param pkcs11 Enable PKCS#11 client authentication for hardware security modules and smart cards.
    * @return 0 if success, or the error code if failure.
+   * @see OrthancPluginCallPeerApi()
    **/
   ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginHttpClient(
     OrthancPluginContext*       context,
@@ -5691,6 +5711,278 @@
     return context->InvokeService(context, _OrthancPluginService_RegisterIncomingHttpRequestFilter2, &params);
   }
 
+
+
+  typedef struct
+  {
+    OrthancPluginPeers**  peers;
+  } _OrthancPluginGetPeers;
+
+  /**
+   * @brief Return the list of available Orthanc peers.
+   *
+   * This function returns the parameters of the Orthanc peers that are known to
+   * the Orthanc server hosting the plugin.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @return NULL if error, or a newly allocated opaque data structure containing the peers.
+   * This structure must be freed with OrthancPluginFreePeers().
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginPeers* OrthancPluginGetPeers(
+    OrthancPluginContext*  context)
+  {
+    OrthancPluginPeers* peers = NULL;
+
+    _OrthancPluginGetPeers params;
+    memset(&params, 0, sizeof(params));
+    params.peers = &peers;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetPeers, &params) != OrthancPluginErrorCode_Success)
+    {
+      return NULL;
+    }
+    else
+    {
+      return peers;
+    }
+  }
+
+
+  typedef struct
+  {
+    OrthancPluginPeers*   peers;
+  } _OrthancPluginFreePeers;
+
+  /**
+   * @brief Free the list of available Orthanc peers.
+   *
+   * This function frees the data structure returned by OrthancPluginGetPeers().
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param peers The data structure describing the Orthanc peers.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE void  OrthancPluginFreePeers(
+    OrthancPluginContext*     context, 
+    OrthancPluginPeers* peers)
+  {
+    _OrthancPluginFreePeers params;
+    params.peers = peers;
+
+    context->InvokeService(context, _OrthancPluginService_FreePeers, &params);
+  }
+
+
+  typedef struct
+  {
+    uint32_t*                  target;
+    const OrthancPluginPeers*  peers;
+  } _OrthancPluginGetPeersCount;
+
+  /**
+   * @brief Get the number of Orthanc peers.
+   *
+   * This function returns the number of Orthanc peers.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param peers The data structure describing the Orthanc peers.
+   * @result The number of peers. 
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE uint32_t OrthancPluginGetPeersCount(
+    OrthancPluginContext*      context,
+    const OrthancPluginPeers*  peers)
+  {
+    uint32_t target = 0;
+
+    _OrthancPluginGetPeersCount params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.peers = peers;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetPeersCount, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return 0;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  typedef struct
+  {
+    const char**               target;
+    const OrthancPluginPeers*  peers;
+    uint32_t                   peerIndex;
+  } _OrthancPluginGetPeerProperty;
+
+  /**
+   * @brief Get the symbolic name of an Orthanc peer.
+   *
+   * This function returns the symbolic name of the Orthanc peer,
+   * which corresponds to the key of the "OrthancPeers" configuration
+   * option of Orthanc.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param peers The data structure describing the Orthanc peers.
+   * @param peerIndex The index of the peer of interest.
+   * This value must be lower than OrthancPluginGetPeersCount().
+   * @result The symbolic name, or NULL in the case of an error.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetPeerName(
+    OrthancPluginContext*      context,
+    const OrthancPluginPeers*  peers,
+    uint32_t                   peerIndex)
+  {
+    const char* target = NULL;
+
+    _OrthancPluginGetPeerProperty params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.peers = peers;
+    params.peerIndex = peerIndex;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetPeerName, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+  /**
+   * @brief Get the base URL of an Orthanc peer.
+   *
+   * This function returns the base URL to the REST API of some Orthanc peer.
+   *
+   * @param context The Orthanc plugin context, as received by OrthancPluginInitialize().
+   * @param peers The data structure describing the Orthanc peers.
+   * @param peerIndex The index of the peer of interest.
+   * This value must be lower than OrthancPluginGetPeersCount().
+   * @result The URL, or NULL in the case of an error.
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE const char* OrthancPluginGetPeerUrl(
+    OrthancPluginContext*      context,
+    const OrthancPluginPeers*  peers,
+    uint32_t                   peerIndex)
+  {
+    const char* target = NULL;
+
+    _OrthancPluginGetPeerProperty params;
+    memset(&params, 0, sizeof(params));
+    params.target = &target;
+    params.peers = peers;
+    params.peerIndex = peerIndex;
+
+    if (context->InvokeService(context, _OrthancPluginService_GetPeerUrl, &params) != OrthancPluginErrorCode_Success)
+    {
+      /* Error */
+      return NULL;
+    }
+    else
+    {
+      return target;
+    }
+  }
+
+
+
+  typedef struct
+  {
+    OrthancPluginMemoryBuffer*  answerBody;
+    OrthancPluginMemoryBuffer*  answerHeaders;
+    uint16_t*                   httpStatus;
+    const OrthancPluginPeers*   peers;
+    uint32_t                    peerIndex;
+    OrthancPluginHttpMethod     method;
+    const char*                 uri;
+    uint32_t                    additionalHeadersCount;
+    const char* const*          additionalHeadersKeys;
+    const char* const*          additionalHeadersValues;
+    const char*                 body;
+    uint32_t                    bodySize;
+    uint32_t                    timeout;
+  } _OrthancPluginCallPeerApi;
+
+  /**
+   * @brief Call the REST API of an Orthanc peer.
+   * 
+   * Make a REST call to the given URI in the REST API of a remote
+   * Orthanc peer. The result to the query is stored into a newly
+   * allocated memory buffer. The HTTP request will be done according
+   * to the "OrthancPeers" configuration option of Orthanc.
+   * 
+   * @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 peers The data structure describing the Orthanc peers.
+   * @param peerIndex The index of the peer of interest.
+   * This value must be lower than OrthancPluginGetPeersCount().
+   * @param method HTTP method to be used.
+   * @param uri The URI of interest in the REST API.
+   * @param additionalHeadersCount The number of HTTP headers to be added to the
+   * HTTP headers provided in the global configuration of Orthanc.
+   * @param additionalHeadersKeys Array containing the keys of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param additionalHeadersValues Array containing the values of the HTTP headers (can be <tt>NULL</tt> if no header).
+   * @param body The HTTP body for a POST or PUT request.
+   * @param bodySize The size of the body.
+   * @param timeout Timeout in seconds (0 for default timeout).
+   * @return 0 if success, or the error code if failure.
+   * @see OrthancPluginHttpClient()
+   * @ingroup Toolbox
+   **/
+  ORTHANC_PLUGIN_INLINE OrthancPluginErrorCode  OrthancPluginCallPeerApi(
+    OrthancPluginContext*       context,
+    OrthancPluginMemoryBuffer*  answerBody,
+    OrthancPluginMemoryBuffer*  answerHeaders,
+    uint16_t*                   httpStatus,
+    const OrthancPluginPeers*   peers,
+    uint32_t                    peerIndex,
+    OrthancPluginHttpMethod     method,
+    const char*                 uri,
+    uint32_t                    additionalHeadersCount,
+    const char* const*          additionalHeadersKeys,
+    const char* const*          additionalHeadersValues,
+    const char*                 body,
+    uint32_t                    bodySize,
+    uint32_t                    timeout)
+  {
+    _OrthancPluginCallPeerApi params;
+    memset(&params, 0, sizeof(params));
+
+    params.answerBody = answerBody;
+    params.answerHeaders = answerHeaders;
+    params.httpStatus = httpStatus;
+    params.peers = peers;
+    params.peerIndex = peerIndex;
+    params.method = method;
+    params.uri = uri;
+    params.additionalHeadersCount = additionalHeadersCount;
+    params.additionalHeadersKeys = additionalHeadersKeys;
+    params.additionalHeadersValues = additionalHeadersValues;
+    params.body = body;
+    params.bodySize = bodySize;
+    params.timeout = timeout;
+
+    return context->InvokeService(context, _OrthancPluginService_CallPeerApi, &params);
+  }
+  
+
 #ifdef  __cplusplus
 }
 #endif