changeset 6629:71400f1c6143

merge
author Alain Mazy <am@orthanc.team>
date Wed, 18 Mar 2026 12:13:09 +0100
parents 640fc89647a6 (diff) 7f44bfb58e7c (current diff)
children 742fb41bff59 dfa66ab964c9
files NEWS
diffstat 12 files changed, 146 insertions(+), 39 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Tue Mar 17 16:07:57 2026 +0100
+++ b/NEWS	Wed Mar 18 12:13:09 2026 +0100
@@ -25,6 +25,9 @@
 * Support for OF and OD value representations in "/instances/{...}/file" with content type
   "application/dicom+json" (i.e. DICOMweb) and in "/instances/{...}/tags" (contribution by
   Yusuf Sayıta, Philips).
+* New "Content.Resources" field in "DicomModalityStore", "OrthancPeerStore" and "ResourceModification" 
+  jobs that contains JSON objects with "ID" and "Type" of each resource.  The "Content.ParentResources"
+  field that only contains the IDs is preserved for backward compatibility.
 
 Lua
 ---
@@ -1737,7 +1740,7 @@
 * API version has been upgraded to 3
 * "/modalities/{id}/query": New argument "Normalize" can be set to "false"
   to bypass the automated correction of outgoing C-FIND queries
-* Reporting of "ParentResources" in "DicomModalityStore" and "DicomModalityStore" jobs
+* Reporting of "ParentResources" in "DicomModalityStore", "OrthancPeerStore" and "ResourceModification" jobs
 
 Plugins
 -------
--- a/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp	Tue Mar 17 16:07:57 2026 +0100
+++ b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.cpp	Wed Mar 18 12:13:09 2026 +0100
@@ -27,6 +27,7 @@
 
 #include "../OrthancException.h"
 #include "../SerializationToolbox.h"
+#include "../Logging.h"
 
 #include <cassert>
 
@@ -130,12 +131,11 @@
   }
 
 
-  void SetOfInstancesJob::AddParentResource(const std::string &resource)
+  void SetOfInstancesJob::AddParentResource(const std::string &resource, ResourceType level)
   {
-    parentResources_.insert(resource);
+    parentResources_[resource] = level;
   }
 
