changeset 6218:add00150107b

UserData in jobs
author Alain Mazy <am@orthanc.team>
date Fri, 04 Jul 2025 15:33:46 +0200
parents b56c1db69d0d
children faf965a59f5c 96c7a44a595b
files NEWS OrthancFramework/Sources/JobsEngine/IJob.h OrthancFramework/Sources/JobsEngine/JobInfo.cpp OrthancFramework/Sources/JobsEngine/JobStatus.cpp OrthancFramework/Sources/JobsEngine/JobStatus.h OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h OrthancFramework/UnitTestsSources/JobsTests.cpp OrthancServer/Plugins/Engine/PluginsJob.h OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp OrthancServer/Sources/ServerJobs/ArchiveJob.h OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h OrthancServer/UnitTestsSources/ServerJobsTests.cpp
diffstat 17 files changed, 170 insertions(+), 8 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Thu Jul 03 15:37:40 2025 +0200
+++ b/NEWS	Fri Jul 04 15:33:46 2025 +0200
@@ -6,6 +6,13 @@
 
 * Lua: new "SetStableStatus" function.
 
+REST API
+--------
+
+* When creating a job, you can now add a "UserData" field in the payload.
+  This data will travel along with the job and will be available in the 
+  /jobs/{jobId} route.
+
 
 Plugin SDK
 ----------
--- a/OrthancFramework/Sources/JobsEngine/IJob.h	Thu Jul 03 15:37:40 2025 +0200
+++ b/OrthancFramework/Sources/JobsEngine/IJob.h	Fri Jul 04 15:33:46 2025 +0200
@@ -76,5 +76,7 @@
     // This function can only be called if the job has reached its
     // "success" state
     virtual void DeleteAllOutputs() {}
+
+    virtual bool GetUserData(Json::Value& userData) const = 0;
   };
 }
--- a/OrthancFramework/Sources/JobsEngine/JobInfo.cpp	Thu Jul 03 15:37:40 2025 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobInfo.cpp	Fri Jul 04 15:33:46 2025 +0200
@@ -190,6 +190,11 @@
     target["CreationTime"] = boost::posix_time::to_iso_string(creationTime_);
     target["EffectiveRuntime"] = static_cast<double>(runtime_.total_milliseconds()) / 1000.0;
     target["Progress"] = boost::math::iround(status_.GetProgress() * 100.0f);
+    
+    if (status_.HasUserData())
+    {
+      target["UserData"] = status_.GetUserData();
+    }
 
     target["Type"] = status_.GetJobType();
     target["Content"] = status_.GetPublicContent();
--- a/OrthancFramework/Sources/JobsEngine/JobStatus.cpp	Thu Jul 03 15:37:40 2025 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobStatus.cpp	Fri Jul 04 15:33:46 2025 +0200
@@ -59,7 +59,8 @@
 
     job.GetJobType(jobType_);
     job.GetPublicContent(publicContent_);
-
+    job.GetUserData(userData_);
+    
     hasSerialized_ = job.Serialize(serialized_);
   }
 
--- a/OrthancFramework/Sources/JobsEngine/JobStatus.h	Thu Jul 03 15:37:40 2025 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobStatus.h	Fri Jul 04 15:33:46 2025 +0200
@@ -38,6 +38,7 @@
     Json::Value    serialized_;
     bool           hasSerialized_;
     std::string    details_;
+    Json::Value    userData_;
 
   public:
     JobStatus();
@@ -87,5 +88,15 @@
     {
       return details_;
     }
+
+    bool HasUserData() const
+    {
+      return !userData_.isNull();
+    }
+
+    const Json::Value& GetUserData() const
+    {
+      return userData_;
+    }
   };
 }
--- a/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Thu Jul 03 15:37:40 2025 +0200
+++ b/OrthancFramework/Sources/JobsEngine/JobsRegistry.cpp	Fri Jul 04 15:33:46 2025 +0200
@@ -43,6 +43,7 @@
   static const char* RUNTIME = "Runtime";
   static const char* ERROR_CODE = "ErrorCode";
   static const char* ERROR_DETAILS = "ErrorDetails";
+  static const char* USER_DATA = "UserData";
 
 
   class JobsRegistry::JobHandler : public boost::noncopyable
@@ -296,6 +297,13 @@
         target[ERROR_CODE] = static_cast<int>(lastStatus_.GetErrorCode());
         target[ERROR_DETAILS] = lastStatus_.GetDetails();
         
+        // New in Orthanc 1.12.9
+        Json::Value userData;
+        if (job_->GetUserData(userData))
+        {
+          target[USER_DATA] = userData;
+        }
+        
         return true;
       }
       else
