changeset 489:4cb232b42168

Possibility to store the definition of DICOMweb servers into the Orthanc database
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 22 Jan 2021 14:20:58 +0100
parents b0e9d55ee247
children a5e98f4af53d
files NEWS Plugin/Configuration.cpp Plugin/Configuration.h Plugin/DicomWebFormatter.cpp Plugin/DicomWebServers.cpp Plugin/DicomWebServers.h Plugin/Plugin.cpp Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h UnitTestsSources/UnitTestsMain.cpp
diffstat 10 files changed, 298 insertions(+), 54 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Mon Jan 18 16:46:17 2021 +0100
+++ b/NEWS	Fri Jan 22 14:20:58 2021 +0100
@@ -1,6 +1,8 @@
 Pending changes in the mainline
 ===============================
 
+* Possibility to store the definition of DICOMweb servers into the Orthanc database
+* New configuration option "DicomWeb.ServersInDatabase"
 * Fix compliance with DICOM >= 2016c: If no "transfer-syntax" is provided in WADO-RS
   Retrieve Instance/Series/Study, DICOM shall be transcoded to Little Endian Explicit
 
--- a/Plugin/Configuration.cpp	Mon Jan 18 16:46:17 2021 +0100
+++ b/Plugin/Configuration.cpp	Fri Jan 22 14:20:58 2021 +0100
@@ -25,6 +25,7 @@
 #include "DicomWebServers.h"
 
 #include <Compatibility.h>
+#include <Logging.h>
 #include <Toolbox.h>
 
 #include <fstream>
@@ -33,6 +34,13 @@
 #include <boost/algorithm/string/predicate.hpp>
 
 