-
   void SetOfInstancesJob::AddInstance(const std::string& instance)
   {
     AddCommand(new InstanceCommand(*this, instance));
@@ -201,28 +201,46 @@
 
   static const char* KEY_TRAILING_STEP = "TrailingStep";
   static const char* KEY_FAILED_INSTANCES = "FailedInstances";
-  static const char* KEY_PARENT_RESOURCES = "ParentResources";
+  static const char* KEY_PARENT_RESOURCES = "ParentResources"; // old style but we keep it for backward compatibility
+  static const char* KEY_RESOURCES = "Resources"; // new style with the Resource type
+
+
+  static void SerializeResources(Json::Value& target, const std::map<std::string, ResourceType>& parentResources, bool includeParentResourcesField)
+  {
+    if (!parentResources.empty())
+    {
+      target[KEY_RESOURCES] = Json::arrayValue;
+      SerializationToolbox::WriteMapOfResourcesAndTypes(target[KEY_RESOURCES], parentResources);
+
+      if (includeParentResourcesField)
+      {
+        std::set<std::string> keys;
+        for (std::map<std::string, ResourceType>::const_iterator it = parentResources.begin(); it != parentResources.end(); ++it) 
+        {
+          keys.insert(it->first);
+        }
+
+        SerializationToolbox::WriteSetOfStrings(target, keys, KEY_PARENT_RESOURCES);
+      }
+    }
+  }
 
   void SetOfInstancesJob::GetPublicContent(Json::Value& target) const
   {
     SetOfCommandsJob::GetPublicContent(target);
     target["InstancesCount"] = static_cast<uint32_t>(GetInstancesCount());
     target["FailedInstancesCount"] = static_cast<uint32_t>(failedInstances_.size());
-
-    if (!parentResources_.empty())
-    {
-      SerializationToolbox::WriteSetOfStrings(target, parentResources_, KEY_PARENT_RESOURCES);
-    }
+    
+    SerializeResources(target, parentResources_, true);
   }
 
-
   bool SetOfInstancesJob::Serialize(Json::Value& target) const 
   {
     if (SetOfCommandsJob::Serialize(target))
     {
       target[KEY_TRAILING_STEP] = hasTrailingStep_;
       SerializationToolbox::WriteSetOfStrings(target, failedInstances_, KEY_FAILED_INSTANCES);
-      SerializationToolbox::WriteSetOfStrings(target, parentResources_, KEY_PARENT_RESOURCES);
+      SerializeResources(target, parentResources_, false);
       return true;
     }
     else
@@ -237,10 +255,13 @@
   {
     SerializationToolbox::ReadSetOfStrings(failedInstances_, source, KEY_FAILED_INSTANCES);
 
-    if (source.isMember(KEY_PARENT_RESOURCES))
+    if (source.isMember(KEY_PARENT_RESOURCES) && !source.isMember(KEY_RESOURCES))
     {
-      // Backward compatibility with Orthanc <= 1.5.6
-      SerializationToolbox::ReadSetOfStrings(parentResources_, source, KEY_PARENT_RESOURCES);
+      LOG(ERROR) << "Unable to read the " << KEY_PARENT_RESOURCES << " of a job that has been saved with the previous version of Orthanc";
+    }
+    else if (source.isMember(KEY_RESOURCES) && source[KEY_RESOURCES].isArray())
+    {
+      SerializationToolbox::ReadMapOfResourcesAndTypes(parentResources_, source, KEY_RESOURCES);
     }
     
     if (source.isMember(KEY_TRAILING_STEP))
--- a/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h	Tue Mar 17 16:07:57 2026 +0100
+++ b/OrthancFramework/Sources/JobsEngine/SetOfInstancesJob.h	Wed Mar 18 12:13:09 2026 +0100
@@ -28,6 +28,7 @@
 #include "SetOfCommandsJob.h"
 
 #include <set>
+#include <map>
 
 namespace Orthanc
 {
@@ -40,7 +41,7 @@
     
     bool                   hasTrailingStep_;
     std::set<std::string>  failedInstances_;
-    std::set<std::string>  parentResources_;
+    std::map<std::string, ResourceType> parentResources_;
 
   protected:
     virtual bool HandleInstance(const std::string& instance) = 0;
@@ -57,7 +58,7 @@
 
     // Only used for reporting in the public content
     // https://groups.google.com/d/msg/orthanc-users/9GCV88GLEzw/6wAgP_PRAgAJ
-    void AddParentResource(const std::string& resource);
+    void AddParentResource(const std::string& resource, ResourceType level);
     
     void AddInstance(const std::string& instance);
 
--- a/OrthancFramework/Sources/SerializationToolbox.cpp	Tue Mar 17 16:07:57 2026 +0100
+++ b/OrthancFramework/Sources/SerializationToolbox.cpp	Wed Mar 18 12:13:09 2026 +0100
@@ -373,6 +373,35 @@
     }
   }
 
+  void SerializationToolbox::ReadMapOfResourcesAndTypes(std::map<std::string, ResourceType>& target,
+                                                        const Json::Value& value,
+                                                        const std::string& field)
+  {
+    if (!value[field].isArray())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat, "Array expected in field: " + field);
+    }
+
+    target.clear();
+    
+    for (Json::ArrayIndex i = 0; i < value[field].size(); ++i)
+    {
+      target[value[field][i]["ID"].asString()] = StringToResourceType(value[field][i]["Type"].asString().c_str());
+    }
+  }
+
+  void SerializationToolbox::WriteMapOfResourcesAndTypes(Json::Value& targetArray,
+                                                         const std::map<std::string, ResourceType>& values)
+  {
+    for (std::map<std::string, ResourceType>::const_iterator it = values.begin(); it != values.end(); ++it) 
+    {
+      Json::Value resource;
+      resource["ID"] = it->first;
+      resource["Type"] = EnumerationToString(it->second);
+      targetArray.append(resource);
+    }
+  }
+
 
   void SerializationToolbox::WriteArrayOfStrings(Json::Value& target,
                                                  const std::vector<std::string>& values,
--- a/OrthancFramework/Sources/SerializationToolbox.h	Tue Mar 17 16:07:57 2026 +0100
+++ b/OrthancFramework/Sources/SerializationToolbox.h	Wed Mar 18 12:13:09 2026 +0100
@@ -90,6 +90,10 @@
                               const Json::Value& value,
                               const std::string& field);
 
+    static void ReadMapOfResourcesAndTypes(std::map<std::string, ResourceType>& target,
+                                           const Json::Value& value,
+                                           const std::string& field);
+
     static void WriteArrayOfStrings(Json::Value& target,
                                     const std::vector<std::string>& values,
                                     const std::string& field);
@@ -105,6 +109,9 @@
     static void WriteSetOfStrings(Json::Value& targetArray,
                                   const std::set<std::string>& values);
 
