changeset 593:0c3bc2ea0756

store series/metadata in an attachment
author Alain Mazy <am@osimis.io>
date Mon, 21 Aug 2023 15:16:23 +0200
parents afb98ea1458d
children 68aa83ffa290
files CMakeLists.txt NEWS Plugin/Configuration.cpp Plugin/Configuration.h Plugin/DicomWebFormatter.cpp Plugin/DicomWebFormatter.h Plugin/Plugin.cpp Plugin/WadoRs.cpp Plugin/WadoRs.h
diffstat 9 files changed, 139 insertions(+), 7 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Mon Aug 14 10:17:17 2023 +0200
+++ b/CMakeLists.txt	Mon Aug 21 15:16:23 2023 +0200
@@ -85,7 +85,8 @@
   set(ENABLE_PUGIXML ON)
   set(ENABLE_MODULE_JOBS OFF)
   set(USE_BOOST_ICONV ON)
-  
+  set(ENABLE_ZLIB ON)
+    
   include(${ORTHANC_FRAMEWORK_ROOT}/../Resources/CMake/OrthancFrameworkConfiguration.cmake)
   include_directories(${ORTHANC_FRAMEWORK_ROOT})
 endif()
@@ -204,6 +205,8 @@
 
 add_dependencies(OrthancDicomWeb AutogeneratedTarget)
 
+DefineSourceBasenameForTarget(OrthancDicomWeb)
+
 message("Setting the version of the library to ${ORTHANC_DICOM_WEB_VERSION}")
 
 add_definitions(-DORTHANC_DICOM_WEB_VERSION="${ORTHANC_DICOM_WEB_VERSION}")
--- a/NEWS	Mon Aug 14 10:17:17 2023 +0200
+++ b/NEWS	Mon Aug 21 15:16:23 2023 +0200
@@ -1,3 +1,11 @@
+Pending changes in the mainline
+===============================
+
+* speed improvement:
+  - Now storing the output of studies/../series/../metadata route in an attachment when in "Full" mode.
+    The json file is gzipped and stored in attachment 4301 everytime a series is stable or the first time
+    its studies/../series/../metadata route is called if the attachment does not exist yet.
+
 Version 1.14 (2023-07-05)
 =========================
 
--- a/Plugin/Configuration.cpp	Mon Aug 14 10:17:17 2023 +0200
+++ b/Plugin/Configuration.cpp	Mon Aug 21 15:16:23 2023 +0200
@@ -680,6 +680,7 @@
     MetadataMode GetMetadataMode(Orthanc::ResourceType level)
     {
       static const std::string FULL = "Full";
+      static const std::string FULL_NO_CACHE = "FullNoCache";
       static const std::string MAIN_DICOM_TAGS = "MainDicomTags";
       static const std::string EXTRAPOLATE = "Extrapolate";
       
@@ -702,6 +703,10 @@
 
       if (value == FULL)
       {
+        return MetadataMode_FullWithCache;
+      }
+      else if (value == FULL_NO_CACHE)
+      {
         return MetadataMode_Full;
       }
       else if (value == MAIN_DICOM_TAGS)
--- a/Plugin/Configuration.h	Mon Aug 14 10:17:17 2023 +0200
+++ b/Plugin/Configuration.h	Mon Aug 21 15:16:23 2023 +0200
@@ -49,6 +49,7 @@
   enum MetadataMode
   {
     MetadataMode_Full,           // Read all the DICOM instances from the storage area
+    MetadataMode_FullWithCache,  // Read all the DICOM instances from the storage area and store them in an attachment on StableSeries event
     MetadataMode_MainDicomTags,  // Only use the Orthanc database (main DICOM tags only)
     MetadataMode_Extrapolate     // Extrapolate user-specified tags from a few DICOM instances
   };
--- a/Plugin/DicomWebFormatter.cpp	Mon Aug 14 10:17:17 2023 +0200
+++ b/Plugin/DicomWebFormatter.cpp	Mon Aug 21 15:16:23 2023 +0200
@@ -165,7 +165,7 @@
     first_(true)
   {
     if (context_ == NULL ||
-        output_ == NULL)
+        (isXml_ && output_ == NULL))  // allow no output when working with Json output.
     {
       throw Orthanc::OrthancException(Orthanc::ErrorCode_NullPointer);
     }
@@ -331,6 +331,15 @@
     }
   }
 
+  void DicomWebFormatter::HttpWriter::CloseAndGetJsonOutput(std::string& target)
+  {
+    if (!isXml_)
+    {
+      jsonBuffer_.AddChunk("]");
+      
+      jsonBuffer_.Flatten(target);
+    }
+  }
 
   void DicomWebFormatter::HttpWriter::AddInstance(const DicomInstance& instance,
                                                   const std::string& bulkRoot)
--- a/Plugin/DicomWebFormatter.h	Mon Aug 14 10:17:17 2023 +0200
+++ b/Plugin/DicomWebFormatter.h	Mon Aug 21 15:16:23 2023 +0200
@@ -120,6 +120,8 @@
 
       void Send();
 
+      void CloseAndGetJsonOutput(std::string& target);
+
       void AddInstance(const DicomInstance& instance,
                        const std::string& bulkRoot);
     };
--- a/Plugin/Plugin.cpp	Mon Aug 14 10:17:17 2023 +0200
+++ b/Plugin/Plugin.cpp	Mon Aug 21 15:16:23 2023 +0200
@@ -471,6 +471,10 @@
         OrthancPlugins::Configuration::LoadDicomWebServers();
         break;
 
+      case OrthancPluginChangeType_StableSeries:
+        CacheSeriesMetadata(resourceId);
+        break;
+
       default:
         break;
     }
--- a/Plugin/WadoRs.cpp	Mon Aug 14 10:17:17 2023 +0200
+++ b/Plugin/WadoRs.cpp	Mon Aug 21 15:16:23 2023 +0200
@@ -30,11 +30,14 @@
 #include <Logging.h>
 #include <Toolbox.h>
 #include <MultiThreading/SharedMessageQueue.h>
+#include <Compression/GzipCompressor.h>
 
 #include <memory>
 #include <boost/thread/mutex.hpp>
 #include <boost/thread.hpp>
 
+static const std::string SERIES_METADATA_ATTACHMENT_ID = "4301";
+static std::string WADO_BASE_PLACEHOLDER = "$WADO_BASE_PLACEHOLDER$";
 static const char* const MAIN_DICOM_TAGS = "MainDicomTags";
 static const char* const REQUESTED_TAGS = "RequestedTags";
 static const char* const INSTANCES = "Instances";
@@ -814,6 +817,7 @@
     }
 
     case OrthancPlugins::MetadataMode_Full:
+    case OrthancPlugins::MetadataMode_FullWithCache:
     {
       const std::string bulkRoot = (wadoBase +
                                     "studies/" + studyInstanceUid +
@@ -1250,7 +1254,7 @@
           instanceToLoad->bulkRoot = (data->wadoBase +
                               "studies/" + instanceToLoad->studyInstanceUid +
                               "/series/" + instanceToLoad->seriesInstanceUid + 
-                              "/instances/" + instanceResource["MainDicomTags"]["SOPInstanceUID"].asString() + "/bulk");
+                              "/instances/" + instanceResource[MAIN_DICOM_TAGS]["SOPInstanceUID"].asString() + "/bulk");
         }
       }
 
@@ -1278,8 +1282,7 @@
   }
 }
 
-void RetrieveSeriesMetadataInternal(OrthancPluginRestOutput* output,
-                                    OrthancPlugins::DicomWebFormatter::HttpWriter& writer,
+void RetrieveSeriesMetadataInternal(OrthancPlugins::DicomWebFormatter::HttpWriter& writer,
                                     MainDicomTagsCache& cache,
                                     const OrthancPlugins::MetadataMode& mode,
                                     bool isXml,
@@ -1370,6 +1373,101 @@
   }
 }
 
+void CacheSeriesMetadataInternal(std::string& serializedSeriesMetadata,
+                                 OrthancPlugins::DicomWebFormatter::HttpWriter& writer,
+                                 MainDicomTagsCache& cache,
+                                 const std::string& studyInstanceUid, 
+                                 const std::string& seriesInstanceUid, 
+                                 const std::string& seriesOrthancId)
+{
+  Orthanc::GzipCompressor compressor;
+  std::string compressedSeriesMetadata;
+
+  // compute the series metadata with a placeholder WADO base url because, the base url might change (e.g if there are 2 Orthanc connected to the same DB)
+  RetrieveSeriesMetadataInternal(writer, cache, OrthancPlugins::MetadataMode_Full, false /* isXml */, seriesOrthancId, studyInstanceUid, seriesInstanceUid, WADO_BASE_PLACEHOLDER);
+  writer.CloseAndGetJsonOutput(serializedSeriesMetadata);
+
+  // save in attachments for future use
+  Orthanc::IBufferCompressor::Compress(compressedSeriesMetadata, compressor, serializedSeriesMetadata);
+
+  Json::Value putResult;
+  std::string attachmentUrl = "/series/" + seriesOrthancId + "/attachments/" + SERIES_METADATA_ATTACHMENT_ID;
+  if (!OrthancPlugins::RestApiPut(putResult, attachmentUrl, compressedSeriesMetadata, false))
+  {
+    LOG(WARNING) << "DicomWEB: failed to write series metadata attachment";
+  }
+
+}
+
+void CacheSeriesMetadata(const std::string& seriesOrthancId)
+{
+  const OrthancPlugins::MetadataMode mode =
+    OrthancPlugins::Configuration::GetMetadataMode(Orthanc::ResourceType_Series);
+
+  if (mode == OrthancPlugins::MetadataMode_FullWithCache)
+  {
+    LOG(INFO) << "DicomWEB: pre-computing the WADO-RS series metadata";
+
+    std::string studyInstanceUid, seriesInstanceUid;
+    
+    Json::Value result;
+    if (OrthancPlugins::RestApiGet(result, "/series/" + seriesOrthancId, false))
+    {
+      seriesInstanceUid = result[MAIN_DICOM_TAGS]["SeriesInstanceUID"].asString();
+      if (OrthancPlugins::RestApiGet(result, "/studies/" + result["ParentStudy"].asString(), false))
+      {
+        studyInstanceUid = result[MAIN_DICOM_TAGS]["StudyInstanceUID"].asString();
+
+        MainDicomTagsCache cache;
+        OrthancPlugins::DicomWebFormatter::HttpWriter writer(NULL /* output */, false /* isXml */);  // we cache only the JSON format -> no need for an HttpOutput
+
+        std::string serializedSeriesMetadataNotUsed;
+        CacheSeriesMetadataInternal(serializedSeriesMetadataNotUsed, writer, cache, studyInstanceUid, seriesInstanceUid, seriesOrthancId);
+      }
+    }
+  }
+}
+
+
+void RetrieveSeriesMetadataInternalWithCache(OrthancPlugins::DicomWebFormatter::HttpWriter& writer,
+                                             MainDicomTagsCache& cache,
+                                             const OrthancPlugins::MetadataMode& mode,
+                                             bool isXml,
+                                             const std::string& seriesOrthancId,
+                                             const std::string& studyInstanceUid,
+                                             const std::string& seriesInstanceUid,
+                                             const std::string& wadoBase)
+{
+  if (mode == OrthancPlugins::MetadataMode_FullWithCache && !isXml)
+  {
+    // check if we already have computed the series metadata and saved them in an attachment
+    std::string serializedSeriesMetadata;
+    std::string compressedSeriesMetadata;
+    Orthanc::GzipCompressor compressor;
+
+    std::string attachmentUrl = "/series/" + seriesOrthancId + "/attachments/" + SERIES_METADATA_ATTACHMENT_ID;
+
+    if (!OrthancPlugins::RestApiGetString(compressedSeriesMetadata, attachmentUrl + "/data", false))
+    {
+      CacheSeriesMetadataInternal(serializedSeriesMetadata, writer, cache, studyInstanceUid, seriesInstanceUid, seriesOrthancId);
+    }
+    else
+    {
+      Orthanc::IBufferCompressor::Uncompress(serializedSeriesMetadata, compressor, compressedSeriesMetadata);
+    }
+    
+    boost::replace_all(serializedSeriesMetadata, WADO_BASE_PLACEHOLDER, wadoBase);
+
+    writer.AddDicomWebSerializedJson(serializedSeriesMetadata.c_str(), serializedSeriesMetadata.size());
+  }
+  else
+  {
+    RetrieveSeriesMetadataInternal(writer, cache, mode, isXml, seriesOrthancId, studyInstanceUid, seriesInstanceUid, wadoBase);
+    writer.Send();
+  }
+
+}
+
 
 void RetrieveSeriesMetadata(OrthancPluginRestOutput* output,
                             const char* /*url*/,
@@ -1389,7 +1487,7 @@
   if (LocateSeries(output, seriesOrthancId, studyInstanceUid, seriesInstanceUid, request))
   {
     std::string wadoBase = OrthancPlugins::Configuration::GetBasePublicUrl(request);
-    RetrieveSeriesMetadataInternal(output, writer, cache, mode, isXml, seriesOrthancId, studyInstanceUid, seriesInstanceUid, wadoBase);
+    RetrieveSeriesMetadataInternalWithCache(writer, cache, mode, isXml, seriesOrthancId, studyInstanceUid, seriesInstanceUid, wadoBase);
   }
 
   writer.Send();
@@ -1425,7 +1523,7 @@
       std::string seriesDicomUid;
       GetChildrenIdentifiers(instances, seriesDicomUid, Orthanc::ResourceType_Series, *s);
 
-      RetrieveSeriesMetadataInternal(output, writer, cache, mode, isXml, *s, studyDicomUid, seriesDicomUid, wadoBase);
+      RetrieveSeriesMetadataInternalWithCache(writer, cache, mode, isXml, *s, studyDicomUid, seriesDicomUid, wadoBase);
     }
 
     writer.Send();
--- a/Plugin/WadoRs.h	Mon Aug 14 10:17:17 2023 +0200
+++ b/Plugin/WadoRs.h	Mon Aug 21 15:16:23 2023 +0200
@@ -63,6 +63,8 @@
                             const char* url,
                             const OrthancPluginHttpRequest* request);
 
+void CacheSeriesMetadata(const std::string& seriesOrthancId);
+
 void RetrieveInstanceMetadata(OrthancPluginRestOutput* output,
                               const char* url,
                               const OrthancPluginHttpRequest* request);