+// Assume Latin-1 encoding by default (as in the Orthanc core)
+static Orthanc::Encoding defaultEncoding_ = Orthanc::Encoding_Latin1;
+static std::unique_ptr<OrthancPlugins::OrthancConfiguration> configuration_;
+static bool serversInDatabase_ = false;
+static const int32_t GLOBAL_PROPERTY_SERVERS = 5468;
+
+
 namespace OrthancPlugins
 {
   bool LookupHttpHeader(std::string& value,
@@ -140,7 +148,7 @@
   {
     OrthancPluginDictionaryEntry entry;
     
-    if (OrthancPluginLookupDictionary(OrthancPlugins::GetGlobalContext(), &entry, name.c_str()) == OrthancPluginErrorCode_Success)
+    if (OrthancPluginLookupDictionary(GetGlobalContext(), &entry, name.c_str()) == OrthancPluginErrorCode_Success)
     {
       target = Orthanc::DicomTag(entry.group, entry.element);
       return true;
@@ -284,16 +292,11 @@
 
   namespace Configuration
   {
-    // Assume Latin-1 encoding by default (as in the Orthanc core)
-    static Orthanc::Encoding defaultEncoding_ = Orthanc::Encoding_Latin1;
-    static std::unique_ptr<OrthancConfiguration> configuration_;
-
-
     void Initialize()
     {
       configuration_.reset(new OrthancConfiguration);
       
-      OrthancPlugins::OrthancConfiguration global;
+      OrthancConfiguration global;
       global.GetSection(*configuration_, "DicomWeb");
 
       std::string s;
@@ -302,10 +305,22 @@
         defaultEncoding_ = Orthanc::StringToEncoding(s.c_str());
       }
 
-      OrthancPlugins::OrthancConfiguration servers;
-      configuration_->GetSection(servers, "Servers");
-      OrthancPlugins::DicomWebServers::GetInstance().Load(servers.GetJson());
+      if (!configuration_->LookupBooleanValue(serversInDatabase_, "ServersInDatabase"))
+      {
+        serversInDatabase_ = false;
+      }
 
+      if (serversInDatabase_)
+      {
+        LOG(INFO) << "The DICOMweb plugin stores the DICOMweb servers in the Orthanc database";
+      }
+      else
+      {
+        LOG(INFO) << "The DICOMweb plugin reads the DICOMweb servers from the configuration file";
+      }
+
+      DicomWebServers::GetInstance().Clear();
+        
       // Check configuration during initialization
       GetMetadataMode(Orthanc::ResourceType_Study);
       GetMetadataMode(Orthanc::ResourceType_Series);
@@ -371,7 +386,7 @@
     
     std::string GetOrthancApiRoot()
     {
-      std::string root = OrthancPlugins::Configuration::GetDicomWebRoot();
+      std::string root = Configuration::GetDicomWebRoot();
       std::vector<std::string> tokens;
       Orthanc::Toolbox::TokenizeString(tokens, root, '/');
 
@@ -444,10 +459,10 @@
 
 
     static bool LookupHttpHeader2(std::string& value,
-                                  const OrthancPlugins::HttpClient::HttpHeaders& headers,
+                                  const HttpClient::HttpHeaders& headers,
                                   const std::string& name)
     {
-      for (OrthancPlugins::HttpClient::HttpHeaders::const_iterator
+      for (HttpClient::HttpHeaders::const_iterator
              it = headers.begin(); it != headers.end(); ++it)
       {
         if (boost::iequals(it->first, name))
@@ -461,7 +476,7 @@
     }
 
 
-    std::string GetBaseUrl(const OrthancPlugins::HttpClient::HttpHeaders& headers)
+    std::string GetBaseUrl(const HttpClient::HttpHeaders& headers)
     {
       assert(configuration_.get() != NULL);
       std::string host = configuration_->GetStringValue("Host", "");
@@ -521,7 +536,7 @@
 
     std::string GetBaseUrl(const OrthancPluginHttpRequest* request)
     {
-      OrthancPlugins::HttpClient::HttpHeaders headers;
+      HttpClient::HttpHeaders headers;
 
       std::string value;
       if (LookupHttpHeader(value, request, "forwarded"))
@@ -584,8 +599,8 @@
       }
       else
       {
-        OrthancPlugins::LogError("Unsupported return MIME type: " + accept +
-                                 ", will return DICOM+JSON");
+        LogError("Unsupported return MIME type: " + accept +
+                 ", will return DICOM+JSON");
         return false;
       }
     }
@@ -610,7 +625,7 @@
     {
       std::string accept;
 
-      if (OrthancPlugins::LookupHttpHeader(accept, request, "accept"))
+      if (LookupHttpHeader(accept, request, "accept"))
       {
         return IsXmlExpected(accept);
       }
@@ -709,5 +724,57 @@
           throw Orthanc::OrthancException(Orthanc::ErrorCode_ParameterOutOfRange);
       }
     }
+
+
+    void LoadDicomWebServers()
+    {
+      if (serversInDatabase_)
+      {
+        // New in DICOMweb 1.5
+        OrthancString property;
+        property.Assign(OrthancPluginGetGlobalProperty(
+                          GetGlobalContext(), GLOBAL_PROPERTY_SERVERS, "{}"));
+
+        if (property.GetContent() == NULL)
+        {
+          DicomWebServers::GetInstance().Clear();
+        }
+        else
+        {
+          try
+          {
+            DicomWebServers::GetInstance().UnserializeGlobalProperty(property.GetContent());
+          }
+          catch (Orthanc::OrthancException&)
+          {
+            DicomWebServers::GetInstance().Clear();
+            LOG(ERROR) << "Cannot read the DICOMweb servers from the database, no server will be defined";
+          }
+        }
+      }
+      else
+      {
+        OrthancConfiguration servers;
+        configuration_->GetSection(servers, "Servers");
+        DicomWebServers::GetInstance().LoadGlobalConfiguration(servers.GetJson());
+      }
+    }
+
+    
+    void SaveDicomWebServers()
+    {
+      if (serversInDatabase_)
+      {
+        // New in DICOMweb 1.5
+        std::string property;
+        DicomWebServers::GetInstance().SerializeGlobalProperty(property);
+
+        if (!OrthancPluginSetGlobalProperty(
+              OrthancPlugins::GetGlobalContext(), GLOBAL_PROPERTY_SERVERS, property.c_str()))
+        {
+          LOG(ERROR) << "Cannot write the DICOMweb servers into the database";
+        }
+      }
+    }
   }
 }
--- a/Plugin/Configuration.h	Mon Jan 18 16:46:17 2021 +0100
+++ b/Plugin/Configuration.h	Fri Jan 22 14:20:58 2021 +0100
@@ -133,5 +133,9 @@
 
     void GetExtrapolatedMetadataTags(std::set<Orthanc::DicomTag>& tags,
                                      Orthanc::ResourceType level);
+
+    void LoadDicomWebServers();
+
+    void SaveDicomWebServers();
   }
 }
--- a/Plugin/DicomWebFormatter.cpp	Mon Jan 18 16:46:17 2021 +0100
+++ b/Plugin/DicomWebFormatter.cpp	Fri Jan 22 14:20:58 2021 +0100
@@ -126,7 +126,7 @@
 
   DicomWebFormatter::HttpWriter::HttpWriter(OrthancPluginRestOutput* output,
                                             bool isXml) :
-    context_(OrthancPlugins::GetGlobalContext()),
+    context_(GetGlobalContext()),
     output_(output),
     isXml_(isXml),
     first_(true)
@@ -163,8 +163,7 @@
 
     std::string item;
 
-    OrthancPlugins::DicomWebFormatter::Apply(
-      item, context_, dicom, size, isXml_, mode, bulkRoot);
+    DicomWebFormatter::Apply(item, context_, dicom, size, isXml_, mode, bulkRoot);
    
     if (isXml_)
     {
--- a/Plugin/DicomWebServers.cpp	Mon Jan 18 16:46:17 2021 +0100
+++ b/Plugin/DicomWebServers.cpp	Fri Jan 22 14:20:58 2021 +0100
@@ -34,12 +34,15 @@
   {
     for (Servers::iterator it = servers_.begin(); it != servers_.end(); ++it)
     {
+      assert(it->second != NULL);
       delete it->second;
     }
+
+    servers_.clear();
   }
 
 
-  void DicomWebServers::Load(const Json::Value& servers)
+  void DicomWebServers::LoadGlobalConfiguration(const Json::Value& servers)
   {
     boost::mutex::scoped_lock lock(mutex_);
 
@@ -68,8 +71,8 @@
     }
     catch (Orthanc::OrthancException& e)
     {
-      OrthancPlugins::LogError("Exception while parsing the \"DicomWeb.Servers\" section "
-                               "of the configuration file: " + std::string(e.What()));
+      LogError("Exception while parsing the \"DicomWeb.Servers\" section "
+               "of the configuration file: " + std::string(e.What()));
       throw;
     }
 
@@ -114,6 +117,7 @@
     servers.clear();
     for (Servers::const_iterator it = servers_.begin(); it != servers_.end(); ++it)
     {
+      assert(it->second != NULL);
       servers.push_back(it->first);
     }
   }
@@ -207,7 +211,7 @@
 
 
 
-  void CallServer(OrthancPlugins::MemoryBuffer& answerBody /* out */,
+  void CallServer(MemoryBuffer& answerBody /* out */,
                   std::map<std::string, std::string>& answerHeaders /* out */,
                   const Orthanc::WebServiceParameters& server,
                   OrthancPluginHttpMethod method,
@@ -271,7 +275,7 @@
       bodySize = body.size();
     }
 
-    OrthancPluginContext* context = OrthancPlugins::GetGlobalContext();
+    OrthancPluginContext* context = GetGlobalContext();
 
     uint16_t status = 0;
     MemoryBuffer answerHeadersTmp;
@@ -370,4 +374,51 @@
       }
     }
   }
+
+
+  void DicomWebServers::SerializeGlobalProperty(std::string& target)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Json::Value json = Json::objectValue;
+
+    for (Servers::const_iterator it = servers_.begin(); it != servers_.end(); ++it)
+    {
+      assert(it->second != NULL);
+
+      Json::Value server;
+      it->second->Serialize(server, true /* advanced format */, true /* store passwords */);
+      json[it->first] = server;
+    }
+
+    Orthanc::Toolbox::WriteFastJson(target, json);
+  }
+
+
+  void DicomWebServers::UnserializeGlobalProperty(const std::string& source)
+  {
+    Json::Value json;
+    
+    if (!Orthanc::Toolbox::ReadJson(json, source) ||
+        json.type() != Json::objectValue)
+    {
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_BadFileFormat, "Cannot unserialize "
+                                      "the list of DICOMweb servers from global properties");
+    }
+
+    Clear();
+
+    std::vector<std::string> members = json.getMemberNames();
+    
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      const std::string& name = members[i];
+      
+      std::unique_ptr<Orthanc::WebServiceParameters> server(new Orthanc::WebServiceParameters);
+      server->Unserialize(json[name]);
+
+      assert(servers_.find(name) == servers_.end());
+      servers_[name] = server.release();
+    }
+  }
 }