+    static void WriteMapOfResourcesAndTypes(Json::Value& targetArray,
+                                            const std::map<std::string, ResourceType>& values);
+
     static void WriteSetOfTags(Json::Value& target,
                                const std::set<DicomTag>& tags,
                                const std::string& field);
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Tue Mar 17 16:07:57 2026 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Wed Mar 18 12:13:09 2026 +0100
@@ -433,10 +433,13 @@
            it = resources.begin(); it != resources.end(); ++it)
     {
       std::list<std::string> instances;
-      context.GetIndex().GetChildInstances(instances, *it);
+      ResourceType level;
+      context.GetIndex().LookupResourceType(level, *it);
+
+      context.GetIndex().GetChildInstances(instances, *it, level);
       job->AddInstances(instances);
 
-      job->AddParentResource(*it);
+      job->AddParentResource(*it, level);
     }
 
     job->PerformSanityChecks();
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp	Tue Mar 17 16:07:57 2026 +0100
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestModalities.cpp	Wed Mar 18 12:13:09 2026 +0100
@@ -1446,9 +1446,12 @@
                                "This string is not a valid Orthanc identifier: " + stripped);
       }
 
-      job.AddParentResource(stripped);  // New in Orthanc 1.5.7
+      ResourceType level;
+      context.GetIndex().LookupResourceType(level, stripped);
+
+      job.AddParentResource(stripped, level);  // New in Orthanc 1.5.7
       
-      context.AddChildInstances(job, stripped);
+      context.AddChildInstances(job, stripped, level);
 
       if (logExportedResources)
       {
--- a/OrthancServer/Sources/ServerContext.cpp	Tue Mar 17 16:07:57 2026 +0100
+++ b/OrthancServer/Sources/ServerContext.cpp	Wed Mar 18 12:13:09 2026 +0100
@@ -1810,10 +1810,11 @@
 
 
   void ServerContext::AddChildInstances(SetOfInstancesJob& job,
-                                        const std::string& publicId)
+                                        const std::string& publicId,
+                                        ResourceType level)
   {
     std::list<std::string> instances;
-    GetIndex().GetChildInstances(instances, publicId);
+    GetIndex().GetChildInstances(instances, publicId, level);
 
     job.Reserve(job.GetInstancesCount() + instances.size());
 
--- a/OrthancServer/Sources/ServerContext.h	Tue Mar 17 16:07:57 2026 +0100
+++ b/OrthancServer/Sources/ServerContext.h	Wed Mar 18 12:13:09 2026 +0100
@@ -514,7 +514,8 @@
     bool HasPlugins() const;
 
     void AddChildInstances(SetOfInstancesJob& job,
-                           const std::string& publicId);
+                           const std::string& publicId,
+                           ResourceType level);
 
     void SignalUpdatedModalities();
 