--- a/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h	Thu Jul 03 15:37:40 2025 +0200
+++ b/OrthancFramework/Sources/JobsEngine/Operations/SequenceOfOperationsJob.h	Fri Jul 04 15:33:46 2025 +0200
@@ -139,5 +139,10 @@
     }
 
     void AwakeTrailingSleep();
+
+    virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
   };
 }
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp	Thu Jul 03 15:37:40 2025 +0200
+++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.cpp	Fri Jul 04 15:33:46 2025 +0200
@@ -235,7 +235,7 @@
   static const char* KEY_POSITION = "Position";
   static const char* KEY_TYPE = "Type";
   static const char* KEY_COMMANDS = "Commands";
-
+  static const char* KEY_USER_DATA = "UserData";
   
   void SetOfCommandsJob::GetPublicContent(Json::Value& value) const
   {
@@ -254,6 +254,7 @@
     target[KEY_PERMISSIVE] = permissive_;
     target[KEY_POSITION] = static_cast<unsigned int>(position_);
     target[KEY_DESCRIPTION] = description_;
+    target[KEY_USER_DATA] = userData_;
 
     target[KEY_COMMANDS] = Json::arrayValue;
     Json::Value& tmp = target[KEY_COMMANDS];
@@ -280,7 +281,13 @@
     permissive_ = SerializationToolbox::ReadBoolean(source, KEY_PERMISSIVE);
     position_ = SerializationToolbox::ReadUnsignedInteger(source, KEY_POSITION);
     description_ = SerializationToolbox::ReadString(source, KEY_DESCRIPTION);
-    
+
+    // new in 1.12.9
+    if (source.isMember(KEY_USER_DATA))
+    {
+      userData_ = source[KEY_USER_DATA];
+    }
+
     if (!source.isMember(KEY_COMMANDS) ||
         source[KEY_COMMANDS].type() != Json::arrayValue)
     {
--- a/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h	Thu Jul 03 15:37:40 2025 +0200
+++ b/OrthancFramework/Sources/JobsEngine/SetOfCommandsJob.h	Fri Jul 04 15:33:46 2025 +0200
@@ -63,6 +63,7 @@
     bool                    permissive_;
     size_t                  position_;
     std::string             description_;
+    Json::Value             userData_;
 
   public:
     SetOfCommandsJob();
@@ -116,5 +117,21 @@
     {
       return false;
     }
+
+    void SetUserData(const Json::Value& userData)
+    {
+      userData_ = userData;
+    }
+
+    virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE
+    {
+      if (!userData_.isNull())
+      {
+        userData = userData_;
+        return true;
+      }
+      return false;
+    }
+
   };
 }
--- a/OrthancFramework/UnitTestsSources/JobsTests.cpp	Thu Jul 03 15:37:40 2025 +0200
+++ b/OrthancFramework/UnitTestsSources/JobsTests.cpp	Fri Jul 04 15:33:46 2025 +0200
@@ -137,6 +137,10 @@
     {
       return false;
     }
+    virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
   };
 
 
--- a/OrthancServer/Plugins/Engine/PluginsJob.h	Thu Jul 03 15:37:40 2025 +0200
+++ b/OrthancServer/Plugins/Engine/PluginsJob.h	Fri Jul 04 15:33:46 2025 +0200
@@ -83,6 +83,12 @@
       // TODO
       return false;
     }
+
+    virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
+
   };
 }
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Thu Jul 03 15:37:40 2025 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestApi.cpp	Fri Jul 04 15:33:46 2025 +0200
@@ -324,6 +324,7 @@
   static const char* KEY_PRIORITY = "Priority";
   static const char* KEY_SYNCHRONOUS = "Synchronous";
   static const char* KEY_ASYNCHRONOUS = "Asynchronous";
+  static const char* KEY_USER_DATA = "UserData";
 
   
   bool OrthancRestApi::IsSynchronousJobRequest(bool isDefaultSynchronous,
@@ -441,6 +442,11 @@
       job->SetPermissive(false);
     }
 
+    if (body.isMember(KEY_USER_DATA))
+    {
+      job->SetUserData(body[KEY_USER_DATA]);
+    }
+
     SubmitGenericJob(call, raii.release(), isDefaultSynchronous, body);
   }
 
@@ -467,6 +473,11 @@
       job->SetPermissive(false);
     }
 
+    if (body.isMember(KEY_USER_DATA))
+    {
+      job->SetUserData(body[KEY_USER_DATA]);
+    }
+
     SubmitGenericJob(call, raii.release(), isDefaultSynchronous, body);
   }  
 
