changeset 161:2ccde9c7311b optimized-routes

added new optimized REST routes. this is a temporary work to try to speed up some routes (used by LRO). This way, we avoid another app to access the Orthanc DB and we skip the plugin SDK update for a very specific route
author Alain Mazy <alain@mazy.be>
date Fri, 10 Jul 2020 13:26:47 +0200
parents 84b6fc6f6b9b
children 6f83b74373d3
files Framework/Plugins/IndexBackend.cpp Framework/Plugins/IndexBackend.h Framework/Plugins/OptimizedRoutes.cpp Framework/Plugins/OptimizedRoutes.h Framework/PostgreSQL/PostgreSQLParameters.cpp Framework/PostgreSQL/PostgreSQLParameters.h PostgreSQL/NEWS PostgreSQL/Plugins/IndexPlugin.cpp Resources/CMake/DatabasesPluginConfiguration.cmake
diffstat 9 files changed, 267 insertions(+), 3 deletions(-) [+]
line wrap: on
line diff
--- a/Framework/Plugins/IndexBackend.cpp	Tue Jul 07 20:38:40 2020 +0200
+++ b/Framework/Plugins/IndexBackend.cpp	Fri Jul 10 13:26:47 2020 +0200
@@ -30,7 +30,7 @@
 #include <Compatibility.h>  // For std::unique_ptr<>
 #include <Logging.h>
 #include <OrthancException.h>
-
+#include <boost/algorithm/string/join.hpp>
 
 namespace OrthancDatabases
 {
@@ -1952,6 +1952,88 @@
     }
   }
 
+  void IndexBackend::GetStudyInstancesIds(std::list<std::string>& target /*out*/,
+                                          std::string& publicStudyId)
+  {
+    DatabaseManager::CachedStatement statement(
+      STATEMENT_FROM_HERE, manager_,
+          "SELECT instances.publicid FROM resources instances"
+          "  INNER JOIN resources series ON instances.parentid = series.internalid"
+          "  INNER JOIN resources studies ON series.parentid = studies.internalid"
+          "  WHERE studies.publicId = ${id}"
+    );
+
+    statement.SetReadOnly(true);
+    statement.SetParameterType("id", ValueType_Utf8String);
+
+    Dictionary args;
+    args.SetUtf8Value("id", publicStudyId);
+
+    ReadListOfStrings(target, statement, args);
+  }
+
+
+  void IndexBackend::GetStudyInstancesMetadata(std::map<std::string, std::map<int32_t, std::string>>& target /*out*/,
+                                               std::string& publicStudyId,
+                                               std::list<int32_t> metadataTypes)
+  {
+    {
+      std::string sql = "SELECT instances.publicid, metadata.type, metadata.value FROM resources instances "
+                        "LEFT JOIN metadata ON metadata.id = instances.internalid "
+                        "INNER JOIN resources series ON instances.parentid = series.internalid "
+                        "INNER JOIN resources studies ON series.parentid = studies.internalid "
+                        "  WHERE studies.publicId = ${id} ";
+
+
+      if (metadataTypes.size() != 0)
+      {
+        std::list<std::string> metadataTypesStrings;
+        for (std::list<int32_t>::const_iterator m = metadataTypes.begin(); m != metadataTypes.end(); m++)
+        {
+          metadataTypesStrings.push_back(boost::lexical_cast<std::string>(*m));
+        }
+
+        std::string metadataTypesFilter = boost::algorithm::join(metadataTypesStrings, ",");
+        sql = sql + "    AND metadata.type IN (" + metadataTypesFilter + ")";
+      }
+
+      DatabaseManager::StandaloneStatement statement(manager_, sql);
+
+      statement.SetReadOnly(true);
+      statement.SetParameterType("id", ValueType_Utf8String);
+
+      Dictionary args;
+      args.SetUtf8Value("id", publicStudyId);
+
+      statement.Execute(args);
+
+      target.clear();
+
+      if (!statement.IsDone())
+      {
+        if (statement.GetResultFieldsCount() != 3)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+
+        while (!statement.IsDone())
+        {
+          std::string instanceId = ReadString(statement, 0);
+          int32_t type = ReadInteger32(statement, 1);
+          std::string value = ReadString(statement, 2);
+
+          if (target.find(instanceId) == target.end())
+          {
+            target[instanceId] = std::map<std::int32_t, std::string>();
+          }
+          target[instanceId][type] = value;
+
+          statement.Next();
+        }
+      }
+    }
+  }
+
 
 #if defined(ORTHANC_PLUGINS_VERSION_IS_ABOVE)      // Macro introduced in 1.3.1
 #  if ORTHANC_PLUGINS_VERSION_IS_ABOVE(1, 5, 4)
--- a/Framework/Plugins/IndexBackend.h	Tue Jul 07 20:38:40 2020 +0200
+++ b/Framework/Plugins/IndexBackend.h	Fri Jul 10 13:26:47 2020 +0200
@@ -258,6 +258,15 @@
     virtual void GetChildren(std::list<std::string>& childrenPublicIds,
                              int64_t id);
 
+    // For optimized routes
+    virtual void GetStudyInstancesMetadata(std::map<std::string, std::map<int32_t, std::string>>& target /*out*/,
+                                           std::string& publicStudyId,
+                                           std::list<int32_t> metadataTypes);
+
+    // For optimized routes
+    virtual void GetStudyInstancesIds(std::list<std::string>& target /*out*/,
+                                      std::string& publicStudyId);
+
 #if ORTHANC_PLUGINS_HAS_DATABASE_CONSTRAINT == 1
     // New primitive since Orthanc 1.5.2
     virtual void LookupResources(const std::vector<Orthanc::DatabaseConstraint>& lookup,
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Plugins/OptimizedRoutes.cpp	Fri Jul 10 13:26:47 2020 +0200
@@ -0,0 +1,120 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., 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 "OptimizedRoutes.h"
+#include "../../Resources/Orthanc/Plugins/OrthancPluginCppWrapper.h"
+
+#include <string>
+#include <map>
+#include <list>
+#include <json/writer.h>
+#include <json/value.h>
+#include <boost/algorithm/string.hpp> // for boost::algorithm::split & boost::starts_with
+
+static OrthancDatabases::IndexBackend* backend_;
+static OrthancPluginContext* context_;
+
+extern "C"
+{
+
+  // handles url like /optimized-routes/studies/{id)/instances-metadata?types=1,10
+  OrthancPluginErrorCode RestGetStudiesInstancesMetadata(OrthancPluginRestOutput* output,
+                                                         const char* /* url */,
+                                                         const OrthancPluginHttpRequest* request)
+  {
+    std::list<int32_t> metadataTypes;
+    for (uint32_t i = 0; i < request->getCount; i++)
+    {
+      if (strcmp(request->getKeys[i], "types") == 0)
+      {
+        std::string getValue(request->getValues[i]);
+        std::vector<std::string> typesString;
+        boost::algorithm::split(typesString, getValue, boost::is_any_of(","));
+
+        for (size_t t = 0; t < typesString.size(); t++)
+        {
+          int32_t metadataType = boost::lexical_cast<int32_t>(typesString[t]);
+          metadataTypes.push_back(metadataType);
+        }
+      }
+    }
+
+    std::map<std::string, std::map<int32_t, std::string>> result;
+    std::string orthancId = std::string(request->groups[0]);
+    backend_->GetStudyInstancesMetadata(result,
+                                        orthancId,
+                                        metadataTypes);
+
+    Json::Value response(Json::objectValue);
+    for (std::map<std::string, std::map<int32_t, std::string>>::const_iterator itInstance = result.begin(); itInstance != result.end(); itInstance++)
+    {
+      Json::Value instanceMetadatas;
+      for (std::map<int32_t, std::string>::const_iterator itMetadata = itInstance->second.begin(); itMetadata != itInstance->second.end(); itMetadata++)
+      {
+        std::string id = boost::lexical_cast<std::string>(itMetadata->first);
+        instanceMetadatas[id] = itMetadata->second;
+      }
+
+      response[itInstance->first] = instanceMetadatas;
+    }
+
+    Json::StyledWriter writer;
+    std::string outputStr = writer.write(response);
+    OrthancPluginAnswerBuffer(context_, output, outputStr.c_str(), outputStr.size(), "application/json");
+
+    return OrthancPluginErrorCode_Success;
+  }
+
+  // handles url like /optimized-routes/studies/{id)/instances-ids
+  OrthancPluginErrorCode RestGetStudiesInstancesIds(OrthancPluginRestOutput* output,
+                                                    const char* /*url*/,
+                                                    const OrthancPluginHttpRequest* request)
+  {
+    std::list<std::string> result;
+    std::string orthancId = std::string(request->groups[0]);
+    backend_->GetStudyInstancesIds(result,
+                                   orthancId);
+
+    Json::Value response(Json::arrayValue);
+    for (std::list<std::string>::const_iterator itInstance = result.begin(); itInstance != result.end(); itInstance++)
+    {
+      response.append(*itInstance);
+    }
+
+    Json::StyledWriter writer;
+    std::string outputStr = writer.write(response);
+    OrthancPluginAnswerBuffer(context_, output, outputStr.c_str(), outputStr.size(), "application/json");
+
+    return OrthancPluginErrorCode_Success;
+  }
+}
+
+namespace OrthancDatabases {
+
+  void OptimizedRoutes::EnableOptimizedRoutes(IndexBackend *backend, OrthancPluginContext *context)
+  {
+    backend_ = backend;
+    context_ = context;
+    // Register optimized rest routes (temporary)
+    OrthancPluginRegisterRestCallbackNoLock(context_, "/optimized-routes/studies/(.*)/instances-metadata", RestGetStudiesInstancesMetadata);
+    OrthancPluginRegisterRestCallbackNoLock(context_, "/optimized-routes/studies/(.*)/instances-ids", RestGetStudiesInstancesIds);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Framework/Plugins/OptimizedRoutes.h	Fri Jul 10 13:26:47 2020 +0200
@@ -0,0 +1,38 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., 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/OrthancCPlugin.h>
+#include "IndexBackend.h"
+
+namespace OrthancDatabases
+{
+
+  // this class exposes Rest API routes that have been optimized for speed
+  // and that are usually very specific to a particular use case
+  class OptimizedRoutes
+  {
+  public:
+    static void EnableOptimizedRoutes(IndexBackend* backend, OrthancPluginContext* context);
+  };
+
+}
--- a/Framework/PostgreSQL/PostgreSQLParameters.cpp	Tue Jul 07 20:38:40 2020 +0200
+++ b/Framework/PostgreSQL/PostgreSQLParameters.cpp	Fri Jul 10 13:26:47 2020 +0200
@@ -41,6 +41,7 @@
     lock_ = true;
     maxConnectionRetries_ = 10;
     connectionRetryInterval_ = 5;
+    enabledOptimizedRoutes_ = false;
   }
 
 
@@ -92,6 +93,7 @@
     }
 
     lock_ = configuration.GetBooleanValue("Lock", true);  // Use locking by default
+    enabledOptimizedRoutes_ = configuration.GetBooleanValue("EnableOptimizedRoutes", false);
 
     maxConnectionRetries_ = configuration.GetUnsignedIntegerValue("MaximumConnectionRetries", 10);
     connectionRetryInterval_ = configuration.GetUnsignedIntegerValue("ConnectionRetryInterval", 5);
--- a/Framework/PostgreSQL/PostgreSQLParameters.h	Tue Jul 07 20:38:40 2020 +0200
+++ b/Framework/PostgreSQL/PostgreSQLParameters.h	Fri Jul 10 13:26:47 2020 +0200
@@ -42,6 +42,7 @@
     bool         lock_;
     unsigned int maxConnectionRetries_;
     unsigned int connectionRetryInterval_;
+    bool         enabledOptimizedRoutes_;
 
     void Reset();
 
@@ -124,6 +125,11 @@
       return connectionRetryInterval_;
     }
 
+    bool GetEnabledOptimizedRoutes() const
+    {
+      return enabledOptimizedRoutes_;
+    }
+
     void Format(std::string& target) const;
   };
 }
--- a/PostgreSQL/NEWS	Tue Jul 07 20:38:40 2020 +0200
+++ b/PostgreSQL/NEWS	Fri Jul 10 13:26:47 2020 +0200
@@ -5,7 +5,7 @@
 * Added "MaximumConnectionRetries" & "ConnectionRetryInterval" to configure 
   the retries when connecting to the DB at startup
 * Support of dynamic linking against the system-wide Orthanc framework library
-
+* Added new optimized Rest API routes (work-in-progress)
 
 Release 3.2 (2019-03-01)
 ========================
--- a/PostgreSQL/Plugins/IndexPlugin.cpp	Tue Jul 07 20:38:40 2020 +0200
+++ b/PostgreSQL/Plugins/IndexPlugin.cpp	Fri Jul 10 13:26:47 2020 +0200
@@ -21,12 +21,13 @@
 
 #include "PostgreSQLIndex.h"
 #include "../../Framework/Plugins/PluginInitialization.h"
+#include "../../Framework/Plugins/OptimizedRoutes.h"
 
 #include <Compatibility.h>  // For std::unique_ptr<>
 #include <Logging.h>
 
 static std::unique_ptr<OrthancDatabases::PostgreSQLIndex> backend_;
-
+static std::unique_ptr<OrthancDatabases::OptimizedRoutes> optimizedRoutes_;
 
 extern "C"
 {
@@ -66,6 +67,11 @@
 
       /* Register the PostgreSQL index into Orthanc */
       OrthancPlugins::DatabaseBackendAdapter::Register(context, *backend_);
+
+      if (parameters.GetEnabledOptimizedRoutes())
+      {
+        OrthancDatabases::OptimizedRoutes::EnableOptimizedRoutes(backend_.get(), context);
+      }
     }
     catch (Orthanc::OrthancException& e)
     {
--- a/Resources/CMake/DatabasesPluginConfiguration.cmake	Tue Jul 07 20:38:40 2020 +0200
+++ b/Resources/CMake/DatabasesPluginConfiguration.cmake	Fri Jul 10 13:26:47 2020 +0200
@@ -70,6 +70,7 @@
   ${ORTHANC_CORE_SOURCES}
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/GlobalProperties.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/IndexBackend.cpp
+  ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/OptimizedRoutes.cpp
   ${ORTHANC_DATABASES_ROOT}/Framework/Plugins/StorageBackend.cpp
   ${ORTHANC_DATABASES_ROOT}/Resources/Orthanc/Databases/DatabaseConstraint.cpp
   ${ORTHANC_DATABASES_ROOT}/Resources/Orthanc/Databases/ISqlLookupFormatter.cpp