--- a/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Tue Mar 17 16:07:57 2026 +0100
+++ b/OrthancServer/Sources/ServerJobs/ResourceModificationJob.cpp	Wed Mar 18 12:13:09 2026 +0100
@@ -729,7 +729,7 @@
 
     if (GetContext().IsPatientLevelEnabled() && modificationLevel == ResourceType_Study && replacePatientMainDicomTags)
     {
-      for (std::set<std::string>::const_iterator studyId = parentResources_.begin(); studyId != parentResources_.end(); ++studyId)
+      for (std::map<std::string, ResourceType>::const_iterator it = parentResources_.begin(); it != parentResources_.end(); ++it)
       {
         // When modifying a study, you may not modify patient tags as you wish.
         // - If this is the patient's only study, you may modify all patient tags. This could be performed in 2 steps (modify the patient and then, the study) but, 
@@ -737,6 +737,7 @@
         // - If the patient already has other studies, you may only 'attach' the study to an existing patient by modifying 
         //   all patient tags from the study to match those of the target patient.
         // - Otherwise, you can't modify the patient tags
+        const std::string& studyId = it->first;
         
         std::string targetPatientId;
         if (modification_->IsReplaced(DICOM_TAG_PATIENT_ID))
@@ -746,7 +747,7 @@
         else
         {
           FindRequest request(ResourceType_Study);
-          request.SetOrthancStudyId(*studyId);
+          request.SetOrthancStudyId(studyId);
           request.SetRetrieveMainDicomTags(true);
 
           FindResponse response;
@@ -788,7 +789,7 @@
             bool targetPatientHasOtherStudies = childrenIds.size() > 1;
             if (childrenIds.size() == 1)
             {
-              targetPatientHasOtherStudies = (childrenIds.find(*studyId) == childrenIds.end());  // if the patient has one study that is not the one being modified
+              targetPatientHasOtherStudies = (childrenIds.find(studyId) == childrenIds.end());  // if the patient has one study that is not the one being modified
             }
 
             if (targetPatientHasOtherStudies)
--- a/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp	Tue Mar 17 16:07:57 2026 +0100
+++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp	Wed Mar 18 12:13:09 2026 +0100
@@ -371,6 +371,7 @@
 
   static const char* KEY_FAILED_INSTANCES = "FailedInstances";
   static const char* KEY_PARENT_RESOURCES = "ParentResources";
+  static const char* KEY_RESOURCES = "Resources";
   static const char* KEY_DESCRIPTION = "Description";
   static const char* KEY_PERMISSIVE = "Permissive";
   static const char* KEY_USER_DATA = "UserData";
@@ -382,6 +383,26 @@
   static const char* KEY_KEEP_SOURCE = "KeepSource";
   static const char* KEY_WORKERS_COUNT = "WorkersCount";
 
+  static void SerializeResources(Json::Value& target, const std::map<std::string, ResourceType>& parentResources, bool includeParentResourcesField)
+  {
+    if (!parentResources.empty())
+    {
+      target[KEY_RESOURCES] = Json::arrayValue;
+      SerializationToolbox::WriteMapOfResourcesAndTypes(target[KEY_RESOURCES], parentResources);
+
+      if (includeParentResourcesField)
+      {
+        std::set<std::string> keys;
+        for (std::map<std::string, ResourceType>::const_iterator it = parentResources.begin(); it != parentResources.end(); ++it) 
+        {
+          keys.insert(it->first);
+        }
+
+        SerializationToolbox::WriteSetOfStrings(target, keys, KEY_PARENT_RESOURCES);
+      }
+    }
+  }
+
 
   void ThreadedSetOfInstancesJob::GetPublicContent(Json::Value& target) const
   {
@@ -391,10 +412,7 @@
     target[KEY_INSTANCES_COUNT] = static_cast<uint32_t>(GetInstancesCount());
     target[KEY_FAILED_INSTANCES_COUNT] = static_cast<uint32_t>(failedInstances_.size());
 
-    if (!parentResources_.empty())
-    {
-      SerializationToolbox::WriteSetOfStrings(target, parentResources_, KEY_PARENT_RESOURCES);
-    }
+    SerializeResources(target, parentResources_, true);
   }
 
 
@@ -417,7 +435,7 @@
     
     SerializationToolbox::WriteSetOfStrings(target, instancesToProcess_, KEY_INSTANCES);
     SerializationToolbox::WriteSetOfStrings(target, failedInstances_, KEY_FAILED_INSTANCES);
-    SerializationToolbox::WriteSetOfStrings(target, parentResources_, KEY_PARENT_RESOURCES);
+    SerializeResources(target, parentResources_, false);
 
     return true;
   }
@@ -439,10 +457,29 @@
   {
     SerializationToolbox::ReadSetOfStrings(failedInstances_, source, KEY_FAILED_INSTANCES);
 
-    if (source.isMember(KEY_PARENT_RESOURCES))
+    if (source.isMember(KEY_PARENT_RESOURCES) && !source.isMember(KEY_RESOURCES))
     {
-      // Backward compatibility with Orthanc <= 1.5.6
-      SerializationToolbox::ReadSetOfStrings(parentResources_, source, KEY_PARENT_RESOURCES);
+      // Backward compatibility with Orthanc <= 1.12.11 (a KEY_PARENT_RESOURCES field with the resources ids)
+      std::set<std::string> parentResources;
+      SerializationToolbox::ReadSetOfStrings(parentResources, source, KEY_PARENT_RESOURCES);
+
+      for (std::set<std::string>::const_iterator it = parentResources.begin(); it != parentResources.end(); ++it)
+      {
+        try
+        {
+          ResourceType level;
+          context.GetIndex().LookupResourceType(level, *it);
+          parentResources_[*it] = level;
+        }
+        catch(...)
+        {
+          // ignore errors, the resource might have disappear 
+        }
+      }
+    }
+    else if (source.isMember(KEY_RESOURCES) && source[KEY_RESOURCES].isArray())
+    {
+      SerializationToolbox::ReadMapOfResourcesAndTypes(parentResources_, source, KEY_RESOURCES);
     }
     
     if (source.isMember(KEY_KEEP_SOURCE))
@@ -635,11 +672,11 @@
   }
 
 
-  void ThreadedSetOfInstancesJob::AddParentResource(const std::string &resource)
+  void ThreadedSetOfInstancesJob::AddParentResource(const std::string &resource, ResourceType level)
   {
     boost::recursive_mutex::scoped_lock lock(mutex_);
 
-    parentResources_.insert(resource);
+    parentResources_[resource] = level;
   }
 
 }
--- a/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h	Tue Mar 17 16:07:57 2026 +0100
+++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h	Wed Mar 18 12:13:09 2026 +0100
@@ -71,7 +71,7 @@
   
   protected:
     mutable boost::recursive_mutex      mutex_;
-    std::set<std::string>               parentResources_;
+    std::map<std::string, ResourceType> parentResources_;
 
   public:
     ThreadedSetOfInstancesJob(ServerContext& context,
@@ -129,7 +129,7 @@
 
     void AddInstances(const std::list<std::string>& instances);
 
-    void AddParentResource(const std::string &resource);
+    void AddParentResource(const std::string &resource, ResourceType level);
 
     bool IsPermissive() const;