@@ -492,7 +503,9 @@
     DocumentSubmitGenericJob(call);
     call.GetDocumentation()
       .SetRequestField(KEY_PERMISSIVE, RestApiCallDocumentation::Type_Boolean,
-                       "If `true`, ignore errors during the individual steps of the job.", false);
+                       "If `true`, ignore errors during the individual steps of the job.", false)
+      .SetRequestField(KEY_USER_DATA, RestApiCallDocumentation::Type_JsonObject,
+                       "User data that will travel along with the job.", false);
   }
 
 
--- a/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp	Thu Jul 03 15:37:40 2025 +0200
+++ b/OrthancServer/Sources/OrthancRestApi/OrthancRestArchive.cpp	Fri Jul 04 15:33:46 2025 +0200
@@ -44,7 +44,8 @@
   static const char* const KEY_TRANSCODE = "Transcode";
   static const char* const KEY_LOSSY_QUALITY = "LossyQuality";
   static const char* const KEY_FILENAME = "Filename";
-  
+  static const char* const KEY_USER_DATA = "UserData";
+
   static const char* const GET_TRANSCODE = "transcode";
   static const char* const GET_LOSSY_QUALITY = "lossy-quality";
   static const char* const GET_FILENAME = "filename";
@@ -124,6 +125,7 @@
                                int& priority,                /* out */
                                unsigned int& loaderThreads,  /* out */
                                std::string& filename,        /* out */
+                               Json::Value& userData,        /* out */
                                const Json::Value& body,      /* in */
                                const bool defaultExtended    /* in */,
                                const std::string& defaultFilename /* in */)
@@ -174,6 +176,12 @@
       filename = defaultFilename;
     }
 
+    if (body.type() == Json::objectValue &&
+      body.isMember(KEY_USER_DATA) && body[KEY_USER_DATA].isString())
+    {
+      userData = body[KEY_USER_DATA].asString();
+    }
+
     {
       OrthancConfiguration::ReaderLock lock;
       loaderThreads = lock.GetConfiguration().GetUnsignedIntegerParameter(CONFIG_LOADER_THREADS, 0);  // New in Orthanc 1.10.0
@@ -548,6 +556,8 @@
                         "(including file extension)", false)
       .SetRequestField("Priority", RestApiCallDocumentation::Type_Number,
                        "In asynchronous mode, the priority of the job. The higher the value, the higher the priority.", false)
+      .SetRequestField(KEY_USER_DATA, RestApiCallDocumentation::Type_JsonObject,
+                       "In asynchronous mode, user data that will be attached to the job.", false)
       .AddAnswerType(MimeType_Zip, "In synchronous mode, the ZIP file containing the archive")
       .AddAnswerType(MimeType_Json, "In asynchronous mode, information about the job that has been submitted to "
                      "generate the archive: https://orthanc.uclouvain.be/book/users/advanced-rest.html#jobs")
@@ -593,9 +603,10 @@
       unsigned int loaderThreads;
       std::string filename;
       unsigned int lossyQuality;
+      Json::Value userData;
 
       GetJobParameters(synchronous, extended, transcode, transferSyntax, lossyQuality,
-                       priority, loaderThreads, filename, body, DEFAULT_IS_EXTENDED, "Archive.zip");
+                       priority, loaderThreads, filename, userData, body, DEFAULT_IS_EXTENDED, "Archive.zip");
       
       std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended, ResourceType_Patient));
       AddResourcesOfInterest(*job, body);
@@ -607,6 +618,7 @@
       }
       
       job->SetLoaderThreads(loaderThreads);
+      job->SetUserData(userData);
 
       SubmitJob(call.GetOutput(), context, job, priority, synchronous, filename);
     }
@@ -783,8 +795,10 @@
       unsigned int loaderThreads;
       std::string filename;
       unsigned int lossyQuality;
+      Json::Value userData;
+
       GetJobParameters(synchronous, extended, transcode, transferSyntax, lossyQuality,
-                       priority, loaderThreads, filename, body, false /* by default, not extented */, id + ".zip");
+                       priority, loaderThreads, filename, userData, body, false /* by default, not extented */, id + ".zip");
       
       std::unique_ptr<ArchiveJob> job(new ArchiveJob(context, IS_MEDIA, extended, LEVEL));
       job->AddResource(id, true, LEVEL);
@@ -796,6 +810,7 @@
       }
 
       job->SetLoaderThreads(loaderThreads);
+      job->SetUserData(userData);
 
       SubmitJob(call.GetOutput(), context, job, priority, synchronous, filename);
     }
