changeset 2640:c691fcf66071 jobs

ResourceModificationJob
author Sebastien Jodogne <s.jodogne@gmail.com>
date Mon, 28 May 2018 16:30:17 +0200
parents 75a404e40323
children 3ce8863398ab
files CMakeLists.txt Core/JobsEngine/SetOfInstancesJob.cpp Core/JobsEngine/SetOfInstancesJob.h OrthancServer/DicomInstanceOrigin.cpp OrthancServer/DicomInstanceOrigin.h OrthancServer/DicomInstanceToStore.cpp OrthancServer/DicomInstanceToStore.h OrthancServer/LuaScripting.cpp OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp OrthancServer/OrthancRestApi/OrthancRestApi.cpp OrthancServer/ServerIndex.cpp OrthancServer/ServerJobs/DicomModalityStoreJob.cpp OrthancServer/ServerJobs/ModifyInstanceOperation.cpp OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp OrthancServer/main.cpp Plugins/Engine/OrthancPlugins.cpp
diffstat 16 files changed, 529 insertions(+), 218 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Mon May 28 14:39:22 2018 +0200
+++ b/CMakeLists.txt	Mon May 28 16:30:17 2018 +0200
@@ -55,6 +55,7 @@
 set(ORTHANC_SERVER_SOURCES
   OrthancServer/DatabaseWrapper.cpp
   OrthancServer/DatabaseWrapperBase.cpp
+  OrthancServer/DicomInstanceOrigin.cpp
   OrthancServer/DicomInstanceToStore.cpp
   OrthancServer/ExportedResource.cpp
   OrthancServer/LuaScripting.cpp
--- a/Core/JobsEngine/SetOfInstancesJob.cpp	Mon May 28 14:39:22 2018 +0200
+++ b/Core/JobsEngine/SetOfInstancesJob.cpp	Mon May 28 16:30:17 2018 +0200
@@ -178,6 +178,14 @@
   }
 
     