--- a/Plugin/DicomWebServers.h	Mon Jan 18 16:46:17 2021 +0100
+++ b/Plugin/DicomWebServers.h	Fri Jan 22 14:20:58 2021 +0100
@@ -39,23 +39,23 @@
     boost::mutex  mutex_;
     Servers       servers_;
 
-    void Clear();
-
     DicomWebServers()  // Forbidden (singleton pattern)
     {
     }
 
   public:
+    ~DicomWebServers()
+    {
+      Clear();
+    }
+    
     static void UriEncode(std::string& uri,
                           const std::string& resource,
                           const std::map<std::string, std::string>& getArguments);
 
-    void Load(const Json::Value& configuration);
+    void Clear();
 
-    ~DicomWebServers()
-    {
-      Clear();
-    }
+    void LoadGlobalConfiguration(const Json::Value& configuration);
 
     static DicomWebServers& GetInstance();
 
@@ -72,10 +72,14 @@
 
     void SetServer(const std::string& name,
                    const Orthanc::WebServiceParameters& parameters);
+
+    void SerializeGlobalProperty(std::string& target);
+
+    void UnserializeGlobalProperty(const std::string& source);
   };
 
 
-  void CallServer(OrthancPlugins::MemoryBuffer& answerBody /* out */,
+  void CallServer(MemoryBuffer& answerBody /* out */,
                   std::map<std::string, std::string>& answerHeaders /* out */,
                   const Orthanc::WebServiceParameters& server,
                   OrthancPluginHttpMethod method,
--- a/Plugin/Plugin.cpp	Mon Jan 18 16:46:17 2021 +0100
+++ b/Plugin/Plugin.cpp	Fri Jan 22 14:20:58 2021 +0100
@@ -128,6 +128,8 @@
     case OrthancPluginHttpMethod_Delete:
     {
       OrthancPlugins::DicomWebServers::GetInstance().DeleteServer(request->groups[0]);
+      OrthancPlugins::Configuration::SaveDicomWebServers();
+
       std::string answer = "{}";
       OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json");
       break;
@@ -141,6 +143,8 @@
       Orthanc::WebServiceParameters parameters(body);
       
       OrthancPlugins::DicomWebServers::GetInstance().SetServer(request->groups[0], parameters);
+      OrthancPlugins::Configuration::SaveDicomWebServers();
+      
       std::string answer = "{}";
       OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json");
       break;
@@ -449,6 +453,36 @@
 #endif
 
 
+static OrthancPluginErrorCode OnChangeCallback(OrthancPluginChangeType changeType, 
+                                               OrthancPluginResourceType resourceType, 
+                                               const char *resourceId)
+{
+  try
+  {
+    switch (changeType)
+    {
+      case OrthancPluginChangeType_OrthancStarted:
+        OrthancPlugins::Configuration::LoadDicomWebServers();
+        break;
+
+      default:
+        break;
+    }
+  }
+  catch (Orthanc::OrthancException& e)
+  {
+    LOG(ERROR) << "Exception: " << e.What();
+  }
+  catch (...)
+  {
+    LOG(ERROR) << "Uncatched native exception";
+  }  
+
+  return OrthancPluginErrorCode_Success;
+}
+
+
+
 extern "C"
 {
   ORTHANC_PLUGINS_API int32_t OrthancPluginInitialize(OrthancPluginContext* context)
@@ -463,6 +497,8 @@
     Orthanc::Logging::Initialize(context);
 #endif
 
+    Orthanc::Logging::EnableInfoLevel(true);
+
     /* Check the version of the Orthanc core */
     if (OrthancPluginCheckVersion(context) == 0)
     {
@@ -538,6 +574,8 @@
         OrthancPlugins::RegisterRestCallback<RetrieveInstanceRendered>(root + "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/rendered", true);
         OrthancPlugins::RegisterRestCallback<RetrieveFrameRendered>(root + "studies/([^/]*)/series/([^/]*)/instances/([^/]*)/frames/([^/]*)/rendered", true);
 
+        OrthancPluginRegisterOnChangeCallback(context, OnChangeCallback);
+
 
         // Extend the default Orthanc Explorer with custom JavaScript for STOW client
         std::string explorer;
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp	Mon Jan 18 16:46:17 2021 +0100
+++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.cpp	Fri Jan 22 14:20:58 2021 +0100
@@ -313,6 +313,37 @@
   }
 
 
+  static bool ReadJsonInternal(Json::Value& target,
+                               const void* buffer,
+                               size_t size,
+                               bool collectComments)
+  {
+#if JSONCPP_USE_DEPRECATED == 1
+    Json::Reader reader;
+    return reader.parse(reinterpret_cast<const char*>(buffer),
+                        reinterpret_cast<const char*>(buffer) + size, target, collectComments);
+#else
+    Json::CharReaderBuilder builder;
+    builder.settings_["collectComments"] = collectComments;
+    
+    const std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
+    assert(reader.get() != NULL);
+    
+    JSONCPP_STRING err;
+    if (reader->parse(reinterpret_cast<const char*>(buffer),
+                      reinterpret_cast<const char*>(buffer) + size, &target, &err))
+    {
+      return true;
+    }
+    else
+    {
+      LogError("Cannot parse JSON: " + std::string(err));
+      return false;
+    }
+#endif
+  }
+
+
   bool ReadJson(Json::Value& target,
                 const std::string& source)
   {
@@ -324,29 +355,25 @@
                 const void* buffer,
                 size_t size)
   {
-#if JSONCPP_USE_DEPRECATED == 1
-    Json::Reader reader;
-    return reader.parse(reinterpret_cast<const char*>(buffer),
-                        reinterpret_cast<const char*>(buffer) + size, target);
-#else
-    Json::CharReaderBuilder builder;
-    const std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
-    assert(reader.get() != NULL);
-    JSONCPP_STRING err;
-    if (reader->parse(reinterpret_cast<const char*>(buffer),
-                      reinterpret_cast<const char*>(buffer) + size, &target, &err))
-    {
-      return true;
-    }
-    else
-    {
-      LogError("Cannot parse JSON: " + err);
-      return false;
-    }
-#endif
+    return ReadJsonInternal(target, buffer, size, true);
   }
   
 
+  bool ReadJsonWithoutComments(Json::Value& target,
+                               const std::string& source)
+  {
+    return ReadJsonWithoutComments(target, source.empty() ? NULL : source.c_str(), source.size());
+  }
+  
+
+  bool ReadJsonWithoutComments(Json::Value& target,
+                               const void* buffer,
+                               size_t size)
+  {
+    return ReadJsonInternal(target, buffer, size, false);
+  }
+
+
   void WriteFastJson(std::string& target,
                      const Json::Value& source)
   {
--- a/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h	Mon Jan 18 16:46:17 2021 +0100
+++ b/Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h	Fri Jan 22 14:20:58 2021 +0100
@@ -483,6 +483,13 @@
                 const void* buffer,
                 size_t size);
 
+  bool ReadJsonWithoutComments(Json::Value& target,
+                               const std::string& source);  
+
+  bool ReadJsonWithoutComments(Json::Value& target,
+                               const void* buffer,
+                               size_t size);
+
   void WriteFastJson(std::string& target,
                      const Json::Value& source);
 
--- a/UnitTestsSources/UnitTestsMain.cpp	Mon Jan 18 16:46:17 2021 +0100
+++ b/UnitTestsSources/UnitTestsMain.cpp	Fri Jan 22 14:20:58 2021 +0100
@@ -24,6 +24,7 @@
 #include <boost/algorithm/string/predicate.hpp>
 
 #include "../Plugin/Configuration.h"
+#include "../Plugin/DicomWebServers.h"
 
 using namespace OrthancPlugins;
 
@@ -61,6 +62,50 @@
 }
 
 
+TEST(DicomWebServers, Serialization)
+{
+  std::list<std::string> servers;
+  DicomWebServers::GetInstance().ListServers(servers);
+  ASSERT_TRUE(servers.empty());
+
+  {
+    std::string json;
+    DicomWebServers::GetInstance().SerializeGlobalProperty(json);
+    DicomWebServers::GetInstance().UnserializeGlobalProperty(json);
+    ASSERT_TRUE(servers.empty());
+  }
+
+  Orthanc::WebServiceParameters p;
+  p.SetUrl("http://hello/");
+  p.SetCredentials("user", "world");
+  DicomWebServers::GetInstance().SetServer("test", p);
+
+  std::string json;
+  DicomWebServers::GetInstance().SerializeGlobalProperty(json);
+
+  p.SetUrl("http://nope/");
+  p.ClearCredentials();
+  DicomWebServers::GetInstance().SetServer("nope", p);
+
+  DicomWebServers::GetInstance().ListServers(servers);
+  ASSERT_EQ(2u, servers.size());
+  
+  DicomWebServers::GetInstance().UnserializeGlobalProperty(json);
+
+  DicomWebServers::GetInstance().ListServers(servers);
+  ASSERT_EQ(1u, servers.size());
+
+  ASSERT_THROW(DicomWebServers::GetInstance().GetServer("nope"), Orthanc::OrthancException);
+  p = DicomWebServers::GetInstance().GetServer("test");
+  ASSERT_EQ("http://hello/", p.GetUrl());
+  ASSERT_EQ("user", p.GetUsername());
+
+  DicomWebServers::GetInstance().Clear();
+  DicomWebServers::GetInstance().ListServers(servers);
+  ASSERT_TRUE(servers.empty());
+}
+
+
 int main(int argc, char **argv)
 {
   ::testing::InitGoogleTest(&argc, argv);