--- a/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Thu Jul 03 15:37:40 2025 +0200
+++ b/OrthancServer/Sources/ServerJobs/ArchiveJob.h	Fri Jul 04 15:33:46 2025 +0200
@@ -58,6 +58,7 @@
     bool                                  enableExtendedSopClass_;
     std::string                           description_;
     std::string                           filename_;
+    Json::Value                           userData_;
 
     boost::shared_ptr<ZipWriterIterator>  writer_;
     size_t                                currentStep_;
@@ -137,5 +138,20 @@
     virtual bool DeleteOutput(const std::string& key) ORTHANC_OVERRIDE;
 
     virtual void DeleteAllOutputs() ORTHANC_OVERRIDE;
+
+    void SetUserData(const Json::Value& userData)
+    {
+      userData_ = userData;
+    }
+
+    virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE
+    {
+      if (!userData_.isNull())
+      {
+        userData = userData_;
+        return true;
+      }
+      return false;
+    }
   };
 }
--- a/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp	Thu Jul 03 15:37:40 2025 +0200
+++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.cpp	Fri Jul 04 15:33:46 2025 +0200
@@ -367,6 +367,7 @@
   static const char* KEY_PARENT_RESOURCES = "ParentResources";
   static const char* KEY_DESCRIPTION = "Description";
   static const char* KEY_PERMISSIVE = "Permissive";
+  static const char* KEY_USER_DATA = "UserData";
   static const char* KEY_CURRENT_STEP = "CurrentStep";
   static const char* KEY_TYPE = "Type";
   static const char* KEY_INSTANCES = "Instances";
@@ -402,6 +403,7 @@
     target[KEY_TYPE] = type;
     
     target[KEY_PERMISSIVE] = permissive_;
+    target[KEY_USER_DATA] = userData_;
     target[KEY_CURRENT_STEP] = static_cast<unsigned int>(currentStep_);
     target[KEY_DESCRIPTION] = description_;
     target[KEY_KEEP_SOURCE] = keepSource_;
@@ -456,6 +458,17 @@
     {
       currentStep_ = static_cast<ThreadedJobStep>(SerializationToolbox::ReadUnsignedInteger(source, KEY_CURRENT_STEP));
     }
+
+    if (source.isMember(KEY_PERMISSIVE))
+    {
+      SerializationToolbox::ReadBoolean(source, KEY_PERMISSIVE);
+    }
+
+    // new in 1.12.9
+    if (source.isMember(KEY_USER_DATA))
+    {
+      userData_ = source[KEY_USER_DATA];
+    }
   }
 
 
@@ -538,6 +551,27 @@
     return description_;
   }
 
+
+  void ThreadedSetOfInstancesJob::SetUserData(const Json::Value& userData)
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    userData_ = userData;
+  }
+
+  bool ThreadedSetOfInstancesJob::GetUserData(Json::Value& userData) const
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    if (!userData_.isNull())
+    {
+      userData = userData_;
+      return true;
+    }
+    return false;
+  }
+
+
   void ThreadedSetOfInstancesJob::SetErrorCode(ErrorCode errorCode)
   {
     boost::recursive_mutex::scoped_lock lock(mutex_);
--- a/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h	Thu Jul 03 15:37:40 2025 +0200
+++ b/OrthancServer/Sources/ServerJobs/ThreadedSetOfInstancesJob.h	Fri Jul 04 15:33:46 2025 +0200
@@ -62,6 +62,7 @@
     bool                    permissive_;
     ThreadedJobStep         currentStep_;
     std::string             description_;
+    Json::Value             userData_;
     size_t                  workersCount_;
 
     ServerContext&          context_;
@@ -170,5 +171,8 @@
       return context_;
     }
 
+    void SetUserData(const Json::Value& userData);
+
+    virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE;
   };
 }
--- a/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Thu Jul 03 15:37:40 2025 +0200
+++ b/OrthancServer/UnitTestsSources/ServerJobsTests.cpp	Fri Jul 04 15:33:46 2025 +0200
@@ -141,6 +141,10 @@
     {
       return false;
     }
+    virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
   };
 
 
@@ -148,7 +152,6 @@
   {
   private:
     bool   trailingStepDone_;
-    
   protected:
     virtual bool HandleInstance(const std::string& instance) ORTHANC_OVERRIDE
     {
@@ -207,6 +210,10 @@
     {
       s = "DummyInstancesJob";
     }
+    virtual bool GetUserData(Json::Value& userData) const ORTHANC_OVERRIDE
+    {
+      return false;
+    }
   };