+  void SetOfInstancesJob::GetPublicContent(Json::Value& value)
+  {
+    value["Description"] = GetDescription();
+    value["InstancesCount"] = static_cast<uint32_t>(GetInstances().size());
+    value["FailedInstancesCount"] = static_cast<uint32_t>(GetFailedInstances().size());
+  }    
+
+
   void SetOfInstancesJob::GetInternalContent(Json::Value& value)
   {
     Json::Value v = Json::arrayValue;
--- a/Core/JobsEngine/SetOfInstancesJob.h	Mon May 28 14:39:22 2018 +0200
+++ b/Core/JobsEngine/SetOfInstancesJob.h	Mon May 28 16:30:17 2018 +0200
@@ -107,6 +107,8 @@
   
     virtual JobStepResult ExecuteStep();
     
+    virtual void GetPublicContent(Json::Value& value);
+    
     virtual void GetInternalContent(Json::Value& value);
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/DicomInstanceOrigin.cpp	Mon May 28 16:30:17 2018 +0200
@@ -0,0 +1,134 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PrecompiledHeadersServer.h"
+#include "DicomInstanceOrigin.h"
+
+#include "../Core/OrthancException.h"
+
+
+namespace Orthanc
+{
+  void DicomInstanceOrigin::Format(Json::Value& result) const
+  {
+    result = Json::objectValue;
+    result["RequestOrigin"] = EnumerationToString(origin_);
+
+    switch (origin_)
+    {
+      case RequestOrigin_Unknown:
+      {
+        // None of the methods "SetDicomProtocolOrigin()", "SetHttpOrigin()",
+        // "SetLuaOrigin()" or "SetPluginsOrigin()" was called!
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+
+      case RequestOrigin_DicomProtocol:
+      {
+        result["RemoteIp"] = remoteIp_;
+        result["RemoteAet"] = dicomRemoteAet_;
+        result["CalledAet"] = dicomCalledAet_;
+        break;
+      }
+
+      case RequestOrigin_RestApi:
+      {
+        result["RemoteIp"] = remoteIp_;
+        result["Username"] = httpUsername_;
+        break;
+      }
+
+      case RequestOrigin_Lua:
+      case RequestOrigin_Plugins:
+      {
+        // No additional information available for these kinds of requests
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  void DicomInstanceOrigin::SetDicomProtocolOrigin(const char* remoteIp,
+                                                   const char* remoteAet,
+                                                   const char* calledAet)
+  {
+    origin_ = RequestOrigin_DicomProtocol;
+    remoteIp_ = remoteIp;
+    dicomRemoteAet_ = remoteAet;
+    dicomCalledAet_ = calledAet;
+  }
+
+  void DicomInstanceOrigin::SetRestOrigin(const RestApiCall& call)
+  {
+    origin_ = call.GetRequestOrigin();
+
+    if (origin_ == RequestOrigin_RestApi)
+    {
+      remoteIp_ = call.GetRemoteIp();
+      httpUsername_ = call.GetUsername();
+    }
+  }
+
+  void DicomInstanceOrigin::SetHttpOrigin(const char* remoteIp,
+                                          const char* username)
+  {
+    origin_ = RequestOrigin_RestApi;
+    remoteIp_ = remoteIp;
+    httpUsername_ = username;
+  }
+
+  void DicomInstanceOrigin::SetLuaOrigin()
+  {
+    origin_ = RequestOrigin_Lua;
+  }
+
+  void DicomInstanceOrigin::SetPluginsOrigin()
+  {
+    origin_ = RequestOrigin_Plugins;
+  }
+
+  const char* DicomInstanceOrigin::GetRemoteAet() const
+  {
+    if (origin_ == RequestOrigin_DicomProtocol)
+    {
+      return dicomRemoteAet_.c_str();
+    }
+    else
+    {
+      return "";
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/DicomInstanceOrigin.h	Mon May 28 16:30:17 2018 +0200
@@ -0,0 +1,77 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * 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
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Core/RestApi/RestApiCall.h"
+
+namespace Orthanc
+{
+  class DicomInstanceOrigin
+  {
+  private:
+    RequestOrigin origin_;
+    std::string   remoteIp_;
+    std::string   dicomRemoteAet_;
+    std::string   dicomCalledAet_;
+    std::string   httpUsername_;
+
+  public:
+    DicomInstanceOrigin() :
+      origin_(RequestOrigin_Unknown)
+    {
+    }
+
+    void SetDicomProtocolOrigin(const char* remoteIp,
+                                const char* remoteAet,
+                                const char* calledAet);
+
+    void SetRestOrigin(const RestApiCall& call);
+
+    void SetHttpOrigin(const char* remoteIp,
+                       const char* username);
+
+    void SetLuaOrigin();
+
+    void SetPluginsOrigin();
+
+    RequestOrigin GetRequestOrigin() const
+    {
+      return origin_;
+    }
+
+    const char* GetRemoteAet() const; 
+
+    void Format(Json::Value& result) const;
+  };
+}
--- a/OrthancServer/DicomInstanceToStore.cpp	Mon May 28 14:39:22 2018 +0200
+++ b/OrthancServer/DicomInstanceToStore.cpp	Mon May 28 16:30:17 2018 +0200
@@ -181,101 +181,6 @@
   }
 
 
-
-  void DicomInstanceToStore::GetOriginInformation(Json::Value& result) const
-  {
-    result = Json::objectValue;
-    result["RequestOrigin"] = EnumerationToString(origin_);
-
-    switch (origin_)
-    {
-      case RequestOrigin_Unknown:
-      {
-        // None of the methods "SetDicomProtocolOrigin()", "SetHttpOrigin()",
-        // "SetLuaOrigin()" or "SetPluginsOrigin()" was called!
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-
-      case RequestOrigin_DicomProtocol:
-      {
-        result["RemoteIp"] = remoteIp_;
-        result["RemoteAet"] = dicomRemoteAet_;
-        result["CalledAet"] = dicomCalledAet_;
-        break;
-      }
-
-      case RequestOrigin_RestApi:
-      {
-        result["RemoteIp"] = remoteIp_;
-        result["Username"] = httpUsername_;
-        break;
-      }
-
-      case RequestOrigin_Lua:
-      case RequestOrigin_Plugins:
-      {
-        // No additional information available for these kinds of requests
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-
-  void DicomInstanceToStore::SetDicomProtocolOrigin(const char* remoteIp,
-                                                    const char* remoteAet,
-                                                    const char* calledAet)
-  {
-    origin_ = RequestOrigin_DicomProtocol;
-    remoteIp_ = remoteIp;
-    dicomRemoteAet_ = remoteAet;
-    dicomCalledAet_ = calledAet;
-  }
-
-  void DicomInstanceToStore::SetRestOrigin(const RestApiCall& call)
-  {
-    origin_ = call.GetRequestOrigin();
-
-    if (origin_ == RequestOrigin_RestApi)
-    {
-      remoteIp_ = call.GetRemoteIp();
-      httpUsername_ = call.GetUsername();
-    }
-  }
-
-  void DicomInstanceToStore::SetHttpOrigin(const char* remoteIp,
-                                           const char* username)
-  {
-    origin_ = RequestOrigin_RestApi;
-    remoteIp_ = remoteIp;
-    httpUsername_ = username;
-  }
-
-  void DicomInstanceToStore::SetLuaOrigin()
-  {
-    origin_ = RequestOrigin_Lua;
-  }
-
-  void DicomInstanceToStore::SetPluginsOrigin()
-  {
-    origin_ = RequestOrigin_Plugins;
-  }
-
-  const char* DicomInstanceToStore::GetRemoteAet() const
-  {
-    if (origin_ == RequestOrigin_DicomProtocol)
-    {
-      return dicomRemoteAet_.c_str();
-    }
-    else
-    {
-      return "";
-    }
-  }
-
-
   bool DicomInstanceToStore::LookupTransferSyntax(std::string& result)
   {
     ComputeMissingInformation();
--- a/OrthancServer/DicomInstanceToStore.h	Mon May 28 14:39:22 2018 +0200
+++ b/OrthancServer/DicomInstanceToStore.h	Mon May 28 16:30:17 2018 +0200
@@ -34,9 +34,9 @@
 #pragma once
 
 #include "../Core/DicomParsing/ParsedDicomFile.h"
+#include "../Core/OrthancException.h"
+#include "DicomInstanceOrigin.h"
 #include "ServerIndex.h"
-#include "../Core/OrthancException.h"
-#include "../Core/RestApi/RestApiCall.h"
 
 namespace Orthanc
 {
@@ -139,46 +139,31 @@
       }
     };
 
-
-    SmartContainer<std::string>  buffer_;
+    DicomInstanceOrigin              origin_;
+    SmartContainer<std::string>      buffer_;
     SmartContainer<ParsedDicomFile>  parsed_;
-    SmartContainer<DicomMap>  summary_;
-    SmartContainer<Json::Value>  json_;
-
-    RequestOrigin origin_;
-    std::string remoteIp_;
-    std::string dicomRemoteAet_;
-    std::string dicomCalledAet_;
-    std::string httpUsername_;
-    ServerIndex::MetadataMap metadata_;
+    SmartContainer<DicomMap>         summary_;
+    SmartContainer<Json::Value>      json_;
+    ServerIndex::MetadataMap         metadata_;
 
     void ComputeMissingInformation();
 
   public:
-    DicomInstanceToStore() : origin_(RequestOrigin_Unknown)
+    void SetOrigin(const DicomInstanceOrigin& origin)
     {
+      origin_ = origin;
     }
-
-    void SetDicomProtocolOrigin(const char* remoteIp,
-                                const char* remoteAet,
-                                const char* calledAet);
-
-    void SetRestOrigin(const RestApiCall& call);
-
-    void SetHttpOrigin(const char* remoteIp,
-                       const char* username);
-
-    void SetLuaOrigin();
-
-    void SetPluginsOrigin();
-
-    RequestOrigin GetRequestOrigin() const
+    
+    DicomInstanceOrigin& GetOrigin()
     {
       return origin_;
     }
-
-    const char* GetRemoteAet() const; 
-
+    
+    const DicomInstanceOrigin& GetOrigin() const
+    {
+      return origin_;
+    }
+    
     void SetBuffer(const std::string& dicom)
     {
       buffer_.SetConstReference(dicom);
@@ -221,8 +206,6 @@
     
     const Json::Value& GetJson();
 
-    void GetOriginInformation(Json::Value& result) const;
-
     bool LookupTransferSyntax(std::string& result);
   };
 }
--- a/OrthancServer/LuaScripting.cpp	Mon May 28 14:39:22 2018 +0200
+++ b/OrthancServer/LuaScripting.cpp	Mon May 28 16:30:17 2018 +0200
@@ -71,7 +71,7 @@
       simplifiedTags_(simplifiedTags),
       metadata_(metadata)
     {
-      instance.GetOriginInformation(origin_);
+      instance.GetOrigin().Format(origin_);
     }
 
     virtual void Apply(LuaScripting& that)
@@ -653,7 +653,7 @@
       call.PushJson(simplified);
 
       Json::Value origin;
-      instance.GetOriginInformation(origin);
+      instance.GetOrigin().Format(origin);
       call.PushJson(origin);
 
       if (!call.ExecutePredicate())
--- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Mon May 28 14:39:22 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Mon May 28 16:30:17 2018 +0200
@@ -54,14 +54,39 @@
   }
 
 
+  static int GetPriority(const Json::Value& request)
+  {
+    static const char* PRIORITY = "Priority";
+    
+    if (request.isMember(PRIORITY))
+    {
+      if (request[PRIORITY].type() == Json::intValue)
+      {
+        return request[PRIORITY].asInt();
+      }
+      else
+      {
+        LOG(ERROR) << "Field \"" << PRIORITY << "\" of a modification request should be an integer";
+        throw OrthancException(ErrorCode_BadFileFormat);        
+      }
+    }
+    else
+    {
+      return 0;   // Default priority
+    }
+  }
+
+
   static void ParseModifyRequest(DicomModification& target,
+                                 int& priority,
                                  const RestApiPostCall& call)
   {
-    // curl http://localhost:8042/series/95a6e2bf-9296e2cc-bf614e2f-22b391ee-16e010e0/modify -X POST -d '{"Replace":{"InstitutionName":"My own clinic"}}'
+    // curl http://localhost:8042/series/95a6e2bf-9296e2cc-bf614e2f-22b391ee-16e010e0/modify -X POST -d '{"Replace":{"InstitutionName":"My own clinic"},"Priority":9}'
 
     Json::Value request;
     if (call.ParseJsonRequest(request))
     {
+      priority = GetPriority(request);
       target.ParseModifyRequest(request);
     }
     else
@@ -72,6 +97,7 @@
 
 
   static void ParseAnonymizationRequest(DicomModification& target,
+                                        int& priority,
                                         RestApiPostCall& call)
   {
     // curl http://localhost:8042/instances/6e67da51-d119d6ae-c5667437-87b9a8a5-0f07c49f/anonymize -X POST -d '{"Replace":{"PatientName":"hello","0010-0020":"world"},"Keep":["StudyDescription", "SeriesDescription"],"KeepPrivateTags": true,"Remove":["Modality"]}' > Anonymized.dcm
@@ -80,6 +106,8 @@
     if (call.ParseJsonRequest(request) &&
         request.isObject())
     {
+      priority = GetPriority(request);
+
       bool patientNameReplaced;
       target.ParseAnonymizationRequest(patientNameReplaced, request);
 
@@ -110,53 +138,132 @@
   }
 
 
-  static void AnonymizeOrModifyResource(DicomModification* modificationRaw,  // Takes ownership
-                                        MetadataType metadataType,
-                                        ResourceType resourceType,
-                                        RestApiPostCall& call)
+
+  class ResourceModificationJob : public SetOfInstancesJob
   {
-    if (modificationRaw == NULL)
+  public:
+    class Output : public boost::noncopyable
     {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-    
-    std::auto_ptr<DicomModification> modification(modificationRaw);
-    
-    bool isFirst = true;
-    Json::Value result(Json::objectValue);
+    private:
+      boost::mutex  mutex_;
+      ResourceType  level_;
+      bool          isFirst_;
+      std::string   id_;
+      std::string   patientId_;
+
+    public:
+      Output(ResourceType  level) :
+        level_(level),
+        isFirst_(true)
+      {
+        if (level_ != ResourceType_Patient &&
+            level_ != ResourceType_Study &&
+            level_ != ResourceType_Series)
+        {
+          throw OrthancException(ErrorCode_ParameterOutOfRange);
+        }            
+      }
 
-    ServerContext& context = OrthancRestApi::GetContext(call);
+      ResourceType GetLevel() const
+      {
+        return level_;
+      }
+
+      void Update(DicomInstanceHasher& hasher)
+      {
+        boost::mutex::scoped_lock lock(mutex_);
+        
+        if (isFirst_)
+        {
+          switch (level_)
+          {
+            case ResourceType_Series:
+              id_ = hasher.HashSeries();
+              break;
+
+            case ResourceType_Study:
+              id_ = hasher.HashStudy();
+              break;
+
+            case ResourceType_Patient:
+              id_ = hasher.HashPatient();
+              break;
+
+            default:
+              throw OrthancException(ErrorCode_InternalError);
+          }
 
-    typedef std::list<std::string> Instances;
-    Instances instances;
-    std::string id = call.GetUriComponent("id", "");
-    context.GetIndex().GetChildInstances(instances, id);
+          patientId_ = hasher.HashPatient();
+          isFirst_ = false;
+        }
+      }
 
-    if (instances.empty())
-    {
-      return;
-    }
+      bool Format(Json::Value& target)
+      {
+        boost::mutex::scoped_lock lock(mutex_);
+        
+        if (isFirst_)
+        {
+          return false;
+        }
+        else
+        {
+          target = Json::objectValue;
+          target["Type"] = EnumerationToString(level_);
+          target["ID"] = id_;
+          target["Path"] = GetBasePath(level_, id_);
+          target["PatientID"] = patientId_;
+          return true;
+        }
+      }
 
-
-    /**
-     * Loop over all the instances of the resource.
-     **/
+      bool GetIdentifier(std::string& id)
+      {
+        boost::mutex::scoped_lock lock(mutex_);
+        
+        if (isFirst_)
+        {
+          return false;
+        }
+        else
+        {
+          id = id_;
+          return true;
+        }
+      }
+    };
+    
+  private:
+    ServerContext&                    context_;
+    std::auto_ptr<DicomModification>  modification_;
+    boost::shared_ptr<Output>         output_;
+    bool                              isAnonymization_;
+    MetadataType                      metadataType_;
+    std::string                       description_;
+    DicomInstanceOrigin               origin_;
 
-    for (Instances::const_iterator it = instances.begin(); 
-         it != instances.end(); ++it)
+  protected:
+    bool HandleInstance(const std::string& instance)
     {
-      LOG(INFO) << "Modifying instance " << *it;
+      if (modification_.get() == NULL)
+      {
+        LOG(ERROR) << "No modification was provided for this job";
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+
+      
+      LOG(INFO) << "Modifying instance in a job: " << instance;
 
       std::auto_ptr<ServerContext::DicomCacheLocker> locker;
 
       try
       {
-        locker.reset(new ServerContext::DicomCacheLocker(OrthancRestApi::GetContext(call), *it));
+        locker.reset(new ServerContext::DicomCacheLocker(context_, instance));
       }
       catch (OrthancException&)
       {
-        // This child instance has been removed in between
-        continue;
+        LOG(WARNING) << "An instance was removed after the job was issued: " << instance;
+        return false;
       }
 
 
@@ -169,10 +276,10 @@
        **/
 
       std::auto_ptr<ParsedDicomFile> modified(original.Clone(true));
-      modification->Apply(*modified);
+      modification_->Apply(*modified);
 
       DicomInstanceToStore toStore;
-      toStore.SetRestOrigin(call);
+      toStore.SetOrigin(origin_);
       toStore.SetParsedDicomFile(*modified);
 
 
@@ -182,6 +289,10 @@
        **/
 
       DicomInstanceHasher modifiedHasher = modified->GetHasher();
+      
+      MetadataType metadataType = (isAnonymization_ ?
+                                   MetadataType_AnonymizedFrom :
+                                   MetadataType_ModifiedFrom);
 
       if (originalHasher.HashSeries() != modifiedHasher.HashSeries())
       {
@@ -198,8 +309,8 @@
         toStore.AddMetadata(ResourceType_Patient, metadataType, originalHasher.HashPatient());
       }
 
-      assert(*it == originalHasher.HashInstance());
-      toStore.AddMetadata(ResourceType_Instance, metadataType, *it);
+      assert(instance == originalHasher.HashInstance());
+      toStore.AddMetadata(ResourceType_Instance, metadataType, instance);
 
 
       /**
@@ -207,9 +318,9 @@
        **/
 
       std::string modifiedInstance;
-      if (context.Store(modifiedInstance, toStore) != StoreStatus_Success)
+      if (context_.Store(modifiedInstance, toStore) != StoreStatus_Success)
       {
-        LOG(ERROR) << "Error while storing a modified instance " << *it;
+        LOG(ERROR) << "Error while storing a modified instance " << instance;
         throw OrthancException(ErrorCode_CannotStoreInstance);
       }
 
@@ -217,41 +328,127 @@
       assert(modifiedInstance == modifiedHasher.HashInstance());
 
 
-      /**
-       * Compute the JSON object that is returned by the REST call.
-       **/
-
-      if (isFirst)
+      if (output_.get() != NULL)
       {
-        std::string newId;
+        output_->Update(modifiedHasher);
+      }
 
-        switch (resourceType)
-        {
-          case ResourceType_Series:
-            newId = modifiedHasher.HashSeries();
-            break;
+      return true;
+    }
+    
+  public:
+    ResourceModificationJob(ServerContext& context) :
+      context_(context),
+      isAnonymization_(false)
+    {
+    }
 
-          case ResourceType_Study:
-            newId = modifiedHasher.HashStudy();
-            break;
-
-          case ResourceType_Patient:
-            newId = modifiedHasher.HashPatient();
-            break;
+    void SetModification(DicomModification* modification,   // Takes ownership
+                         bool isAnonymization)
+    {
+      if (modification == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+      else if (IsStarted())
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        modification_.reset(modification);
+        isAnonymization_ = isAnonymization;
+      }
+    }
 
-          default:
-            throw OrthancException(ErrorCode_InternalError);
-        }
+    void SetOutput(boost::shared_ptr<Output>& output)
+    {
+      if (IsStarted())
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        output_ = output;
+      }
+    }
 
-        result["Type"] = EnumerationToString(resourceType);
-        result["ID"] = newId;
-        result["Path"] = GetBasePath(resourceType, newId);
-        result["PatientID"] = modifiedHasher.HashPatient();
-        isFirst = false;
+    void SetOrigin(const DicomInstanceOrigin& origin)
+    {
+      if (IsStarted())
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        origin_ = origin;
       }
     }
 
-    call.GetOutput().AnswerJson(result);
+    void SetOrigin(const RestApiCall& call)
+    {
+      DicomInstanceOrigin tmp;
+      tmp.SetRestOrigin(call);      
+      SetOrigin(tmp);
+    }
+
+    virtual void ReleaseResources()
+    {
+    }
+
+    virtual void GetJobType(std::string& target)
+    {
+      target = "ResourceModification";
+    }
+
+    virtual void GetPublicContent(Json::Value& value)
+    {
+      SetOfInstancesJob::GetPublicContent(value);
+
+      value["IsAnonymization"] = isAnonymization_;
+    }
+    
+    virtual void GetInternalContent(Json::Value& value)
+    {
+      SetOfInstancesJob::GetInternalContent(value);
+
+      Json::Value tmp;
+      modification_->Serialize(tmp);
+      value["Modification"] = tmp;
+    }
+  };
+  
+
+
+  static void SubmitJob(std::auto_ptr<DicomModification>& modification,
+                        bool isAnonymization,
+                        ResourceType level,
+                        int priority,
+                        RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    std::auto_ptr<ResourceModificationJob> job(new ResourceModificationJob(context));
+    
+    boost::shared_ptr<ResourceModificationJob::Output> output(new ResourceModificationJob::Output(level));
+    job->SetModification(modification.release(), isAnonymization);
+    job->SetOutput(output);
+    job->SetOrigin(call);
+    job->SetDescription("REST API");
+
+    context.AddChildInstances(*job, call.GetUriComponent("id", ""));
+    
+    if (context.GetJobsEngine().GetRegistry().SubmitAndWait(job.release(), priority))
+    {
+      Json::Value json;
+      if (output->Format(json))
+      {
+        call.GetOutput().AnswerJson(json);
+        return;
+      }
+    }
+
+    call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
   }
 
 
@@ -261,7 +458,8 @@
     DicomModification modification;
     modification.SetAllowManualIdentifiers(true);
 
-    ParseModifyRequest(modification, call);
+    int priority;
+    ParseModifyRequest(modification, priority, call);
 
     if (modification.IsReplaced(DICOM_TAG_PATIENT_ID))
     {
@@ -289,7 +487,8 @@
     DicomModification modification;
     modification.SetAllowManualIdentifiers(true);
 
-    ParseAnonymizationRequest(modification, call);
+    int priority;
+    ParseAnonymizationRequest(modification, priority, call);
 
     AnonymizeOrModifyInstance(modification, call);
   }
@@ -299,10 +498,12 @@
   static void ModifyResource(RestApiPostCall& call)
   {
     std::auto_ptr<DicomModification> modification(new DicomModification);
-    ParseModifyRequest(*modification, call);
+
+    int priority;
+    ParseModifyRequest(*modification, priority, call);
 
     modification->SetLevel(resourceType);
-    AnonymizeOrModifyResource(modification.release(), MetadataType_ModifiedFrom, resourceType, call);
+    SubmitJob(modification, false, resourceType, priority, call);
   }
 
 
@@ -310,9 +511,11 @@
   static void AnonymizeResource(RestApiPostCall& call)
   {
     std::auto_ptr<DicomModification> modification(new DicomModification);
-    ParseAnonymizationRequest(*modification, call);
 
-    AnonymizeOrModifyResource(modification.release(), MetadataType_AnonymizedFrom, resourceType, call);
+    int priority;
+    ParseAnonymizationRequest(*modification, priority, call);
+
+    SubmitJob(modification, true, resourceType, priority, call);
   }
 
 
@@ -321,7 +524,7 @@
                                    ParsedDicomFile& dicom)
   {
     DicomInstanceToStore toStore;
-    toStore.SetRestOrigin(call);
+    toStore.GetOrigin().SetRestOrigin(call);
     toStore.SetParsedDicomFile(dicom);
 
     ServerContext& context = OrthancRestApi::GetContext(call);
--- a/OrthancServer/OrthancRestApi/OrthancRestApi.cpp	Mon May 28 14:39:22 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestApi.cpp	Mon May 28 16:30:17 2018 +0200
@@ -93,7 +93,7 @@
     std::string postData(call.GetBodyData(), call.GetBodySize());
 
     DicomInstanceToStore toStore;
-    toStore.SetRestOrigin(call);
+    toStore.GetOrigin().SetRestOrigin(call);
     toStore.SetBuffer(postData);
 
     std::string publicId;
--- a/OrthancServer/ServerIndex.cpp	Mon May 28 14:39:22 2018 +0200
+++ b/OrthancServer/ServerIndex.cpp	Mon May 28 16:30:17 2018 +0200
@@ -778,9 +778,10 @@
       // Attach the auto-computed metadata for the instance level,
       // reflecting these additions into the input metadata map
       SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_ReceptionDate, now);
-      SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_RemoteAet, instanceToStore.GetRemoteAet());
+      SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_RemoteAet,
+                          instanceToStore.GetOrigin().GetRemoteAet());
       SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_Origin, 
-                          EnumerationToString(instanceToStore.GetRequestOrigin()));
+                          EnumerationToString(instanceToStore.GetOrigin().GetRequestOrigin()));
         
       {
         std::string s;
--- a/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp	Mon May 28 14:39:22 2018 +0200
+++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp	Mon May 28 16:30:17 2018 +0200
@@ -51,6 +51,7 @@
 
   bool DicomModalityStoreJob::HandleInstance(const std::string& instance)
   {
+    assert(IsStarted());
     OpenConnection();
 
     LOG(INFO) << "Sending instance " << instance << " to modality \"" 
@@ -170,7 +171,8 @@
 
   void DicomModalityStoreJob::GetPublicContent(Json::Value& value)
   {
-    value["Description"] = GetDescription();
+    SetOfInstancesJob::GetPublicContent(value);
+    
     value["LocalAet"] = localAet_;
     value["RemoteAet"] = remote_.GetApplicationEntityTitle();
 
@@ -179,8 +181,5 @@
       value["MoveOriginatorAET"] = GetMoveOriginatorAet();
       value["MoveOriginatorID"] = GetMoveOriginatorId();
     }
-
-    value["InstancesCount"] = static_cast<uint32_t>(GetInstances().size());
-    value["FailedInstancesCount"] = static_cast<uint32_t>(GetFailedInstances().size());
   }
 }
--- a/OrthancServer/ServerJobs/ModifyInstanceOperation.cpp	Mon May 28 14:39:22 2018 +0200
+++ b/OrthancServer/ServerJobs/ModifyInstanceOperation.cpp	Mon May 28 16:30:17 2018 +0200
@@ -105,7 +105,7 @@
 
       DicomInstanceToStore toStore;
       assert(origin_ == RequestOrigin_Lua);
-      toStore.SetLuaOrigin();
+      toStore.GetOrigin().SetLuaOrigin();
       toStore.SetParsedDicomFile(*modified);
 
       // TODO other metadata
--- a/OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp	Mon May 28 14:39:22 2018 +0200
+++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp	Mon May 28 16:30:17 2018 +0200
@@ -95,12 +95,10 @@
 
   void OrthancPeerStoreJob::GetPublicContent(Json::Value& value)
   {
+    SetOfInstancesJob::GetPublicContent(value);
+    
     Json::Value v;
     peer_.ToJson(v);
     value["Peer"] = v;
-
-    value["Description"] = GetDescription();
-    value["InstancesCount"] = static_cast<uint32_t>(GetInstances().size());
-    value["FailedInstancesCount"] = static_cast<uint32_t>(GetFailedInstances().size());
   }
 }
--- a/OrthancServer/main.cpp	Mon May 28 14:39:22 2018 +0200
+++ b/OrthancServer/main.cpp	Mon May 28 16:30:17 2018 +0200
@@ -75,7 +75,7 @@
     if (dicomFile.size() > 0)
     {
       DicomInstanceToStore toStore;
-      toStore.SetDicomProtocolOrigin(remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str());
+      toStore.GetOrigin().SetDicomProtocolOrigin(remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str());
       toStore.SetBuffer(dicomFile);
       toStore.SetSummary(dicomSummary);
       toStore.SetJson(dicomJson);
--- a/Plugins/Engine/OrthancPlugins.cpp	Mon May 28 14:39:22 2018 +0200
+++ b/Plugins/Engine/OrthancPlugins.cpp	Mon May 28 16:30:17 2018 +0200
@@ -1544,7 +1544,7 @@
     switch (service)
     {
       case _OrthancPluginService_GetInstanceRemoteAet:
-        *p.resultString = instance.GetRemoteAet();
+        *p.resultString = instance.GetOrigin().GetRemoteAet();
         return;
 
       case _OrthancPluginService_GetInstanceSize:
@@ -1585,7 +1585,7 @@
       }
 
       case _OrthancPluginService_GetInstanceOrigin:   // New in Orthanc 0.9.5
-        *p.resultOrigin = Plugins::Convert(instance.GetRequestOrigin());
+        *p.resultOrigin = Plugins::Convert(instance.GetOrigin().GetRequestOrigin());
         return;
 
       default: