changeset 2623:bd6e0b70e915 jobs

integration mainline->jobs
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 23 May 2018 14:28:25 +0200
parents 83ac5a05ce84 (diff) 3603a2e14592 (current diff)
children 714dcddeb65f
files NEWS Plugins/Include/orthanc/OrthancCPlugin.h
diffstat 145 files changed, 10954 insertions(+), 3628 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Wed May 23 10:08:04 2018 +0200
+++ b/CMakeLists.txt	Wed May 23 14:28:25 2018 +0200
@@ -70,14 +70,6 @@
   OrthancServer/OrthancRestApi/OrthancRestResources.cpp
   OrthancServer/OrthancRestApi/OrthancRestSystem.cpp
   OrthancServer/QueryRetrieveHandler.cpp
-  OrthancServer/Scheduler/CallSystemCommand.cpp
-  OrthancServer/Scheduler/DeleteInstanceCommand.cpp
-  OrthancServer/Scheduler/ModifyInstanceCommand.cpp
-  OrthancServer/Scheduler/ServerCommandInstance.cpp
-  OrthancServer/Scheduler/ServerJob.cpp
-  OrthancServer/Scheduler/ServerScheduler.cpp
-  OrthancServer/Scheduler/StorePeerCommand.cpp
-  OrthancServer/Scheduler/StoreScuCommand.cpp
   OrthancServer/Search/HierarchicalMatcher.cpp
   OrthancServer/Search/IFindConstraint.cpp
   OrthancServer/Search/ListConstraint.cpp
@@ -90,6 +82,15 @@
   OrthancServer/ServerContext.cpp
   OrthancServer/ServerEnumerations.cpp
   OrthancServer/ServerIndex.cpp
+  OrthancServer/ServerJobs/DeleteResourceOperation.cpp
+  OrthancServer/ServerJobs/DicomModalityStoreJob.cpp
+  OrthancServer/ServerJobs/LuaJobManager.cpp
+  OrthancServer/ServerJobs/ModifyInstanceOperation.cpp
+  OrthancServer/ServerJobs/OrthancJobUnserializer.cpp
+  OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp
+  OrthancServer/ServerJobs/StorePeerOperation.cpp
+  OrthancServer/ServerJobs/StoreScuOperation.cpp
+  OrthancServer/ServerJobs/SystemCallOperation.cpp
   OrthancServer/ServerToolbox.cpp
   OrthancServer/SliceOrdering.cpp
   )
--- a/Core/DicomNetworking/DicomUserConnection.cpp	Wed May 23 10:08:04 2018 +0200
+++ b/Core/DicomNetworking/DicomUserConnection.cpp	Wed May 23 14:28:25 2018 +0200
@@ -264,8 +264,6 @@
                                          const std::string& moveOriginatorAET,
                                          uint16_t moveOriginatorID)
   {
-    CheckIsOpen();
-
     DcmFileFormat dcmff;
     Check(dcmff.read(is, EXS_Unknown, EGL_noChange, DCM_MaxReadLength));
 
@@ -284,23 +282,36 @@
     bool isGeneric = IsGenericTransferSyntax(syntax);
 
     bool renegotiate;
-    if (isGeneric)
+
+    if (!IsOpen())
+    {
+      renegotiate = true;
+    }
+    else if (isGeneric)
     {
       // Are we making a generic-to-specific or specific-to-generic change of
       // the transfer syntax? If this is the case, renegotiate the connection.
       renegotiate = !IsGenericTransferSyntax(connection.GetPreferredTransferSyntax());
+
+      if (renegotiate)
+      {
+        LOG(INFO) << "Use of non-generic transfer syntax: the C-Store associated must be renegotiated";
+      }
     }
     else
     {
       // We are using a specific transfer syntax. Renegotiate if the
       // current connection does not match this transfer syntax.
       renegotiate = (syntax != connection.GetPreferredTransferSyntax());
+
+      if (renegotiate)
+      {
+        LOG(INFO) << "Change in the transfer syntax: the C-Store associated must be renegotiated";
+      }
     }
 
     if (renegotiate)
     {
-      LOG(INFO) << "Change in the transfer syntax: the C-Store associated must be renegotiated";
-
       if (isGeneric)
       {
         connection.ResetPreferredTransferSyntax();
@@ -313,7 +324,6 @@
 
     if (!connection.IsOpen())
     {
-      LOG(INFO) << "Renegotiating a C-Store association due to a change in the parameters";
       connection.Open();
     }
 
@@ -785,13 +795,12 @@
   }
 
 
-  DicomUserConnection::DicomUserConnection() : 
-    pimpl_(new PImpl),
-    preferredTransferSyntax_(DEFAULT_PREFERRED_TRANSFER_SYNTAX),
-    localAet_("STORESCU"),
-    remoteAet_("ANY-SCP"),
-    remoteHost_("127.0.0.1")
+  void DicomUserConnection::DefaultSetup()
   {
+    preferredTransferSyntax_ = DEFAULT_PREFERRED_TRANSFER_SYNTAX;
+    localAet_ = "STORESCU";
+    remoteAet_ = "ANY-SCP";
+    remoteHost_ = "127.0.0.1";
     remotePort_ = 104;
     manufacturer_ = ModalityManufacturer_Generic;
 
@@ -809,6 +818,24 @@
 
     ResetStorageSOPClasses();
   }
+   
+
+  DicomUserConnection::DicomUserConnection() : 
+    pimpl_(new PImpl)
+  {
+    DefaultSetup();
+  }
+  
+
+  DicomUserConnection::DicomUserConnection(const std::string& localAet,
+                                           const RemoteModalityParameters& remote) : 
+    pimpl_(new PImpl)
+  {
+    DefaultSetup();
+    SetLocalApplicationEntityTitle(localAet);
+    SetRemoteModality(remote);
+  }
+
 
   DicomUserConnection::~DicomUserConnection()
   {
@@ -1217,4 +1244,15 @@
               << seconds << " seconds (0 = no timeout)";
     defaultTimeout_ = seconds;
   }  
+
+
+  bool DicomUserConnection::IsSameAssociation(const std::string& localAet,
+                                              const RemoteModalityParameters& remote) const
+  {
+    return (localAet_ == localAet &&
+            remoteAet_ == remote.GetApplicationEntityTitle() &&
+            remoteHost_ == remote.GetHost() &&
+            remotePort_ == remote.GetPort() &&
+            manufacturer_ == remote.GetManufacturer());
+  }
 }
--- a/Core/DicomNetworking/DicomUserConnection.h	Wed May 23 10:08:04 2018 +0200
+++ b/Core/DicomNetworking/DicomUserConnection.h	Wed May 23 14:28:25 2018 +0200
@@ -77,11 +77,18 @@
 
     void CheckStorageSOPClassesInvariant() const;
 
+    void DefaultSetup();
+
   public:
     DicomUserConnection();
 
     ~DicomUserConnection();
 
+    // This constructor corresponds to behavior of the old class
+    // "ReusableDicomUserConnection", without the call to "Open()"
+    DicomUserConnection(const std::string& localAet,
+                        const RemoteModalityParameters& remote);
+
     void SetRemoteModality(const RemoteModalityParameters& parameters);
 
     void SetLocalApplicationEntityTitle(const std::string& aet);
@@ -201,5 +208,8 @@
                       ParsedDicomFile& query);
 
     static void SetDefaultTimeout(uint32_t seconds);
+
+    bool IsSameAssociation(const std::string& localAet,
+                           const RemoteModalityParameters& remote) const;
   };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/IDicomConnectionManager.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,60 @@
+/**
+ * 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 "DicomUserConnection.h"
+
+namespace Orthanc
+{
+  class IDicomConnectionManager : public boost::noncopyable
+  {
+  public:
+    virtual ~IDicomConnectionManager()
+    {
+    }
+
+    class IResource : public boost::noncopyable
+    {
+    public:
+      virtual ~IResource()
+      {
+      }
+
+      virtual DicomUserConnection& GetConnection() = 0;
+    };
+
+    virtual IResource* AcquireConnection(const std::string& localAet,
+                                         const RemoteModalityParameters& remote) = 0;
+  };
+}
--- a/Core/DicomNetworking/ReusableDicomUserConnection.cpp	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,188 +0,0 @@
-/**
- * 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 "../PrecompiledHeaders.h"
-#include "ReusableDicomUserConnection.h"
-
-#include "../Logging.h"
-#include "../OrthancException.h"
-
-namespace Orthanc
-{
-  static boost::posix_time::ptime Now()
-  {
-    return boost::posix_time::microsec_clock::local_time();
-  }
-
-  void ReusableDicomUserConnection::Open(const std::string& localAet,
-                                         const RemoteModalityParameters& remote)
-  {
-    if (connection_ != NULL &&
-        connection_->GetLocalApplicationEntityTitle() == localAet &&
-        connection_->GetRemoteApplicationEntityTitle() == remote.GetApplicationEntityTitle() &&
-        connection_->GetRemoteHost() == remote.GetHost() &&
-        connection_->GetRemotePort() == remote.GetPort() &&
-        connection_->GetRemoteManufacturer() == remote.GetManufacturer())
-    {
-      // The current connection can be reused
-      LOG(INFO) << "Reusing the previous SCU connection";
-      return;
-    }
-
-    Close();
-
-    connection_ = new DicomUserConnection();
-    connection_->SetLocalApplicationEntityTitle(localAet);
-    connection_->SetRemoteModality(remote);
-    connection_->Open();
-  }
-    
-  void ReusableDicomUserConnection::Close()
-  {
-    if (connection_ != NULL)
-    {
-      delete connection_;
-      connection_ = NULL;
-    }
-  }
-
-  void ReusableDicomUserConnection::CloseThread(ReusableDicomUserConnection* that)
-  {
-    for (;;)
-    {
-      boost::this_thread::sleep(boost::posix_time::milliseconds(100));
-      if (!that->continue_)
-      {
-        //LOG(INFO) << "Finishing the thread watching the global SCU connection";
-        return;
-      }
-
-      {
-        boost::mutex::scoped_lock lock(that->mutex_);
-        if (that->connection_ != NULL &&
-            Now() >= that->lastUse_ + that->timeBeforeClose_)
-        {
-          LOG(INFO) << "Closing the global SCU connection after timeout";
-          that->Close();
-        }
-      }
-    }
-  }
-    
-
-  ReusableDicomUserConnection::Locker::Locker(ReusableDicomUserConnection& that,
-                                              const std::string& localAet,
-                                              const RemoteModalityParameters& remote) :
-    ::Orthanc::Locker(that)
-  {
-    that.Open(localAet, remote);
-    connection_ = that.connection_;    
-  }
-
-
-  DicomUserConnection& ReusableDicomUserConnection::Locker::GetConnection()
-  {
-    if (connection_ == NULL)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    return *connection_;
-  }      
-
-  ReusableDicomUserConnection::ReusableDicomUserConnection() : 
-    connection_(NULL), 
-    timeBeforeClose_(boost::posix_time::seconds(5))  // By default, close connection after 5 seconds
-  {
-    lastUse_ = Now();
-    continue_ = true;
-    closeThread_ = boost::thread(CloseThread, this);
-  }
-
-  ReusableDicomUserConnection::~ReusableDicomUserConnection()
-  {
-    if (continue_)
-    {
-      LOG(ERROR) << "INTERNAL ERROR: ReusableDicomUserConnection::Finalize() should be invoked manually to avoid mess in the destruction order!";
-      Finalize();
-    }
-  }
-
-  void ReusableDicomUserConnection::SetMillisecondsBeforeClose(uint64_t ms)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    if (ms == 0)
-    {
-      ms = 1;
-    }
-
-    timeBeforeClose_ = boost::posix_time::milliseconds(ms);
-  }
-
-  void ReusableDicomUserConnection::Lock()
-  {
-    mutex_.lock();
-  }
-
-  void ReusableDicomUserConnection::Unlock()
-  {
-    if (connection_ != NULL &&
-        connection_->GetRemoteManufacturer() == ModalityManufacturer_StoreScp)
-    {
-      // "storescp" from DCMTK has problems when reusing a
-      // connection. Always close.
-      Close();
-    }
-
-    lastUse_ = Now();
-    mutex_.unlock();
-  }
-
-  
-  void ReusableDicomUserConnection::Finalize()
-  {
-    if (continue_)
-    {
-      continue_ = false;
-
-      if (closeThread_.joinable())
-      {
-        closeThread_.join();
-      }
-
-      Close();
-    }
-  }
-}
-
--- a/Core/DicomNetworking/ReusableDicomUserConnection.h	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,89 +0,0 @@
-/**
- * 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 "DicomUserConnection.h"
-#include "../../Core/MultiThreading/Locker.h"
-
-#include <boost/thread.hpp>
-#include <boost/date_time/posix_time/posix_time.hpp>
-
-namespace Orthanc
-{
-  class ReusableDicomUserConnection : public ILockable
-  {
-  private:
-    boost::mutex mutex_;
-    DicomUserConnection* connection_;
-    bool continue_;
-    boost::posix_time::time_duration timeBeforeClose_;
-    boost::posix_time::ptime lastUse_;
-    boost::thread closeThread_;
-
-    void Open(const std::string& localAet,
-              const RemoteModalityParameters& remote);
-    
-    void Close();
-
-    static void CloseThread(ReusableDicomUserConnection* that);
-
-  protected:
-    virtual void Lock();
-
-    virtual void Unlock();
-    
-  public:
-    class Locker : public ::Orthanc::Locker
-    {
-    private:
-      DicomUserConnection* connection_;
-
-    public:
-      Locker(ReusableDicomUserConnection& that,
-             const std::string& localAet,
-             const RemoteModalityParameters& remote);
-
-      DicomUserConnection& GetConnection();
-    };
-
-    ReusableDicomUserConnection();
-
-    virtual ~ReusableDicomUserConnection();
-
-    void SetMillisecondsBeforeClose(uint64_t ms);
-
-    void Finalize();
-  };
-}
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp	Wed May 23 14:28:25 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 "../PrecompiledHeaders.h"
+#include "TimeoutDicomConnectionManager.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  static boost::posix_time::ptime GetNow()
+  {
+    return boost::posix_time::microsec_clock::universal_time();
+  }
+
+  class TimeoutDicomConnectionManager::Resource : public IDicomConnectionManager::IResource
+  {
+  private:
+    TimeoutDicomConnectionManager&  that_;
+
+  public:
+    Resource(TimeoutDicomConnectionManager& that) : 
+      that_(that)
+    {
+      if (that_.connection_.get() == NULL)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    ~Resource()
+    {
+      that_.Touch();
+    }
+
+    DicomUserConnection& GetConnection()
+    {
+      assert(that_.connection_.get() != NULL);
+      return *that_.connection_;
+    }
+  };
+
+
+  void TimeoutDicomConnectionManager::Touch()
+  {
+    lastUse_ = GetNow();
+  }
+
+
+  void TimeoutDicomConnectionManager::CheckTimeoutInternal()
+  {
+    if (connection_.get() != NULL &&
+        (GetNow() - lastUse_) >= timeout_)
+    {
+      Close();
+    }
+  }
+
+
+  void TimeoutDicomConnectionManager::SetTimeout(unsigned int timeout)
+  {
+    timeout_ = boost::posix_time::milliseconds(timeout);
+    CheckTimeoutInternal();
+  }
+
+
+  unsigned int TimeoutDicomConnectionManager::GetTimeout()
+  {
+    return timeout_.total_milliseconds();
+  }
+
+
+  void TimeoutDicomConnectionManager::Close()
+  {
+    if (connection_.get() != NULL)
+    {
+      LOG(INFO) << "Closing inactive DICOM association with modality: "
+                << connection_->GetRemoteApplicationEntityTitle();
+
+      connection_.reset(NULL);
+    }
+  }
+
+
+  void TimeoutDicomConnectionManager::CheckTimeout()
+  {
+    CheckTimeoutInternal();
+  }
+
+
+  IDicomConnectionManager::IResource* 
+  TimeoutDicomConnectionManager::AcquireConnection(const std::string& localAet,
+                                                   const RemoteModalityParameters& remote)
+  {
+    if (connection_.get() == NULL ||
+        !connection_->IsSameAssociation(localAet, remote))
+    {
+      connection_.reset(new DicomUserConnection(localAet, remote));
+    }
+
+    return new Resource(*this);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomNetworking/TimeoutDicomConnectionManager.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,72 @@
+/**
+ * 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 "IDicomConnectionManager.h"
+
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+namespace Orthanc
+{
+  class TimeoutDicomConnectionManager : public IDicomConnectionManager
+  {
+  private:
+    class Resource;
+
+    std::auto_ptr<DicomUserConnection>   connection_;
+    boost::posix_time::ptime             lastUse_;
+    boost::posix_time::time_duration     timeout_;
+
+    void Touch();
+
+    void CheckTimeoutInternal();
+
+  public:
+    TimeoutDicomConnectionManager() :
+      timeout_(boost::posix_time::milliseconds(1000))
+    {
+    }
+
+    void SetTimeout(unsigned int timeout);
+
+    unsigned int GetTimeout();
+
+    void Close();
+
+    void CheckTimeout();
+
+    virtual IResource* AcquireConnection(const std::string& localAet,
+                                         const RemoteModalityParameters& remote);
+  };
+}
--- a/Core/DicomParsing/DicomModification.cpp	Wed May 23 10:08:04 2018 +0200
+++ b/Core/DicomParsing/DicomModification.cpp	Wed May 23 14:28:25 2018 +0200
@@ -1237,4 +1237,11 @@
     patientNameReplaced = (IsReplaced(DICOM_TAG_PATIENT_NAME) &&
                            GetReplacement(DICOM_TAG_PATIENT_NAME) == patientName);
   }
+
+  
+  void DicomModification::Serialize(Json::Value& value) const
+  {
+    // TODO
+    value = "TODO";
+  }
 }
--- a/Core/DicomParsing/DicomModification.h	Wed May 23 10:08:04 2018 +0200
+++ b/Core/DicomParsing/DicomModification.h	Wed May 23 14:28:25 2018 +0200
@@ -172,5 +172,7 @@
     {
       identifierGenerator_ = &generator;
     }
+
+    void Serialize(Json::Value& value) const;
   };
 }
--- a/Core/Enumerations.cpp	Wed May 23 10:08:04 2018 +0200
+++ b/Core/Enumerations.cpp	Wed May 23 14:28:25 2018 +0200
@@ -164,6 +164,9 @@
       case ErrorCode_DatabaseUnavailable:
         return "The database is currently not available (probably a transient situation)";
 
+      case ErrorCode_CanceledJob:
+        return "This job was canceled";
+
       case ErrorCode_SQLiteNotOpened:
         return "SQLite: The database is not opened";
 
@@ -984,6 +987,34 @@
   }
 
 
+  const char* EnumerationToString(JobState state)
+  {
+    switch (state)
+    {
+      case JobState_Pending:
+        return "Pending";
+        
+      case JobState_Running:
+        return "Running";
+        
+      case JobState_Success:
+        return "Success";
+        
+      case JobState_Failure:
+        return "Failure";
+        
+      case JobState_Paused:
+        return "Paused";
+        
+      case JobState_Retry:
+        return "Retry";
+        
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+  
+
   Encoding StringToEncoding(const char* encoding)
   {
     std::string s(encoding);
--- a/Core/Enumerations.h	Wed May 23 10:08:04 2018 +0200
+++ b/Core/Enumerations.h	Wed May 23 14:28:25 2018 +0200
@@ -96,6 +96,7 @@
     ErrorCode_NotAcceptable = 34    /*!< Cannot send a response which is acceptable according to the Accept HTTP header */,
     ErrorCode_NullPointer = 35    /*!< Cannot handle a NULL pointer */,
     ErrorCode_DatabaseUnavailable = 36    /*!< The database is currently not available (probably a transient situation) */,
+    ErrorCode_CanceledJob = 37    /*!< This job was canceled */,
     ErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     ErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     ErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
@@ -542,6 +543,24 @@
     TransferSyntax_Rle
   };
 
+  enum JobState
+  {
+    JobState_Pending,
+    JobState_Running,
+    JobState_Success,
+    JobState_Failure,
+    JobState_Paused,
+    JobState_Retry
+  };
+
+  enum JobStepCode
+  {
+    JobStepCode_Success,
+    JobStepCode_Failure,
+    JobStepCode_Continue,
+    JobStepCode_Retry
+  };
+
 
   /**
    * WARNING: Do not change the explicit values in the enumerations
@@ -622,6 +641,8 @@
 
   const char* EnumerationToString(ValueRepresentation vr);
 
+  const char* EnumerationToString(JobState state);
+
   Encoding StringToEncoding(const char* encoding);
 
   ResourceType StringToResourceType(const char* type);
--- a/Core/HttpServer/IIncomingHttpRequestFilter.h	Wed May 23 10:08:04 2018 +0200
+++ b/Core/HttpServer/IIncomingHttpRequestFilter.h	Wed May 23 14:28:25 2018 +0200
@@ -49,6 +49,6 @@
                            const char* ip,
                            const char* username,
                            const IHttpHandler::Arguments& httpHeaders,
-                           const IHttpHandler::GetArguments& getArguments) const = 0;
+                           const IHttpHandler::GetArguments& getArguments) = 0;
   };
 }
--- a/Core/HttpServer/MongooseServer.cpp	Wed May 23 10:08:04 2018 +0200
+++ b/Core/HttpServer/MongooseServer.cpp	Wed May 23 14:28:25 2018 +0200
@@ -676,7 +676,7 @@
 
     std::string username = GetAuthenticatedUsername(headers);
 
-    const IIncomingHttpRequestFilter *filter = server.GetIncomingHttpRequestFilter();
+    IIncomingHttpRequestFilter *filter = server.GetIncomingHttpRequestFilter();
     if (filter != NULL)
     {
       if (!filter->IsAllowed(method, request->uri, remoteIp,
--- a/Core/HttpServer/MongooseServer.h	Wed May 23 10:08:04 2018 +0200
+++ b/Core/HttpServer/MongooseServer.h	Wed May 23 14:28:25 2018 +0200
@@ -162,7 +162,7 @@
 
     void SetHttpCompressionEnabled(bool enabled);
 
-    const IIncomingHttpRequestFilter* GetIncomingHttpRequestFilter() const
+    IIncomingHttpRequestFilter* GetIncomingHttpRequestFilter() const
     {
       return filter_;
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/GenericJobUnserializer.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,90 @@
+/**
+ * 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 "../PrecompiledHeaders.h"
+#include "GenericJobUnserializer.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+
+#include "Operations/LogJobOperation.h"
+#include "Operations/NullOperationValue.h"
+#include "Operations/StringOperationValue.h"
+
+namespace Orthanc
+{
+  IJob* GenericJobUnserializer::UnserializeJob(const Json::Value& source)
+  {
+    const std::string type = GetString(source, "Type");
+
+    LOG(ERROR) << "Cannot unserialize job of type: " << type;
+    throw OrthancException(ErrorCode_BadFileFormat);
+  }
+
+
+  IJobOperation* GenericJobUnserializer::UnserializeOperation(const Json::Value& source)
+  {
+    const std::string type = GetString(source, "Type");
+
+    if (type == "Log")
+    {
+      return new LogJobOperation;
+    }
+    else
+    {
+      LOG(ERROR) << "Cannot unserialize operation of type: " << type;
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  JobOperationValue* GenericJobUnserializer::UnserializeValue(const Json::Value& source)
+  {
+    const std::string type = GetString(source, "Type");
+
+    if (type == "String")
+    {
+      return new StringOperationValue(GetString(source, "Content"));
+    }
+    else if (type == "Null")
+    {
+      return new NullOperationValue;
+    }
+    else
+    {
+      LOG(ERROR) << "Cannot unserialize value of type: " << type;
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/GenericJobUnserializer.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,49 @@
+/**
+ * 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 "IJobUnserializer.h"
+
+namespace Orthanc
+{
+  class GenericJobUnserializer : public IJobUnserializer
+  {
+  public:
+    virtual IJob* UnserializeJob(const Json::Value& source);
+
+    virtual IJobOperation* UnserializeOperation(const Json::Value& source);
+
+    virtual JobOperationValue* UnserializeValue(const Json::Value& source);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/IJob.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,68 @@
+/**
+ * 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 "JobStepResult.h"
+
+#include <boost/noncopyable.hpp>
+#include <json/value.h>
+
+namespace Orthanc
+{
+  class IJob : public boost::noncopyable
+  {
+  public:
+    virtual ~IJob()
+    {
+    }
+
+    // Method called once the job enters the jobs engine
+    virtual void Start() = 0;
+    
+    virtual JobStepResult ExecuteStep() = 0;
+
+    // Method called once the job is resubmitted after a failure
+    virtual void SignalResubmit() = 0;
+
+    virtual void ReleaseResources() = 0;   // For pausing/canceling jobs
+
+    virtual float GetProgress() = 0;
+
+    virtual void GetJobType(std::string& target) = 0;
+    
+    virtual void GetPublicContent(Json::Value& value) = 0;
+
+    virtual void GetInternalContent(Json::Value& value) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/IJobUnserializer.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,103 @@
+/**
+ * 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 "../PrecompiledHeaders.h"
+#include "IJobUnserializer.h"
+
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  void IJobUnserializer::CheckType(const Json::Value& source,
+                                   const std::string& expectedType)
+  {
+    static const char* TYPE = "Type";
+
+    if (source.type() != Json::objectValue ||
+        !source.isMember(TYPE) ||
+        source[TYPE].type() != Json::stringValue ||
+        source[TYPE].asString() != expectedType)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  std::string IJobUnserializer::GetString(const Json::Value& source,
+                                          const std::string& name)
+  {
+    if (source.type() != Json::objectValue ||
+        !source.isMember(name.c_str()) ||
+        source[name.c_str()].type() != Json::stringValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+    else
+    {
+      return source[name.c_str()].asString();
+    }
+  }
+
+
+  int IJobUnserializer::GetInteger(const Json::Value& source,
+                                   const std::string& name)
+  {
+    if (source.type() != Json::objectValue ||
+        !source.isMember(name.c_str()) ||
+        (source[name.c_str()].type() != Json::intValue &&
+         source[name.c_str()].type() != Json::uintValue))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+    else
+    {
+      return source[name.c_str()].asInt();
+    }    
+  }
+
+
+  unsigned int IJobUnserializer::GetUnsignedInteger(const Json::Value& source,
+                                                    const std::string& name)
+  {
+    int tmp = GetInteger(source, name);
+
+    if (tmp < 0)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+    else
+    {
+      return static_cast<unsigned int>(tmp);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/IJobUnserializer.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,67 @@
+/**
+ * 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 "IJob.h"
+#include "Operations/JobOperationValue.h"
+#include "Operations/IJobOperation.h"
+
+namespace Orthanc
+{
+  class IJobUnserializer : public boost::noncopyable
+  {
+  public:
+    virtual ~IJobUnserializer()
+    {
+    }
+
+    virtual IJob* UnserializeJob(const Json::Value& source) = 0;
+
+    virtual IJobOperation* UnserializeOperation(const Json::Value& source) = 0;
+
+    virtual JobOperationValue* UnserializeValue(const Json::Value& source) = 0;
+
+    static void CheckType(const Json::Value& source,
+                          const std::string& expectedType);
+
+    static std::string GetString(const Json::Value& source,
+                                 const std::string& name);
+
+    static int GetInteger(const Json::Value& source,
+                          const std::string& name);
+
+    static unsigned int GetUnsignedInteger(const Json::Value& source,
+                                           const std::string& name);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobInfo.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,154 @@
+/**
+ * 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 "../PrecompiledHeaders.h"
+#include "JobInfo.h"
+
+#include "../OrthancException.h"
+
+// This "include" is mandatory for Release builds using Linux Standard Base
+#include <boost/math/special_functions/round.hpp>
+
+namespace Orthanc
+{
+  JobInfo::JobInfo(const std::string& id,
+                   int priority,
+                   JobState state,
+                   const JobStatus& status,
+                   const boost::posix_time::ptime& creationTime,
+                   const boost::posix_time::ptime& lastStateChangeTime,
+                   const boost::posix_time::time_duration& runtime) :
+    id_(id),
+    priority_(priority),
+    state_(state),
+    timestamp_(boost::posix_time::microsec_clock::universal_time()),
+    creationTime_(creationTime),
+    lastStateChangeTime_(lastStateChangeTime),
+    runtime_(runtime),
+    hasEta_(false),
+    status_(status)
+  {
+    if (state_ == JobState_Running)
+    {
+      float ms = static_cast<float>(runtime_.total_milliseconds());
+
+      if (status_.GetProgress() > 0.01f &&
+          ms > 0.01f)
+      {
+        float ratio = static_cast<float>(1.0 - status_.GetProgress());
+        long long remaining = boost::math::llround(ratio * ms);
+        eta_ = timestamp_ + boost::posix_time::milliseconds(remaining);
+        hasEta_ = true;
+      }
+    }
+  }
+
+
+  JobInfo::JobInfo() :
+    priority_(0),
+    state_(JobState_Failure),
+    timestamp_(boost::posix_time::microsec_clock::universal_time()),
+    creationTime_(timestamp_),
+    lastStateChangeTime_(timestamp_),
+    runtime_(boost::posix_time::milliseconds(0)),
+    hasEta_(false)
+  {
+  }
+
+
+  bool JobInfo::HasCompletionTime() const
+  {
+    return (state_ == JobState_Success ||
+            state_ == JobState_Failure);
+  }
+
+
+  const boost::posix_time::ptime& JobInfo::GetEstimatedTimeOfArrival() const
+  {
+    if (hasEta_)
+    {
+      return eta_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  const boost::posix_time::ptime& JobInfo::GetCompletionTime() const
+  {
+    if (HasCompletionTime())
+    {
+      return lastStateChangeTime_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void JobInfo::Serialize(Json::Value& target,
+                          bool includeInternalContent) const
+  {
+    target = Json::objectValue;
+    target["ID"] = id_;
+    target["Priority"] = priority_;
+    target["ErrorCode"] = static_cast<int>(status_.GetErrorCode());
+    target["ErrorDescription"] = EnumerationToString(status_.GetErrorCode());
+    target["State"] = EnumerationToString(state_);
+    target["Timestamp"] = boost::posix_time::to_iso_string(timestamp_);
+    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);
+
+    target["Type"] = status_.GetJobType();
+    target["PublicContent"] = status_.GetPublicContent();
+
+    if (includeInternalContent)
+    {
+      target["InternalContent"] = status_.GetInternalContent();
+    }
+
+    if (HasEstimatedTimeOfArrival())
+    {
+      target["EstimatedTimeOfArrival"] = boost::posix_time::to_iso_string(GetEstimatedTimeOfArrival());
+    }
+
+    if (HasCompletionTime())
+    {
+      target["CompletionTime"] = boost::posix_time::to_iso_string(GetCompletionTime());
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobInfo.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,121 @@
+/**
+ * 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 "JobStatus.h"
+
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+namespace Orthanc
+{
+  class JobInfo
+  {
+  private:
+    std::string                       id_;
+    int                               priority_;
+    JobState                          state_;
+    boost::posix_time::ptime          timestamp_;
+    boost::posix_time::ptime          creationTime_;
+    boost::posix_time::ptime          lastStateChangeTime_;
+    boost::posix_time::time_duration  runtime_;
+    bool                              hasEta_;
+    boost::posix_time::ptime          eta_;
+    JobStatus                         status_;
+
+  public:
+    JobInfo(const std::string& id,
+            int priority,
+            JobState state,
+            const JobStatus& status,
+            const boost::posix_time::ptime& creationTime,
+            const boost::posix_time::ptime& lastStateChangeTime,
+            const boost::posix_time::time_duration& runtime);
+
+    JobInfo();
+
+    const std::string& GetIdentifier() const
+    {
+      return id_;
+    }
+
+    int GetPriority() const
+    {
+      return priority_;
+    }
+
+    JobState GetState() const
+    {
+      return state_;
+    }
+
+    const boost::posix_time::ptime& GetInfoTime() const
+    {
+      return timestamp_;
+    }
+
+    const boost::posix_time::ptime& GetCreationTime() const
+    {
+      return creationTime_;
+    }
+
+    const boost::posix_time::time_duration& GetRuntime() const
+    {
+      return runtime_;
+    }
+
+    bool HasEstimatedTimeOfArrival() const
+    {
+      return hasEta_;
+    }
+
+    bool HasCompletionTime() const;
+
+    const boost::posix_time::ptime& GetEstimatedTimeOfArrival() const;
+
+    const boost::posix_time::ptime& GetCompletionTime() const;
+
+    const JobStatus& GetStatus() const
+    {
+      return status_;
+    }
+
+    JobStatus& GetStatus()
+    {
+      return status_;
+    }
+
+    void Serialize(Json::Value& target,
+                   bool includeInternalContent) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobStatus.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,70 @@
+/**
+ * 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 "../PrecompiledHeaders.h"
+#include "JobStatus.h"
+
+namespace Orthanc
+{
+  JobStatus::JobStatus() :
+    errorCode_(ErrorCode_InternalError),
+    progress_(0),
+    jobType_("Invalid"),
+    publicContent_(Json::objectValue),
+    internalContent_(Json::objectValue)
+  {
+  }
+
+  
+  JobStatus::JobStatus(ErrorCode code,
+                       IJob& job) :
+    errorCode_(code),
+    progress_(job.GetProgress()),
+    publicContent_(Json::objectValue),
+    internalContent_(Json::objectValue)
+  {
+    if (progress_ < 0)
+    {
+      progress_ = 0;
+    }
+      
+    if (progress_ > 1)
+    {
+      progress_ = 1;
+    }
+
+    job.GetJobType(jobType_);
+    job.GetPublicContent(publicContent_);
+    job.GetInternalContent(internalContent_);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobStatus.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,85 @@
+/**
+ * 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 "IJob.h"
+
+namespace Orthanc
+{
+  class JobStatus
+  {
+  private:
+    ErrorCode      errorCode_;
+    float          progress_;
+    std::string    jobType_;
+    Json::Value    publicContent_;
+    Json::Value    internalContent_;
+
+  public:
+    JobStatus();
+
+    JobStatus(ErrorCode code,
+              IJob& job);
+
+    ErrorCode GetErrorCode() const
+    {
+      return errorCode_;
+    }
+
+    void SetErrorCode(ErrorCode error)
+    {
+      errorCode_ = error;
+    }
+
+    float GetProgress() const
+    {
+      return progress_;
+    }
+
+    const std::string& GetJobType() const
+    {
+      return jobType_;
+    }
+
+    const Json::Value& GetPublicContent() const
+    {
+      return publicContent_;
+    }
+
+    const Json::Value& GetInternalContent() const
+    {
+      return internalContent_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobStepResult.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,81 @@
+/**
+ * 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 "../PrecompiledHeaders.h"
+#include "JobStepResult.h"
+
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  JobStepResult JobStepResult::Retry(unsigned int timeout)
+  {
+    JobStepResult result(JobStepCode_Retry);
+    result.timeout_ = timeout;
+    return result;
+  }
+
+
+  JobStepResult JobStepResult::Failure(const ErrorCode& error)
+  {
+    JobStepResult result(JobStepCode_Failure);
+    result.error_ = error;
+    return result;
+  }
+
+
+  unsigned int JobStepResult::GetRetryTimeout() const
+  {
+    if (code_ == JobStepCode_Retry)
+    {
+      return timeout_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  ErrorCode JobStepResult::GetFailureCode() const
+  {
+    if (code_ == JobStepCode_Failure)
+    {
+      return error_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobStepResult.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,85 @@
+/**
+ * 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 "../Enumerations.h"
+
+namespace Orthanc
+{
+  class JobStepResult
+  {
+  private:
+    JobStepCode   code_;
+    unsigned int  timeout_;
+    ErrorCode     error_;
+    
+    explicit JobStepResult(JobStepCode code) :
+      code_(code),
+      timeout_(0),
+      error_(ErrorCode_Success)
+    {
+    }
+
+  public:
+    explicit JobStepResult() :
+      code_(JobStepCode_Failure),
+      timeout_(0),
+      error_(ErrorCode_InternalError)
+    {
+    }
+
+    static JobStepResult Success()
+    {
+      return JobStepResult(JobStepCode_Success);
+    }
+
+    static JobStepResult Continue()
+    {
+      return JobStepResult(JobStepCode_Continue);
+    }
+
+    static JobStepResult Retry(unsigned int timeout);
+
+    static JobStepResult Failure(const ErrorCode& error);
+
+    JobStepCode GetCode() const
+    {
+      return code_;
+    }
+
+    unsigned int GetRetryTimeout() const;
+
+    ErrorCode GetFailureCode() const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobsEngine.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,264 @@
+/**
+ * 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 "../PrecompiledHeaders.h"
+#include "JobsEngine.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  bool JobsEngine::IsRunning()
+  {
+    boost::mutex::scoped_lock lock(stateMutex_);
+    return (state_ == State_Running);
+  }
+  
+  
+  bool JobsEngine::ExecuteStep(JobsRegistry::RunningJob& running,
+                               size_t workerIndex)
+  {
+    assert(running.IsValid());
+
+    if (running.IsPauseScheduled())
+    {
+      running.GetJob().ReleaseResources();
+      running.MarkPause();
+      return false;
+    }
+
+    if (running.IsCancelScheduled())
+    {
+      running.GetJob().ReleaseResources();
+      running.MarkCanceled();
+      return false;
+    }
+
+    JobStepResult result;
+
+    try
+    {
+      result = running.GetJob().ExecuteStep();
+    }
+    catch (OrthancException& e)
+    {
+      result = JobStepResult::Failure(e.GetErrorCode());
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+      result = JobStepResult::Failure(ErrorCode_BadFileFormat);
+    }
+    catch (...)
+    {
+      result = JobStepResult::Failure(ErrorCode_InternalError);
+    }
+
+    switch (result.GetCode())
+    {
+      case JobStepCode_Success:
+        running.UpdateStatus(ErrorCode_Success);
+        running.GetJob().ReleaseResources();
+        running.MarkSuccess();
+        return false;
+
+      case JobStepCode_Failure:
+        running.GetJob().ReleaseResources();
+        running.UpdateStatus(result.GetFailureCode());
+        running.MarkFailure();
+        return false;
+
+      case JobStepCode_Retry:
+        running.GetJob().ReleaseResources();
+        running.UpdateStatus(ErrorCode_Success);
+        running.MarkRetry(result.GetRetryTimeout());
+        return false;
+
+      case JobStepCode_Continue:
+        running.UpdateStatus(ErrorCode_Success);
+        return true;
+            
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+    
+  void JobsEngine::RetryHandler(JobsEngine* engine)
+  {
+    assert(engine != NULL);
+
+    while (engine->IsRunning())
+    {
+      boost::this_thread::sleep(boost::posix_time::milliseconds(200));
+      engine->GetRegistry().ScheduleRetries();
+    }
+  }
+
+    
+  void JobsEngine::Worker(JobsEngine* engine,
+                          size_t workerIndex)
+  {
+    assert(engine != NULL);
+
+    LOG(INFO) << "Worker thread " << workerIndex << " has started";
+
+    while (engine->IsRunning())
+    {
+      JobsRegistry::RunningJob running(engine->GetRegistry(), 100);
+
+      if (running.IsValid())
+      {
+        LOG(INFO) << "Executing job with priority " << running.GetPriority()
+                  << " in worker thread " << workerIndex << ": " << running.GetId();
+
+        while (engine->IsRunning())
+        {
+          if (!engine->ExecuteStep(running, workerIndex))
+          {
+            break;
+          }
+        }
+      }
+    }      
+  }
+
+
+  JobsEngine::JobsEngine() :
+    state_(State_Setup),
+    workers_(1)
+  {
+  }
+
+    
+  JobsEngine::~JobsEngine()
+  {
+    if (state_ != State_Setup &&
+        state_ != State_Done)
+    {
+      LOG(ERROR) << "INTERNAL ERROR: JobsEngine::Stop() should be invoked manually to avoid mess in the destruction order!";
+      Stop();
+    }
+  }
+
+    
+  void JobsEngine::SetWorkersCount(size_t count)
+  {
+    boost::mutex::scoped_lock lock(stateMutex_);
+      
+    if (state_ != State_Setup)
+    {
+      // Can only be invoked before calling "Start()"
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    workers_.resize(count);
+  }
+    
+
+  void JobsEngine::Start()
+  {
+    boost::mutex::scoped_lock lock(stateMutex_);
+
+    if (state_ != State_Setup)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    retryHandler_ = boost::thread(RetryHandler, this);
+
+    if (workers_.size() == 0)
+    {
+      // Use all the available CPUs
+      size_t n = boost::thread::hardware_concurrency();
+      
+      if (n == 0)
+      {
+        n = 1;
+      }
+
+      workers_.resize(n);
+    }      
+
+    for (size_t i = 0; i < workers_.size(); i++)
+    {
+      assert(workers_[i] == NULL);
+      workers_[i] = new boost::thread(Worker, this, i);
+    }
+
+    state_ = State_Running;
+
+    LOG(WARNING) << "The jobs engine has started with " << workers_.size() << " threads";
+  }
+
+
+  void JobsEngine::Stop()
+  {
+    {
+      boost::mutex::scoped_lock lock(stateMutex_);
+
+      if (state_ != State_Running)
+      {
+        return;
+      }
+        
+      state_ = State_Stopping;
+    }
+
+    LOG(INFO) << "Stopping the jobs engine";
+      
+    if (retryHandler_.joinable())
+    {
+      retryHandler_.join();
+    }
+      
+    for (size_t i = 0; i < workers_.size(); i++)
+    {
+      assert(workers_[i] != NULL);
+
+      if (workers_[i]->joinable())
+      {
+        workers_[i]->join();
+      }
+
+      delete workers_[i];
+    }
+      
+    {
+      boost::mutex::scoped_lock lock(stateMutex_);
+      state_ = State_Done;
+    }
+
+    LOG(WARNING) << "The jobs engine has stopped";
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobsEngine.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,85 @@
+/**
+ * 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 "JobsRegistry.h"
+
+#include <boost/thread.hpp>
+
+namespace Orthanc
+{
+  class JobsEngine
+  {
+  private:
+    enum State
+    {
+      State_Setup,
+      State_Running,
+      State_Stopping,
+      State_Done
+    };
+
+    boost::mutex                 stateMutex_;
+    State                        state_;
+    JobsRegistry                 registry_;
+    boost::thread                retryHandler_;
+    std::vector<boost::thread*>  workers_;
+
+    bool IsRunning();
+    
+    bool ExecuteStep(JobsRegistry::RunningJob& running,
+                     size_t workerIndex);
+    
+    static void RetryHandler(JobsEngine* engine);
+
+    static void Worker(JobsEngine* engine,
+                       size_t workerIndex);
+
+  public:
+    JobsEngine();
+
+    ~JobsEngine();
+
+    void SetWorkersCount(size_t count);
+    
+    JobsRegistry& GetRegistry()
+    {
+      return registry_;
+    }
+
+    void Start();
+
+    void Stop();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobsRegistry.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,1087 @@
+/**
+ * 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 "../PrecompiledHeaders.h"
+#include "JobsRegistry.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../Toolbox.h"
+
+namespace Orthanc
+{
+  class JobsRegistry::JobHandler : public boost::noncopyable
+  {   
+  private:
+    std::string                       id_;
+    JobState                          state_;
+    std::auto_ptr<IJob>               job_;
+    int                               priority_;  // "+inf()" means highest priority
+    boost::posix_time::ptime          creationTime_;
+    boost::posix_time::ptime          lastStateChangeTime_;
+    boost::posix_time::time_duration  runtime_;
+    boost::posix_time::ptime          retryTime_;
+    bool                              pauseScheduled_;
+    bool                              cancelScheduled_;
+    JobStatus                         lastStatus_;
+
+    void Touch()
+    {
+      const boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time();
+
+      if (state_ == JobState_Running)
+      {
+        runtime_ += (now - lastStateChangeTime_);
+      }
+
+      lastStateChangeTime_ = now;
+    }
+
+    void SetStateInternal(JobState state) 
+    {
+      state_ = state;
+      pauseScheduled_ = false;
+      cancelScheduled_ = false;
+      Touch();
+    }
+
+  public:
+    JobHandler(IJob* job,
+               int priority) :
+      id_(Toolbox::GenerateUuid()),
+      state_(JobState_Pending),
+      job_(job),
+      priority_(priority),
+      creationTime_(boost::posix_time::microsec_clock::universal_time()),
+      lastStateChangeTime_(creationTime_),
+      runtime_(boost::posix_time::milliseconds(0)),
+      retryTime_(creationTime_),
+      pauseScheduled_(false),
+      cancelScheduled_(false)
+    {
+      if (job == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+
+      lastStatus_ = JobStatus(ErrorCode_Success, *job);
+      job->Start();
+    }
+
+    const std::string& GetId() const
+    {
+      return id_;
+    }
+
+    IJob& GetJob() const
+    {
+      assert(job_.get() != NULL);
+      return *job_;
+    }
+
+    void SetPriority(int priority)
+    {
+      priority_ = priority;
+    }
+
+    int GetPriority() const
+    {
+      return priority_;
+    }
+
+    JobState GetState() const
+    {
+      return state_;
+    }
+
+    void SetState(JobState state) 
+    {
+      if (state == JobState_Retry)
+      {
+        // Use "SetRetryState()"
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        SetStateInternal(state);
+      }
+    }
+
+    void SetRetryState(unsigned int timeout)
+    {
+      if (state_ == JobState_Running)
+      {
+        SetStateInternal(JobState_Retry);
+        retryTime_ = (boost::posix_time::microsec_clock::universal_time() + 
+                      boost::posix_time::milliseconds(timeout));
+      }
+      else
+      {
+        // Only valid for running jobs
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    void SchedulePause()
+    {
+      if (state_ == JobState_Running)
+      {
+        pauseScheduled_ = true;
+      }
+      else
+      {
+        // Only valid for running jobs
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    void ScheduleCancel()
+    {
+      if (state_ == JobState_Running)
+      {
+        cancelScheduled_ = true;
+      }
+      else
+      {
+        // Only valid for running jobs
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+    }
+
+    bool IsPauseScheduled()
+    {
+      return pauseScheduled_;
+    }
+
+    bool IsCancelScheduled()
+    {
+      return cancelScheduled_;
+    }
+
+    bool IsRetryReady(const boost::posix_time::ptime& now) const
+    {
+      if (state_ != JobState_Retry)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        return retryTime_ <= now;
+      }
+    }
+
+    const boost::posix_time::ptime& GetCreationTime() const
+    {
+      return creationTime_;
+    }
+
+    const boost::posix_time::ptime& GetLastStateChangeTime() const
+    {
+      return lastStateChangeTime_;
+    }
+
+    const boost::posix_time::time_duration& GetRuntime() const
+    {
+      return runtime_;
+    }
+
+    const JobStatus& GetLastStatus() const
+    {
+      return lastStatus_;
+    }
+
+    void SetLastStatus(const JobStatus& status)
+    {
+      lastStatus_ = status;
+      Touch();
+    }
+
+    void SetLastErrorCode(ErrorCode code)
+    {
+      lastStatus_.SetErrorCode(code);
+    }
+  };
+
+
+  bool JobsRegistry::PriorityComparator::operator() (JobHandler*& a,
+                                                     JobHandler*& b) const
+  {
+    return a->GetPriority() < b->GetPriority();
+  }                       
+
+
+#if defined(NDEBUG)
+  void JobsRegistry::CheckInvariants() const
+  {
+  }
+  
+#else
+  bool JobsRegistry::IsPendingJob(const JobHandler& job) const
+  {
+    PendingJobs copy = pendingJobs_;
+    while (!copy.empty())
+    {
+      if (copy.top() == &job)
+      {
+        return true;
+      }
+
+      copy.pop();
+    }
+
+    return false;
+  }
+
+  bool JobsRegistry::IsCompletedJob(JobHandler& job) const
+  {
+    for (CompletedJobs::const_iterator it = completedJobs_.begin();
+         it != completedJobs_.end(); ++it)
+    {
+      if (*it == &job)
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  bool JobsRegistry::IsRetryJob(JobHandler& job) const
+  {
+    return retryJobs_.find(&job) != retryJobs_.end();
+  }
+
+  void JobsRegistry::CheckInvariants() const
+  {
+    {
+      PendingJobs copy = pendingJobs_;
+      while (!copy.empty())
+      {
+        assert(copy.top()->GetState() == JobState_Pending);
+        copy.pop();
+      }
+    }
+
+    assert(completedJobs_.size() <= maxCompletedJobs_);
+
+    for (CompletedJobs::const_iterator it = completedJobs_.begin();
+         it != completedJobs_.end(); ++it)
+    {
+      assert((*it)->GetState() == JobState_Success ||
+             (*it)->GetState() == JobState_Failure);
+    }
+
+    for (RetryJobs::const_iterator it = retryJobs_.begin();
+         it != retryJobs_.end(); ++it)
+    {
+      assert((*it)->GetState() == JobState_Retry);
+    }
+
+    for (JobsIndex::const_iterator it = jobsIndex_.begin();
+         it != jobsIndex_.end(); ++it)
+    {
+      JobHandler& job = *it->second;
+
+      assert(job.GetId() == it->first);
+
+      switch (job.GetState())
+      {
+        case JobState_Pending:
+          assert(!IsRetryJob(job) && IsPendingJob(job) && !IsCompletedJob(job));
+          break;
+            
+        case JobState_Success:
+        case JobState_Failure:
+          assert(!IsRetryJob(job) && !IsPendingJob(job) && IsCompletedJob(job));
+          break;
+            
+        case JobState_Retry:
+          assert(IsRetryJob(job) && !IsPendingJob(job) && !IsCompletedJob(job));
+          break;
+            
+        case JobState_Running:
+        case JobState_Paused:
+          assert(!IsRetryJob(job) && !IsPendingJob(job) && !IsCompletedJob(job));
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+  }
+#endif
+
+
+  void JobsRegistry::ForgetOldCompletedJobs()
+  {
+    if (maxCompletedJobs_ != 0)
+    {
+      while (completedJobs_.size() > maxCompletedJobs_)
+      {
+        assert(completedJobs_.front() != NULL);
+
+        std::string id = completedJobs_.front()->GetId();
+        assert(jobsIndex_.find(id) != jobsIndex_.end());
+
+        jobsIndex_.erase(id);
+        delete(completedJobs_.front());
+        completedJobs_.pop_front();
+      }
+    }
+  }
+
+
+  void JobsRegistry::SetCompletedJob(JobHandler& job,
+                                     bool success)
+  {
+    job.SetState(success ? JobState_Success : JobState_Failure);
+
+    completedJobs_.push_back(&job);
+    ForgetOldCompletedJobs();
+
+    someJobComplete_.notify_all();
+  }
+
+
+  void JobsRegistry::MarkRunningAsCompleted(JobHandler& job,
+                                            bool success)
+  {
+    LOG(INFO) << "Job has completed with " << (success ? "success" : "failure")
+              << ": " << job.GetId();
+
+    CheckInvariants();
+
+    assert(job.GetState() == JobState_Running);
+    SetCompletedJob(job, success);
+
+    CheckInvariants();
+  }
+
+
+  void JobsRegistry::MarkRunningAsRetry(JobHandler& job,
+                                        unsigned int timeout)
+  {
+    LOG(INFO) << "Job scheduled for retry in " << timeout << "ms: " << job.GetId();
+
+    CheckInvariants();
+
+    assert(job.GetState() == JobState_Running &&
+           retryJobs_.find(&job) == retryJobs_.end());
+
+    retryJobs_.insert(&job);
+    job.SetRetryState(timeout);
+
+    CheckInvariants();
+  }
+
+
+  void JobsRegistry::MarkRunningAsPaused(JobHandler& job)
+  {
+    LOG(INFO) << "Job paused: " << job.GetId();
+
+    CheckInvariants();
+    assert(job.GetState() == JobState_Running);
+
+    job.SetState(JobState_Paused);
+
+    CheckInvariants();
+  }
+
+
+  bool JobsRegistry::GetStateInternal(JobState& state,
+                                      const std::string& id)
+  {
+    CheckInvariants();
+
+    JobsIndex::const_iterator it = jobsIndex_.find(id);
+    if (it == jobsIndex_.end())
+    {
+      return false;
+    }
+    else
+    {
+      state = it->second->GetState();
+      return true;
+    }
+  }
+
+  
+  JobsRegistry::~JobsRegistry()
+  {
+    for (JobsIndex::iterator it = jobsIndex_.begin(); it != jobsIndex_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      delete it->second;
+    }
+  }
+
+
+  void JobsRegistry::SetMaxCompletedJobs(size_t n)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    LOG(INFO) << "The size of the history of the jobs engine is set to: " << n << " job(s)";
+
+    maxCompletedJobs_ = n;
+    ForgetOldCompletedJobs();
+
+    CheckInvariants();
+  }
+
+
+  void JobsRegistry::ListJobs(std::set<std::string>& target)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    for (JobsIndex::const_iterator it = jobsIndex_.begin();
+         it != jobsIndex_.end(); ++it)
+    {
+      target.insert(it->first);
+    }
+  }
+
+
+  bool JobsRegistry::GetJobInfo(JobInfo& target,
+                                const std::string& id)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    JobsIndex::const_iterator found = jobsIndex_.find(id);
+
+    if (found == jobsIndex_.end())
+    {
+      return false;
+    }
+    else
+    {
+      const JobHandler& handler = *found->second;
+      target = JobInfo(handler.GetId(),
+                       handler.GetPriority(),
+                       handler.GetState(),
+                       handler.GetLastStatus(),
+                       handler.GetCreationTime(),
+                       handler.GetLastStateChangeTime(),
+                       handler.GetRuntime());
+      return true;
+    }
+  }
+
+
+  void JobsRegistry::Submit(std::string& id,
+                            IJob* job,        // Takes ownership
+                            int priority)
+  {
+    std::auto_ptr<JobHandler>  handler(new JobHandler(job, priority));
+
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+      
+    id = handler->GetId();
+
+    pendingJobs_.push(handler.get());
+    pendingJobAvailable_.notify_one();
+
+    jobsIndex_.insert(std::make_pair(id, handler.release()));
+
+    LOG(INFO) << "New job submitted with priority " << priority << ": " << id;
+
+    CheckInvariants();
+  }
+
+
+  void JobsRegistry::Submit(IJob* job,        // Takes ownership
+                            int priority)
+  {
+    std::string id;
+    Submit(id, job, priority);
+  }
+
+
+  bool JobsRegistry::SubmitAndWait(IJob* job,        // Takes ownership
+                                   int priority)
+  {
+    std::string id;
+    Submit(id, job, priority);
+
+    JobState state;
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      while (GetStateInternal(state, id) &&
+             state != JobState_Success &&
+             state != JobState_Failure)
+      {
+        someJobComplete_.wait(lock);
+      }
+    }
+
+    return (state == JobState_Success);
+  }
+
+
+  bool JobsRegistry::SetPriority(const std::string& id,
+                                 int priority)
+  {
+    LOG(INFO) << "Changing priority to " << priority << " for job: " << id;
+
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    JobsIndex::iterator found = jobsIndex_.find(id);
+
+    if (found == jobsIndex_.end())
+    {
+      LOG(WARNING) << "Unknown job: " << id;
+      return false;
+    }
+    else
+    {
+      found->second->SetPriority(priority);
+
+      if (found->second->GetState() == JobState_Pending)
+      {
+        // If the job is pending, we need to reconstruct the
+        // priority queue, as the heap condition has changed
+
+        PendingJobs copy;
+        std::swap(copy, pendingJobs_);
+
+        assert(pendingJobs_.empty());
+        while (!copy.empty())
+        {
+          pendingJobs_.push(copy.top());
+          copy.pop();
+        }
+      }
+
+      CheckInvariants();
+      return true;
+    }
+  }
+
+
+  void JobsRegistry::RemovePendingJob(const std::string& id)
+  {
+    // If the job is pending, we need to reconstruct the priority
+    // queue to remove it
+    PendingJobs copy;
+    std::swap(copy, pendingJobs_);
+
+    assert(pendingJobs_.empty());
+    while (!copy.empty())
+    {
+      if (copy.top()->GetId() != id)
+      {
+        pendingJobs_.push(copy.top());
+      }
+
+      copy.pop();
+    }
+  }
+
+
+  void JobsRegistry::RemoveRetryJob(JobHandler* handler)
+  {
+    RetryJobs::iterator item = retryJobs_.find(handler);
+    assert(item != retryJobs_.end());            
+    retryJobs_.erase(item);
+  }
+
+
+  bool JobsRegistry::Pause(const std::string& id)
+  {
+    LOG(INFO) << "Pausing job: " << id;
+
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    JobsIndex::iterator found = jobsIndex_.find(id);
+
+    if (found == jobsIndex_.end())
+    {
+      LOG(WARNING) << "Unknown job: " << id;
+      return false;
+    }
+    else
+    {
+      switch (found->second->GetState())
+      {
+        case JobState_Pending:
+          RemovePendingJob(id);
+          found->second->SetState(JobState_Paused);
+          break;
+
+        case JobState_Retry:
+          RemoveRetryJob(found->second);
+          found->second->SetState(JobState_Paused);
+          break;
+
+        case JobState_Paused:
+        case JobState_Success:
+        case JobState_Failure:
+          // Nothing to be done
+          break;
+
+        case JobState_Running:
+          found->second->SchedulePause();
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      CheckInvariants();
+      return true;
+    }
+  }
+
+
+  bool JobsRegistry::Cancel(const std::string& id)
+  {
+    LOG(INFO) << "Canceling job: " << id;
+
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    JobsIndex::iterator found = jobsIndex_.find(id);
+
+    if (found == jobsIndex_.end())
+    {
+      LOG(WARNING) << "Unknown job: " << id;
+      return false;
+    }
+    else
+    {
+      switch (found->second->GetState())
+      {
+        case JobState_Pending:
+          RemovePendingJob(id);
+          SetCompletedJob(*found->second, false);
+          found->second->SetLastErrorCode(ErrorCode_CanceledJob);
+          break;
+
+        case JobState_Retry:
+          RemoveRetryJob(found->second);
+          SetCompletedJob(*found->second, false);
+          found->second->SetLastErrorCode(ErrorCode_CanceledJob);
+          break;
+
+        case JobState_Paused:
+          SetCompletedJob(*found->second, false);
+          found->second->SetLastErrorCode(ErrorCode_CanceledJob);
+          break;
+        
+        case JobState_Success:
+        case JobState_Failure:
+          // Nothing to be done
+          break;
+
+        case JobState_Running:
+          found->second->ScheduleCancel();
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      CheckInvariants();
+      return true;
+    }
+  }
+
+
+  bool JobsRegistry::Resume(const std::string& id)
+  {
+    LOG(INFO) << "Resuming job: " << id;
+
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    JobsIndex::iterator found = jobsIndex_.find(id);
+
+    if (found == jobsIndex_.end())
+    {
+      LOG(WARNING) << "Unknown job: " << id;
+      return false;
+    }
+    else if (found->second->GetState() != JobState_Paused)
+    {
+      LOG(WARNING) << "Cannot resume a job that is not paused: " << id;
+      return false;
+    }
+    else
+    {
+      found->second->SetState(JobState_Pending);
+      pendingJobs_.push(found->second);
+      pendingJobAvailable_.notify_one();
+      CheckInvariants();
+      return true;      
+    }
+  }
+
+
+  bool JobsRegistry::Resubmit(const std::string& id)
+  {
+    LOG(INFO) << "Resubmitting failed job: " << id;
+
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    JobsIndex::iterator found = jobsIndex_.find(id);
+
+    if (found == jobsIndex_.end())
+    {
+      LOG(WARNING) << "Unknown job: " << id;
+      return false;
+    }
+    else if (found->second->GetState() != JobState_Failure)
+    {
+      printf("%s\n", EnumerationToString(found->second->GetState()));
+      LOG(WARNING) << "Cannot resubmit a job that has not failed: " << id;
+      return false;
+    }
+    else
+    {
+      found->second->GetJob().SignalResubmit();
+      
+      bool ok = false;
+      for (CompletedJobs::iterator it = completedJobs_.begin(); 
+           it != completedJobs_.end(); ++it)
+      {
+        if (*it == found->second)
+        {
+          ok = true;
+          completedJobs_.erase(it);
+          break;
+        }
+      }
+
+      assert(ok);
+
+      found->second->SetState(JobState_Pending);
+      pendingJobs_.push(found->second);
+      pendingJobAvailable_.notify_one();
+
+      CheckInvariants();
+      return true;
+    }
+  }
+
+
+  void JobsRegistry::ScheduleRetries()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    RetryJobs copy;
+    std::swap(copy, retryJobs_);
+
+    const boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time();
+
+    assert(retryJobs_.empty());
+    for (RetryJobs::iterator it = copy.begin(); it != copy.end(); ++it)
+    {
+      if ((*it)->IsRetryReady(now))
+      {
+        LOG(INFO) << "Retrying job: " << (*it)->GetId();
+        (*it)->SetState(JobState_Pending);
+        pendingJobs_.push(*it);
+        pendingJobAvailable_.notify_one();
+      }
+      else
+      {
+        retryJobs_.insert(*it);
+      }
+    }
+
+    CheckInvariants();
+  }
+
+
+  bool JobsRegistry::GetState(JobState& state,
+                              const std::string& id)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    return GetStateInternal(state, id);
+  }
+
+  
+  JobsRegistry::RunningJob::RunningJob(JobsRegistry& registry,
+                                       unsigned int timeout) :
+    registry_(registry),
+    handler_(NULL),
+    targetState_(JobState_Failure),
+    targetRetryTimeout_(0),
+    canceled_(false)
+  {
+    {
+      boost::mutex::scoped_lock lock(registry_.mutex_);
+
+      while (registry_.pendingJobs_.empty())
+      {
+        if (timeout == 0)
+        {
+          registry_.pendingJobAvailable_.wait(lock);
+        }
+        else
+        {
+          bool success = registry_.pendingJobAvailable_.timed_wait
+            (lock, boost::posix_time::milliseconds(timeout));
+          if (!success)
+          {
+            // No pending job
+            return;
+          }
+        }
+      }
+
+      handler_ = registry_.pendingJobs_.top();
+      registry_.pendingJobs_.pop();
+
+      assert(handler_->GetState() == JobState_Pending);
+      handler_->SetState(JobState_Running);
+      handler_->SetLastErrorCode(ErrorCode_Success);
+
+      job_ = &handler_->GetJob();
+      id_ = handler_->GetId();
+      priority_ = handler_->GetPriority();
+    }
+  }
+
+      
+  JobsRegistry::RunningJob::~RunningJob()
+  {
+    if (IsValid())
+    {
+      boost::mutex::scoped_lock lock(registry_.mutex_);
+
+      switch (targetState_)
+      {
+        case JobState_Failure:
+          registry_.MarkRunningAsCompleted(*handler_, false);
+
+          if (canceled_)
+          {
+            handler_->SetLastErrorCode(ErrorCode_CanceledJob);
+          }
+          
+          break;
+
+        case JobState_Success:
+          registry_.MarkRunningAsCompleted(*handler_, true);
+          break;
+
+        case JobState_Paused:
+          registry_.MarkRunningAsPaused(*handler_);
+          break;            
+
+        case JobState_Retry:
+          registry_.MarkRunningAsRetry(*handler_, targetRetryTimeout_);
+          break;
+            
+        default:
+          assert(0);
+      }
+    }
+  }
+
+      
+  bool JobsRegistry::RunningJob::IsValid() const
+  {
+    return (handler_ != NULL &&
+            job_ != NULL);
+  }
+
+      
+  const std::string& JobsRegistry::RunningJob::GetId() const
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return id_;
+    }
+  }
+
+      
+  int JobsRegistry::RunningJob::GetPriority() const
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return priority_;
+    }
+  }
+      
+
+  IJob& JobsRegistry::RunningJob::GetJob()
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *job_;
+    }
+  }
+
+      
+  bool JobsRegistry::RunningJob::IsPauseScheduled()
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      boost::mutex::scoped_lock lock(registry_.mutex_);
+      registry_.CheckInvariants();
+      assert(handler_->GetState() == JobState_Running);
+        
+      return handler_->IsPauseScheduled();
+    }
+  }
+
+      
+  bool JobsRegistry::RunningJob::IsCancelScheduled()
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      boost::mutex::scoped_lock lock(registry_.mutex_);
+      registry_.CheckInvariants();
+      assert(handler_->GetState() == JobState_Running);
+        
+      return handler_->IsCancelScheduled();
+    }
+  }
+
+      
+  void JobsRegistry::RunningJob::MarkSuccess()
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      targetState_ = JobState_Success;
+    }
+  }
+
+      
+  void JobsRegistry::RunningJob::MarkFailure()
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      targetState_ = JobState_Failure;
+    }
+  }
+
+      
+  void JobsRegistry::RunningJob::MarkCanceled()
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      targetState_ = JobState_Failure;
+      canceled_ = true;
+    }
+  }
+
+      
+  void JobsRegistry::RunningJob::MarkPause()
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      targetState_ = JobState_Paused;
+    }
+  }
+
+      
+  void JobsRegistry::RunningJob::MarkRetry(unsigned int timeout)
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      targetState_ = JobState_Retry;
+      targetRetryTimeout_ = timeout;
+    }
+  }
+      
+
+  void JobsRegistry::RunningJob::UpdateStatus(ErrorCode code)
+  {
+    if (!IsValid())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      JobStatus status(code, *job_);
+          
+      boost::mutex::scoped_lock lock(registry_.mutex_);
+      registry_.CheckInvariants();
+      assert(handler_->GetState() == JobState_Running);
+        
+      handler_->SetLastStatus(status);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobsRegistry.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,203 @@
+/**
+ * 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
+
+#if !defined(ORTHANC_SANDBOXED)
+#  error The macro ORTHANC_SANDBOXED must be defined
+#endif
+
+#if ORTHANC_SANDBOXED == 1
+#  error The job engine cannot be used in sandboxed environments
+#endif
+
+#include "JobInfo.h"
+
+#include <list>
+#include <set>
+#include <queue>
+#include <boost/thread/mutex.hpp>
+#include <boost/thread/condition_variable.hpp>
+
+namespace Orthanc
+{
+  // This class handles the state machine of the jobs engine
+  class JobsRegistry : public boost::noncopyable
+  {
+  private:
+    class JobHandler;
+
+    struct PriorityComparator
+    {
+      bool operator() (JobHandler*& a,
+                       JobHandler*& b) const;
+    };
+
+    typedef std::map<std::string, JobHandler*>              JobsIndex;
+    typedef std::list<JobHandler*>                          CompletedJobs;
+    typedef std::set<JobHandler*>                           RetryJobs;
+    typedef std::priority_queue<JobHandler*, 
+                                std::vector<JobHandler*>,   // Could be a "std::deque"
+                                PriorityComparator>         PendingJobs;
+
+    boost::mutex               mutex_;
+    JobsIndex                  jobsIndex_;
+    PendingJobs                pendingJobs_;
+    CompletedJobs              completedJobs_;
+    RetryJobs                  retryJobs_;
+
+    boost::condition_variable  pendingJobAvailable_;
+    boost::condition_variable  someJobComplete_;
+    size_t                     maxCompletedJobs_;
+
+
+#ifndef NDEBUG
+    bool IsPendingJob(const JobHandler& job) const;
+
+    bool IsCompletedJob(JobHandler& job) const;
+    
+    bool IsRetryJob(JobHandler& job) const;
+#endif
+
+    void CheckInvariants() const;
+
+    void ForgetOldCompletedJobs();
+
+    void SetCompletedJob(JobHandler& job,
+                         bool success);
+    
+    void MarkRunningAsCompleted(JobHandler& job,
+                                bool success);
+
+    void MarkRunningAsRetry(JobHandler& job,
+                            unsigned int timeout);
+    
+    void MarkRunningAsPaused(JobHandler& job);
+    
+    bool GetStateInternal(JobState& state,
+                          const std::string& id);
+
+    void RemovePendingJob(const std::string& id);
+      
+    void RemoveRetryJob(JobHandler* handler);
+      
+  public:
+    JobsRegistry() :
+      maxCompletedJobs_(10)
+    {
+    }
+
+
+    ~JobsRegistry();
+
+    void SetMaxCompletedJobs(size_t i);
+    
+    void ListJobs(std::set<std::string>& target);
+
+    bool GetJobInfo(JobInfo& target,
+                    const std::string& id);
+    
+    void Submit(std::string& id,
+                IJob* job,        // Takes ownership
+                int priority);
+    
+    void Submit(IJob* job,        // Takes ownership
+                int priority);
+
+    bool SubmitAndWait(IJob* job,        // Takes ownership
+                       int priority);
+    
+    bool SetPriority(const std::string& id,
+                     int priority);
+
+    bool Pause(const std::string& id);
+    
+    bool Resume(const std::string& id);
+
+    bool Resubmit(const std::string& id);
+
+    bool Cancel(const std::string& id);
+    
+    void ScheduleRetries();
+    
+    bool GetState(JobState& state,
+                  const std::string& id);
+
+    class RunningJob : public boost::noncopyable
+    {
+    private:
+      JobsRegistry&  registry_;
+      JobHandler*    handler_;  // Can only be accessed if the
+                                // registry mutex is locked!
+      IJob*          job_;  // Will by design be in mutual exclusion,
+                            // because only one RunningJob can be
+                            // executed at a time on a JobHandler
+
+      std::string    id_;
+      int            priority_;
+      JobState       targetState_;
+      unsigned int   targetRetryTimeout_;
+      bool           canceled_;
+      
+    public:
+      RunningJob(JobsRegistry& registry,
+                 unsigned int timeout);
+
+      ~RunningJob();
+
+      bool IsValid() const;
+
+      const std::string& GetId() const;
+
+      int GetPriority() const;
+
+      IJob& GetJob();
+
+      bool IsPauseScheduled();
+
+      bool IsCancelScheduled();
+
+      void MarkSuccess();
+
+      void MarkFailure();
+
+      void MarkPause();
+
+      void MarkCanceled();
+
+      void MarkRetry(unsigned int timeout);
+
+      void UpdateStatus(ErrorCode code);
+    };
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/IJobOperation.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,54 @@
+/**
+ * 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 "JobOperationValues.h"
+#include "../../DicomNetworking/IDicomConnectionManager.h"
+
+namespace Orthanc
+{
+  class IJobOperation : public boost::noncopyable
+  {
+  public:
+    virtual ~IJobOperation()
+    {
+    }
+
+    virtual void Apply(JobOperationValues& outputs,
+                       const JobOperationValue& input,
+                       IDicomConnectionManager& dicomConnection) = 0;
+
+    virtual void Serialize(Json::Value& result) const = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/JobOperationValue.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,74 @@
+/**
+ * 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 <json/value.h>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class JobOperationValue : public boost::noncopyable
+  {
+  public:
+    enum Type
+    {
+      Type_DicomInstance,
+      Type_Null,
+      Type_String
+    };
+
+  private:
+    Type  type_;
+
+  protected:
+    JobOperationValue(Type type) :
+      type_(type)
+    {
+    }
+
+  public:
+    virtual ~JobOperationValue()
+    {
+    }
+
+    Type GetType() const
+    {
+      return type_;
+    }
+
+    virtual JobOperationValue* Clone() const = 0;
+
+    virtual void Serialize(Json::Value& target) const = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/JobOperationValues.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,143 @@
+/**
+ * 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 "../../PrecompiledHeaders.h"
+#include "JobOperationValues.h"
+
+#include "../IJobUnserializer.h"
+#include "../../OrthancException.h"
+
+#include <cassert>
+#include <memory>
+
+namespace Orthanc
+{
+  void JobOperationValues::Append(JobOperationValues& target,
+                                  bool clear)
+  {
+    target.Reserve(target.GetSize() + GetSize());
+
+    for (size_t i = 0; i < values_.size(); i++)
+    {
+      if (clear)
+      {
+        target.Append(values_[i]);
+        values_[i] = NULL;
+      }
+      else
+      {
+        target.Append(GetValue(i).Clone());
+      }
+    }
+
+    if (clear)
+    {
+      Clear();
+    }
+  }
+
+
+  void JobOperationValues::Clear()
+  {
+    for (size_t i = 0; i < values_.size(); i++)
+    {
+      if (values_[i] != NULL)
+      {
+        delete values_[i];
+      }
+    }
+
+    values_.clear();
+  }
+
+
+  void JobOperationValues::Append(JobOperationValue* value)  // Takes ownership
+  {
+    if (value == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else
+    {
+      values_.push_back(value);
+    }
+  }
+
+
+  JobOperationValue& JobOperationValues::GetValue(size_t index) const
+  {
+    if (index >= values_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(values_[index] != NULL);
+      return *values_[index];
+    }
+  }
+
+
+  void JobOperationValues::Serialize(Json::Value& target) const
+  {
+    target = Json::arrayValue;
+
+    for (size_t i = 0; i < values_.size(); i++)
+    {
+      Json::Value tmp;
+      values_[i]->Serialize(tmp);
+      target.append(tmp);
+    }
+  }
+
+
+  JobOperationValues* Unserialize(IJobUnserializer& unserializer,
+                                  const Json::Value& source)
+  {
+    if (source.type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    std::auto_ptr<JobOperationValues> result(new JobOperationValues);
+
+    result->Reserve(source.size());
+    
+    for (Json::Value::ArrayIndex i = 0; i < source.size(); i++)
+    {
+      result->Append(unserializer.UnserializeValue(source[i]));
+    }
+    
+    return result.release();
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/JobOperationValues.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,89 @@
+/**
+ * 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 "JobOperationValue.h"
+
+#include <vector>
+
+namespace Orthanc
+{
+  class IJobUnserializer;
+
+  class JobOperationValues : public boost::noncopyable
+  {
+  private:
+    std::vector<JobOperationValue*>   values_;
+
+    void Append(JobOperationValues& target,
+                bool clear);
+
+  public:
+    ~JobOperationValues()
+    {
+      Clear();
+    }
+
+    void Move(JobOperationValues& target)
+    {
+      return Append(target, true);
+    }
+
+    void Copy(JobOperationValues& target)
+    {
+      return Append(target, false);
+    }
+
+    void Clear();
+
+    void Reserve(size_t count)
+    {
+      values_.reserve(count);
+    }
+
+    void Append(JobOperationValue* value);  // Takes ownership
+
+    size_t GetSize() const
+    {
+      return values_.size();
+    }
+
+    JobOperationValue& GetValue(size_t index) const;
+
+    void Serialize(Json::Value& target) const;
+
+    static JobOperationValues* Unserialize(IJobUnserializer& unserializer,
+                                           const Json::Value& source);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/LogJobOperation.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,64 @@
+/**
+ * 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 "../../PrecompiledHeaders.h"
+#include "LogJobOperation.h"
+
+#include "../../Logging.h"
+#include "StringOperationValue.h"
+
+namespace Orthanc
+{
+  void LogJobOperation::Apply(JobOperationValues& outputs,
+                              const JobOperationValue& input,
+                              IDicomConnectionManager& connectionManager)
+  {
+    switch (input.GetType())
+    {
+      case JobOperationValue::Type_String:
+        LOG(INFO) << "Job value: "
+                  << dynamic_cast<const StringOperationValue&>(input).GetContent();
+        break;
+
+      case JobOperationValue::Type_Null:
+        LOG(INFO) << "Job value: (null)";
+        break;
+
+      default:
+        LOG(INFO) << "Job value: (unsupport)";
+        break;
+    }
+
+    outputs.Append(input.Clone());
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/LogJobOperation.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,52 @@
+/**
+ * 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 "IJobOperation.h"
+
+namespace Orthanc
+{
+  class LogJobOperation : public IJobOperation
+  {
+  public:
+    virtual void Apply(JobOperationValues& outputs,
+                       const JobOperationValue& input,
+                       IDicomConnectionManager& connectionManager);
+
+    virtual void Serialize(Json::Value& result) const
+    {
+      result["Type"] = "Log";
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/NullOperationValue.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,58 @@
+/**
+ * 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 "JobOperationValue.h"
+
+namespace Orthanc
+{
+  class NullOperationValue : public JobOperationValue
+  {
+  public:
+    NullOperationValue() :
+      JobOperationValue(Type_Null)
+    {
+    }
+
+    virtual JobOperationValue* Clone() const
+    {
+      return new NullOperationValue;
+    }
+
+    virtual void Serialize(Json::Value& target) const
+    {
+      target["Type"] = "Null";
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,381 @@
+/**
+ * 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 "../../PrecompiledHeaders.h"
+#include "SequenceOfOperationsJob.h"
+
+#include "../../Logging.h"
+#include "../../OrthancException.h"
+
+namespace Orthanc
+{
+  class SequenceOfOperationsJob::Operation : public boost::noncopyable
+  {
+  private:
+    size_t                        index_;
+    JobOperationValues            originalInputs_;
+    JobOperationValues            workInputs_;
+    std::auto_ptr<IJobOperation>  operation_;
+    std::list<Operation*>         nextOperations_;
+    size_t                        currentInput_;
+
+  public:
+    Operation(size_t index,
+              IJobOperation* operation) :
+      index_(index),
+      operation_(operation),
+      currentInput_(0)
+    {
+      if (operation == NULL)
+      {
+        throw OrthancException(ErrorCode_NullPointer);
+      }
+    }
+
+    void AddOriginalInput(const JobOperationValue& value)
+    {
+      if (currentInput_ != 0)
+      {
+        // Cannot add input after processing has started
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        originalInputs_.Append(value.Clone());
+      }
+    }
+
+    const JobOperationValues& GetOriginalInputs() const
+    {
+      return originalInputs_;
+    }
+
+    void Reset()
+    {
+      workInputs_.Clear();
+      currentInput_ = 0;
+    }
+
+    void AddNextOperation(Operation& other)
+    {
+      if (other.index_ <= index_)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (currentInput_ != 0)
+      {
+        // Cannot add input after processing has started
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+      else
+      {
+        nextOperations_.push_back(&other);
+      }
+    }
+
+    bool IsDone() const
+    {
+      return currentInput_ >= originalInputs_.GetSize() + workInputs_.GetSize();
+    }
+
+    void Step(IDicomConnectionManager& connectionManager)
+    {
+      if (IsDone())
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+
+      const JobOperationValue* input;
+
+      if (currentInput_ < originalInputs_.GetSize())
+      {
+        input = &originalInputs_.GetValue(currentInput_);
+      }
+      else
+      {
+        input = &workInputs_.GetValue(currentInput_ - originalInputs_.GetSize());
+      }
+
+      JobOperationValues outputs;
+      operation_->Apply(outputs, *input, connectionManager);
+
+      if (!nextOperations_.empty())
+      {
+        std::list<Operation*>::iterator first = nextOperations_.begin();
+        outputs.Move((*first)->workInputs_);
+
+        std::list<Operation*>::iterator current = first;
+        ++current;
+
+        while (current != nextOperations_.end())
+        {
+          (*first)->workInputs_.Copy((*current)->workInputs_);
+          ++current;
+        }
+      }
+
+      currentInput_ += 1;
+    }
+
+    void Serialize(Json::Value& target) const
+    {
+      target = Json::objectValue;
+      operation_->Serialize(target["Operation"]);
+      originalInputs_.Serialize(target["OriginalInputs"]);
+      workInputs_.Serialize(target["WorkInputs"]);      
+
+      Json::Value tmp = Json::arrayValue;
+      for (std::list<Operation*>::const_iterator it = nextOperations_.begin();
+           it != nextOperations_.end(); ++it)
+      {
+        tmp.append(static_cast<int>((*it)->index_));
+      }
+
+      target["NextOperations"] = tmp;
+    }
+  };
+
+
+  SequenceOfOperationsJob::SequenceOfOperationsJob() :
+    done_(false),
+    current_(0),
+    trailingTimeout_(boost::posix_time::milliseconds(1000))
+  {
+  }
+
+
+  SequenceOfOperationsJob::~SequenceOfOperationsJob()
+  {
+    for (size_t i = 0; i < operations_.size(); i++)
+    {
+      if (operations_[i] != NULL)
+      {
+        delete operations_[i];
+      }
+    }
+  }
+
+
+  void SequenceOfOperationsJob::SetDescription(const std::string& description)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    description_ = description;
+  }
+
+
+  void SequenceOfOperationsJob::Register(IObserver& observer)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    observers_.push_back(&observer);
+  }
+
+
+  void SequenceOfOperationsJob::Lock::SetTrailingOperationTimeout(unsigned int timeout)
+  {
+    that_.trailingTimeout_ = boost::posix_time::milliseconds(timeout);
+  }
+
+  
+  void SequenceOfOperationsJob::Lock::SetDicomAssociationTimeout(unsigned int timeout)
+  {
+    that_.connectionManager_.SetTimeout(timeout);
+  }
+
+
+  size_t SequenceOfOperationsJob::Lock::AddOperation(IJobOperation* operation)
+  {
+    if (IsDone())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    size_t index = that_.operations_.size();
+
+    that_.operations_.push_back(new Operation(index, operation));
+    that_.operationAdded_.notify_one();
+
+    return index;
+  }
+
+
+  void SequenceOfOperationsJob::Lock::AddInput(size_t index,
+                                               const JobOperationValue& value)
+  {
+    if (IsDone())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (index >= that_.operations_.size() ||
+             index < that_.current_)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      that_.operations_[index]->AddOriginalInput(value);
+    }
+  }
+      
+
+  void SequenceOfOperationsJob::Lock::Connect(size_t input,
+                                              size_t output)
+  {
+    if (IsDone())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (input >= output ||
+             input >= that_.operations_.size() ||
+             output >= that_.operations_.size() ||
+             input < that_.current_ ||
+             output < that_.current_)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      Operation& a = *that_.operations_[input];
+      Operation& b = *that_.operations_[output];
+      a.AddNextOperation(b);
+    }
+  }
+
+
+  JobStepResult SequenceOfOperationsJob::ExecuteStep()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    if (current_ == operations_.size())
+    {
+      LOG(INFO) << "Executing the trailing timeout in the sequence of operations";
+      operationAdded_.timed_wait(lock, trailingTimeout_);
+            
+      if (current_ == operations_.size())
+      {
+        // No operation was added during the trailing timeout: The
+        // job is over
+        LOG(INFO) << "The sequence of operations is over";
+        done_ = true;
+
+        for (std::list<IObserver*>::iterator it = observers_.begin(); 
+             it != observers_.end(); ++it)
+        {
+          (*it)->SignalDone(*this);
+        }
+
+        connectionManager_.Close();
+        return JobStepResult::Success();
+      }
+      else
+      {
+        LOG(INFO) << "New operation added to the sequence of operations";
+      }
+    }
+
+    assert(current_ < operations_.size());
+
+    while (current_ < operations_.size() &&
+           operations_[current_]->IsDone())
+    {
+      current_++;
+    }
+
+    if (current_ < operations_.size())
+    {
+      operations_[current_]->Step(connectionManager_);
+    }
+
+    connectionManager_.CheckTimeout();
+
+    return JobStepResult::Continue();
+  }
+
+
+  void SequenceOfOperationsJob::SignalResubmit()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+      
+    current_ = 0;
+    done_ = false;
+
+    for (size_t i = 0; i < operations_.size(); i++)
+    {
+      operations_[i]->Reset();
+    }
+  }
+
+
+  void SequenceOfOperationsJob::ReleaseResources()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    connectionManager_.Close();
+  }
+
+
+  float SequenceOfOperationsJob::GetProgress()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+      
+    return (static_cast<float>(current_) / 
+            static_cast<float>(operations_.size() + 1));
+  }
+
+
+  void SequenceOfOperationsJob::GetPublicContent(Json::Value& value)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    value["CountOperations"] = static_cast<unsigned int>(operations_.size());
+    value["Description"] = description_;
+  }
+
+
+  void SequenceOfOperationsJob::GetInternalContent(Json::Value& value)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Json::Value tmp = Json::arrayValue;
+    for (size_t i = 0; i < operations_.size(); i++)
+    {
+      Json::Value operation = Json::objectValue;
+      operations_[i]->Serialize(operation);
+      tmp.append(operation);
+    }
+
+    value["Operations"] = tmp;
+    value["TrailingTimeout"] = static_cast<unsigned int>(trailingTimeout_.total_milliseconds());
+    value["DicomTimeout"] = connectionManager_.GetTimeout();
+    value["Current"] = static_cast<unsigned int>(current_);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,148 @@
+/**
+ * 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 "../IJob.h"
+#include "IJobOperation.h"
+
+#include "../../DicomNetworking/TimeoutDicomConnectionManager.h"
+
+#include <boost/thread/mutex.hpp>
+#include <boost/thread/condition_variable.hpp>
+
+#include <list>
+
+namespace Orthanc
+{
+  class SequenceOfOperationsJob : public IJob
+  {
+  public:
+    class IObserver : public boost::noncopyable
+    {
+    public:
+      virtual ~IObserver()
+      {
+      }
+
+      virtual void SignalDone(const SequenceOfOperationsJob& job) = 0;
+    };
+
+  private:
+    class Operation;
+
+    std::string                       description_;
+    bool                              done_;
+    boost::mutex                      mutex_;
+    std::vector<Operation*>           operations_;
+    size_t                            current_;
+    boost::condition_variable         operationAdded_;
+    boost::posix_time::time_duration  trailingTimeout_;
+    std::list<IObserver*>             observers_;
+    TimeoutDicomConnectionManager     connectionManager_;
+
+  public:
+    SequenceOfOperationsJob();
+
+    virtual ~SequenceOfOperationsJob();
+
+    void SetDescription(const std::string& description);
+
+    void Register(IObserver& observer);
+
+    // This lock allows adding new operations to the end of the job,
+    // from another thread than the worker thread, after the job has
+    // been submitted for processing
+    class Lock : public boost::noncopyable
+    {
+    private:
+      SequenceOfOperationsJob&   that_;
+      boost::mutex::scoped_lock  lock_;
+
+    public:
+      Lock(SequenceOfOperationsJob& that) :
+      that_(that),
+      lock_(that.mutex_)
+      {
+      }
+
+      bool IsDone() const
+      {
+        return that_.done_;
+      }
+
+      void SetTrailingOperationTimeout(unsigned int timeout);
+
+      void SetDicomAssociationTimeout(unsigned int timeout);
+      
+      size_t AddOperation(IJobOperation* operation);
+
+      size_t GetOperationsCount() const
+      {
+        return that_.operations_.size();
+      }
+
+      void AddInput(size_t index,
+                    const JobOperationValue& value);
+      
+      void Connect(size_t input,
+                   size_t output);
+    };
+
+    virtual void Start()
+    {
+    }
+
+    virtual JobStepResult ExecuteStep();
+
+    virtual void SignalResubmit();
+
+    virtual void ReleaseResources();
+
+    virtual float GetProgress();
+
+    virtual void GetJobType(std::string& target)
+    {
+      target = "SequenceOfOperations";
+    }
+
+    virtual void GetPublicContent(Json::Value& value);
+
+    virtual void GetInternalContent(Json::Value& value);
+
+    void AwakeTrailingSleep()
+    {
+      operationAdded_.notify_one();
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/StringOperationValue.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,70 @@
+/**
+ * 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 "JobOperationValue.h"
+
+#include <string>
+
+namespace Orthanc
+{
+  class StringOperationValue : public JobOperationValue
+  {
+  private:
+    std::string  content_;
+
+  public:
+    StringOperationValue(const std::string& content) :
+      JobOperationValue(JobOperationValue::Type_String),
+      content_(content)
+    {
+    }
+
+    virtual JobOperationValue* Clone() const
+    {
+      return new StringOperationValue(content_);
+    }
+
+    const std::string& GetContent() const
+    {
+      return content_;
+    }
+
+    virtual void Serialize(Json::Value& target) const
+    {
+      target["Type"] = "String";
+      target["Content"] = content_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/SetOfInstancesJob.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,203 @@
+/**
+ * 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 "../PrecompiledHeaders.h"
+#include "SetOfInstancesJob.h"
+
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  SetOfInstancesJob::SetOfInstancesJob() :
+    started_(false),
+    permissive_(false),
+    position_(0)
+  {
+  }
+
+    
+  void SetOfInstancesJob::Reserve(size_t size)
+  {
+    if (started_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      instances_.reserve(size);
+    }
+  }
+
+    
+  void SetOfInstancesJob::AddInstance(const std::string& instance)
+  {
+    if (started_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      instances_.push_back(instance);
+    }
+  }
+
+
+  void SetOfInstancesJob::SetPermissive(bool permissive)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      permissive_ = permissive;
+    }
+  }
+
+
+  void SetOfInstancesJob::SignalResubmit()
+  {
+    if (started_)
+    {
+      position_ = 0;
+      failedInstances_.clear();
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+    
+  float SetOfInstancesJob::GetProgress()
+  {
+    if (instances_.size() == 0)
+    {
+      return 0;
+    }
+    else
+    {
+      return (static_cast<float>(position_) /
+              static_cast<float>(instances_.size()));
+    }
+  }
+
+
+  JobStepResult SetOfInstancesJob::ExecuteStep()
+  {
+    if (!started_)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    if (instances_.empty() &&
+        position_ == 0)
+    {
+      // No instance to handle, we're done
+      position_ = 1;
+      return JobStepResult::Success();
+    }
+
+    if (position_ >= instances_.size())
+    {
+      // Already done
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    const std::string currentInstance = instances_[position_];
+    
+    bool ok;
+      
+    try
+    {
+      ok = HandleInstance(currentInstance);
+
+      if (!ok && !permissive_)
+      {
+        return JobStepResult::Failure(ErrorCode_InternalError);
+      }
+    }
+    catch (OrthancException& e)
+    {
+      if (permissive_)
+      {
+        ok = false;
+      }
+      else
+      {
+        throw;
+      }
+    }
+
+    if (!ok)
+    {
+      failedInstances_.insert(currentInstance);
+    }
+
+    position_ += 1;
+
+    if (position_ == instances_.size())
+    {
+      // We're done
+      return JobStepResult::Success();
+    }
+    else
+    {
+      return JobStepResult::Continue();
+    }
+  }
+
+    
+  void SetOfInstancesJob::GetInternalContent(Json::Value& value)
+  {
+    Json::Value v = Json::arrayValue;
+      
+    for (size_t i = 0; i < instances_.size(); i++)
+    {
+      v.append(instances_[i]);
+    }
+
+    value["Instances"] = v;
+
+      
+    v = Json::arrayValue;
+
+    for (std::set<std::string>::const_iterator it = failedInstances_.begin();
+         it != failedInstances_.end(); ++it)
+    {
+      v.append(*it);
+    }
+      
+    value["FailedInstances"] = v;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/SetOfInstancesJob.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,101 @@
+/**
+ * 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 "IJob.h"
+
+#include <set>
+
+namespace Orthanc
+{
+  class SetOfInstancesJob : public IJob
+  {
+  private:
+    bool                      started_;
+    std::vector<std::string>  instances_;
+    bool                      permissive_;
+    size_t                    position_;
+    std::set<std::string>     failedInstances_;
+
+  protected:
+    virtual bool HandleInstance(const std::string& instance) = 0;
+
+  public:
+    SetOfInstancesJob();
+
+    void Reserve(size_t size);
+
+    size_t GetInstancesCount() const
+    {
+      return instances_.size();
+    }
+    
+    void AddInstance(const std::string& instance);
+
+    bool IsPermissive() const
+    {
+      return permissive_;
+    }
+
+    void SetPermissive(bool permissive);
+
+    virtual void SignalResubmit();
+    
+    virtual void Start()
+    {
+      started_ = true;
+    }
+    
+    virtual float GetProgress();
+
+    bool IsStarted() const
+    {
+      return started_;
+    }
+
+    const std::vector<std::string>& GetInstances() const
+    {
+      return instances_;
+    }
+  
+    const std::set<std::string>& GetFailedInstances() const
+    {
+      return failedInstances_;
+    }
+  
+    virtual JobStepResult ExecuteStep();
+    
+    virtual void GetInternalContent(Json::Value& value);
+  };
+}
--- a/Core/MultiThreading/BagOfTasks.h	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-/**
- * 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 "../ICommand.h"
-
-#include <list>
-#include <cstddef>
-
-namespace Orthanc
-{
-  class BagOfTasks : public boost::noncopyable
-  {
-  private:
-    typedef std::list<ICommand*>  Tasks;
-
-    Tasks  tasks_;
-
-  public:
-    ~BagOfTasks()
-    {
-      for (Tasks::iterator it = tasks_.begin(); it != tasks_.end(); ++it)
-      {
-        delete *it;
-      }
-    }
-
-    ICommand* Pop()
-    {
-      ICommand* task = tasks_.front();
-      tasks_.pop_front();
-      return task;
-    }
-
-    void Push(ICommand* task)   // Takes ownership
-    {
-      if (task != NULL)
-      {
-        tasks_.push_back(task);
-      }
-    }
-
-    size_t GetSize() const
-    {
-      return tasks_.size();
-    }
-
-    bool IsEmpty() const
-    {
-      return tasks_.empty();
-    }
-  };
-}
--- a/Core/MultiThreading/BagOfTasksProcessor.cpp	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,277 +0,0 @@
-/**
- * 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 "../PrecompiledHeaders.h"
-#include "BagOfTasksProcessor.h"
-
-#include "../Logging.h"
-#include "../OrthancException.h"
-
-#include <stdio.h>
-
-namespace Orthanc
-{
-  class BagOfTasksProcessor::Task : public IDynamicObject
-  {
-  private:
-    uint64_t                 bag_;
-    std::auto_ptr<ICommand>  command_;
-
-  public:
-    Task(uint64_t  bag,
-         ICommand* command) :
-      bag_(bag),
-      command_(command)
-    {
-    }
-
-    bool Execute()
-    {
-      try
-      {
-        return command_->Execute();
-      }
-      catch (OrthancException& e)
-      {
-        LOG(ERROR) << "Exception while processing a bag of tasks: " << e.What();
-        return false;
-      }
-      catch (std::runtime_error& e)
-      {
-        LOG(ERROR) << "Runtime exception while processing a bag of tasks: " << e.what();
-        return false;
-      }
-      catch (...)
-      {
-        LOG(ERROR) << "Native exception while processing a bag of tasks";
-        return false;
-      }
-    }
-
-    uint64_t GetBag()
-    {
-      return bag_;
-    }
-  };
-
-
-  void BagOfTasksProcessor::SignalProgress(Task& task,
-                                           Bag& bag)
-  {
-    assert(bag.done_ < bag.size_);
-
-    bag.done_ += 1;
-
-    if (bag.done_ == bag.size_)
-    {
-      exitStatus_[task.GetBag()] = (bag.status_ == BagStatus_Running);
-      bagFinished_.notify_all();
-    }
-  }
-
-  void BagOfTasksProcessor::Worker(BagOfTasksProcessor* that)
-  {
-    while (that->continue_)
-    {
-      std::auto_ptr<IDynamicObject> obj(that->queue_.Dequeue(100));
-      if (obj.get() != NULL)
-      {
-        Task& task = *dynamic_cast<Task*>(obj.get());
-
-        {
-          boost::mutex::scoped_lock lock(that->mutex_);
-
-          Bags::iterator bag = that->bags_.find(task.GetBag());
-          assert(bag != that->bags_.end());
-          assert(bag->second.done_ < bag->second.size_);
-
-          if (bag->second.status_ != BagStatus_Running)
-          {
-            // Do not execute this task, as its parent bag of tasks
-            // has failed or is tagged as canceled
-            that->SignalProgress(task, bag->second);
-            continue;
-          }
-        }
-
-        bool success = task.Execute();
-
-        {
-          boost::mutex::scoped_lock lock(that->mutex_);
-
-          Bags::iterator bag = that->bags_.find(task.GetBag());
-          assert(bag != that->bags_.end());
-
-          if (!success)
-          {
-            bag->second.status_ = BagStatus_Failed;
-          }
-
-          that->SignalProgress(task, bag->second);
-        }
-      }
-    }
-  }
-
-
-  void BagOfTasksProcessor::Cancel(int64_t bag)
-  {
-    boost::mutex::scoped_lock  lock(mutex_);
-
-    Bags::iterator it = bags_.find(bag);
-    if (it != bags_.end())
-    {
-      it->second.status_ = BagStatus_Canceled;
-    }
-  }
-
-
-  bool BagOfTasksProcessor::Join(int64_t bag)
-  {
-    boost::mutex::scoped_lock  lock(mutex_);
-
-    while (continue_)
-    {
-      ExitStatus::iterator it = exitStatus_.find(bag);
-      if (it == exitStatus_.end())  // The bag is still running
-      {
-        bagFinished_.wait(lock);
-      }
-      else
-      {
-        bool status = it->second;
-        exitStatus_.erase(it);
-        return status;
-      }
-    }
-
-    return false;   // The processor is stopping
-  }
-
-
-  float BagOfTasksProcessor::GetProgress(int64_t bag)
-  {
-    boost::mutex::scoped_lock  lock(mutex_);
-
-    Bags::const_iterator it = bags_.find(bag);
-    if (it == bags_.end())
-    {
-      // The bag of tasks has finished
-      return 1.0f;
-    }
-    else
-    {
-      return (static_cast<float>(it->second.done_) / 
-              static_cast<float>(it->second.size_));
-    }
-  }
-
-
-  bool BagOfTasksProcessor::Handle::Join()
-  {
-    if (hasJoined_)
-    {
-      return status_;
-    }
-    else
-    {
-      status_ = that_.Join(bag_);
-      hasJoined_ = true;
-      return status_;
-    }
-  }
-
-
-  BagOfTasksProcessor::BagOfTasksProcessor(size_t countThreads) : 
-    countBags_(0),
-    continue_(true)
-  {
-    if (countThreads == 0)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    threads_.resize(countThreads);
-
-    for (size_t i = 0; i < threads_.size(); i++)
-    {
-      threads_[i] = new boost::thread(Worker, this);
-    }
-  }
-
-
-  BagOfTasksProcessor::~BagOfTasksProcessor()
-  {
-    continue_ = false;
-
-    bagFinished_.notify_all();   // Wakes up all the pending "Join()"
-
-    for (size_t i = 0; i < threads_.size(); i++)
-    {
-      if (threads_[i])
-      {
-        if (threads_[i]->joinable())
-        {
-          threads_[i]->join();
-        }
-
-        delete threads_[i];
-        threads_[i] = NULL;
-      }
-    }
-  }
-
-
-  BagOfTasksProcessor::Handle* BagOfTasksProcessor::Submit(BagOfTasks& tasks)
-  {
-    if (tasks.GetSize() == 0)
-    {
-      return new Handle(*this, 0, true);
-    }
-
-    boost::mutex::scoped_lock lock(mutex_);
-
-    uint64_t id = countBags_;
-    countBags_ += 1;
-
-    Bag bag(tasks.GetSize());
-    bags_[id] = bag;
-
-    while (!tasks.IsEmpty())
-    {
-      queue_.Enqueue(new Task(id, tasks.Pop()));
-    }
-
-    return new Handle(*this, id, false);
-  }
-}
--- a/Core/MultiThreading/BagOfTasksProcessor.h	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,150 +0,0 @@
-/**
- * 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 "BagOfTasks.h"
-#include "SharedMessageQueue.h"
-
-#include <stdint.h>
-#include <map>
-
-namespace Orthanc
-{
-  class BagOfTasksProcessor : public boost::noncopyable
-  {
-  private:
-    enum BagStatus
-    {
-      BagStatus_Running,
-      BagStatus_Canceled,
-      BagStatus_Failed
-    };
-
-
-    struct Bag
-    {
-      size_t    size_;
-      size_t    done_;
-      BagStatus status_;
-
-      Bag() :
-        size_(0),
-        done_(0),
-        status_(BagStatus_Failed)
-      {
-      }
-
-      explicit Bag(size_t size) : 
-        size_(size),
-        done_(0),
-        status_(BagStatus_Running)
-      {
-      }
-    };
-
-    class Task;
-
-
-    typedef std::map<uint64_t, Bag>   Bags;
-    typedef std::map<uint64_t, bool>  ExitStatus;
-
-    SharedMessageQueue  queue_;
-
-    boost::mutex  mutex_;
-    uint64_t  countBags_;
-    Bags bags_;
-    std::vector<boost::thread*>   threads_;
-    ExitStatus  exitStatus_;
-    bool continue_;
-
-    boost::condition_variable  bagFinished_;
-
-    static void Worker(BagOfTasksProcessor* that);
-
-    void Cancel(int64_t bag);
-
-    bool Join(int64_t bag);
-
-    float GetProgress(int64_t bag);
-
-    void SignalProgress(Task& task,
-                        Bag& bag);
-
-  public:
-    class Handle : public boost::noncopyable
-    {
-      friend class BagOfTasksProcessor;
-
-    private:
-      BagOfTasksProcessor&  that_;
-      uint64_t              bag_;
-      bool                  hasJoined_;
-      bool                  status_;
- 
-      Handle(BagOfTasksProcessor&  that,
-             uint64_t bag,
-             bool empty) : 
-        that_(that),
-        bag_(bag),
-        hasJoined_(empty)
-      {
-      }
-
-    public:
-      ~Handle()
-      {
-        Join();
-      }
-
-      void Cancel()
-      {
-        that_.Cancel(bag_);
-      }
-
-      bool Join();
-
-      float GetProgress()
-      {
-        return that_.GetProgress(bag_);
-      }
-    };
-  
-
-    explicit BagOfTasksProcessor(size_t countThreads);
-
-    ~BagOfTasksProcessor();
-
-    Handle* Submit(BagOfTasks& tasks);
-  };
-}
--- a/Core/MultiThreading/ILockable.h	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +0,0 @@
-/**
- * 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 <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class ILockable : public boost::noncopyable
-  {
-    friend class Locker;
-
-  protected:
-    virtual void Lock() = 0;
-
-    virtual void Unlock() = 0;
-
-  public:
-    virtual ~ILockable()
-    {
-    }
-  };
-}
--- a/Core/MultiThreading/Locker.h	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-/**
- * 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 "ILockable.h"
-
-namespace Orthanc
-{
-  class Locker : public boost::noncopyable
-  {
-  private:
-    ILockable& lockable_;
-
-  public:
-    Locker(ILockable& lockable) : lockable_(lockable)
-    {
-      lockable_.Lock();
-    }
-
-    virtual ~Locker()
-    {
-      lockable_.Unlock();
-    }
-  };
-}
--- a/Core/MultiThreading/Mutex.cpp	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,122 +0,0 @@
-/**
- * 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 "../PrecompiledHeaders.h"
-#include "Mutex.h"
-
-#include "../OrthancException.h"
-
-#if defined(_WIN32)
-#include <windows.h>
-#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__)
-#include <pthread.h>
-#else
-#error Support your platform here
-#endif
-
-namespace Orthanc
-{
-#if defined (_WIN32)
-
-  struct Mutex::PImpl
-  {
-    CRITICAL_SECTION criticalSection_;
-  };
-
-  Mutex::Mutex()
-  {
-    pimpl_ = new PImpl;
-    ::InitializeCriticalSection(&pimpl_->criticalSection_);
-  }
-
-  Mutex::~Mutex()
-  {
-    ::DeleteCriticalSection(&pimpl_->criticalSection_);
-    delete pimpl_;
-  }
-
-  void Mutex::Lock()
-  {
-    ::EnterCriticalSection(&pimpl_->criticalSection_);
-  }
-
-  void Mutex::Unlock()
-  {
-    ::LeaveCriticalSection(&pimpl_->criticalSection_);
-  }
-
-
-#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__)
-
-  struct Mutex::PImpl
-  {
-    pthread_mutex_t mutex_;
-  };
-
-  Mutex::Mutex()
-  {
-    pimpl_ = new PImpl;
-
-    if (pthread_mutex_init(&pimpl_->mutex_, NULL) != 0)
-    {
-      delete pimpl_;
-      throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-  Mutex::~Mutex()
-  {
-    pthread_mutex_destroy(&pimpl_->mutex_);
-    delete pimpl_;
-  }
-
-  void Mutex::Lock()
-  {
-    if (pthread_mutex_lock(&pimpl_->mutex_) != 0)
-    {
-      throw OrthancException(ErrorCode_InternalError);    
-    }
-  }
-
-  void Mutex::Unlock()
-  {
-    if (pthread_mutex_unlock(&pimpl_->mutex_) != 0)
-    {
-      throw OrthancException(ErrorCode_InternalError);    
-    }
-  }
-
-#else
-#error Support your plateform here
-#endif
-}
--- a/Core/MultiThreading/Mutex.h	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-/**
- * 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 "ILockable.h"
-
-namespace Orthanc
-{
-  class Mutex : public ILockable
-  {
-  private:
-    struct PImpl;
-
-    PImpl *pimpl_;
-
-  protected:
-    virtual void Lock();
-
-    virtual void Unlock();
-    
-  public:
-    Mutex();
-
-    ~Mutex();
-  };
-}
--- a/Core/MultiThreading/ReaderWriterLock.cpp	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,126 +0,0 @@
-/**
- * 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 "../PrecompiledHeaders.h"
-#include "ReaderWriterLock.h"
-
-#include <boost/thread/shared_mutex.hpp>
-
-namespace Orthanc
-{
-  namespace
-  {
-    // Anonymous namespace to avoid clashes between compilation
-    // modules.
-
-    class ReaderLockable : public ILockable
-    {
-    private:
-      boost::shared_mutex& lock_;
-
-    protected:
-      virtual void Lock()
-      {
-        lock_.lock_shared();
-      }
-
-      virtual void Unlock()
-      {
-        lock_.unlock_shared();        
-      }
-
-    public:
-      explicit ReaderLockable(boost::shared_mutex& lock) : lock_(lock)
-      {
-      }
-    };
-
-
-    class WriterLockable : public ILockable
-    {
-    private:
-      boost::shared_mutex& lock_;
-
-    protected:
-      virtual void Lock()
-      {
-        lock_.lock();
-      }
-
-      virtual void Unlock()
-      {
-        lock_.unlock();        
-      }
-
-    public:
-      explicit WriterLockable(boost::shared_mutex& lock) : lock_(lock)
-      {
-      }
-    };
-  }
-
-  struct ReaderWriterLock::PImpl
-  {
-    boost::shared_mutex lock_;
-    ReaderLockable reader_;
-    WriterLockable writer_;
-
-    PImpl() : reader_(lock_), writer_(lock_)
-    {
-    }
-  };
-
-
-  ReaderWriterLock::ReaderWriterLock()
-  {
-    pimpl_ = new PImpl;
-  }
-
-
-  ReaderWriterLock::~ReaderWriterLock()
-  {
-    delete pimpl_;
-  }
-
-
-  ILockable&  ReaderWriterLock::ForReader()
-  {
-    return pimpl_->reader_;
-  }
-
-
-  ILockable&  ReaderWriterLock::ForWriter()
-  {
-    return pimpl_->writer_;
-  }
-}
--- a/Core/MultiThreading/ReaderWriterLock.h	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,58 +0,0 @@
-/**
- * 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 "ILockable.h"
-
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class ReaderWriterLock : public boost::noncopyable
-  {
-  private:
-    struct PImpl;
-
-    PImpl *pimpl_;
-
-  public:
-    ReaderWriterLock();
-
-    virtual ~ReaderWriterLock();
-
-    ILockable& ForReader();
-
-    ILockable& ForWriter();
-  };
-}
--- a/Core/MultiThreading/Semaphore.cpp	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,65 +0,0 @@
-/**
- * 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 "../PrecompiledHeaders.h"
-#include "Semaphore.h"
-
-#include "../OrthancException.h"
-
-
-namespace Orthanc
-{
-  Semaphore::Semaphore(unsigned int count) : count_(count)
-  {
-  }
-
-  void Semaphore::Release()
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    count_++;
-    condition_.notify_one(); 
-  }
-
-  void Semaphore::Acquire()
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    while (count_ == 0)
-    {
-      condition_.wait(lock);
-    }
-
-    count_++;
-  }
-}
--- a/Core/MultiThreading/Semaphore.h	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,73 +0,0 @@
-/**
- * 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 <boost/noncopyable.hpp>
-#include <boost/thread.hpp>
-
-namespace Orthanc
-{
-  class Semaphore : public boost::noncopyable
-  {
-  private:
-    unsigned int count_;
-    boost::mutex mutex_;
-    boost::condition_variable condition_;
-
-  public:
-    explicit Semaphore(unsigned int count);
-
-    void Release();
-
-    void Acquire();
-
-    class Locker : public boost::noncopyable
-    {
-    private:
-      Semaphore&  that_;
-
-    public:
-      explicit Locker(Semaphore& that) :
-        that_(that)
-      {
-        that_.Acquire();
-      }
-
-      ~Locker()
-      {
-        that_.Release();
-      }
-    };
-  };
-}
--- a/NEWS	Wed May 23 10:08:04 2018 +0200
+++ b/NEWS	Wed May 23 14:28:25 2018 +0200
@@ -1,6 +1,11 @@
 Pending changes in the mainline
 ===============================
 
+General
+-------
+
+* New advanced job engine
+
 Orthanc Explorer
 ----------------
 
@@ -9,6 +14,7 @@
 REST API
 --------
 
+* "/jobs/..." to manage the jobs from the REST API
 * New option "?short" to list DICOM tags using their hexadecimal ID in:
   - "/instances/.../tags?short"
   - "/instances/.../header?short"
@@ -27,6 +33,7 @@
 -----------
 
 * Fix generation of DICOMDIR if PatientID is empty
+* Fix issue 25 (Deadlock with Lua scripts): The event queue is now implemented for Lua
 
 
 Version 1.3.2 (2018-04-18)
--- a/OrthancExplorer/explorer.html	Wed May 23 10:08:04 2018 +0200
+++ b/OrthancExplorer/explorer.html	Wed May 23 14:28:25 2018 +0200
@@ -44,6 +44,7 @@
         <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
           <a href="#upload" data-icon="gear" data-role="button">Upload</a>
           <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+          <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a>
         </div>
       </div>
       <div data-role="content">
@@ -62,6 +63,7 @@
         <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
           <a href="#upload" data-icon="gear" data-role="button">Upload</a>
           <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+          <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a>
         </div>
       </div>
       <div data-role="content">
@@ -109,6 +111,7 @@
         <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
           <a href="#upload" data-icon="gear" data-role="button">Upload</a>
           <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+          <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a>
         </div>
       </div>
       <div data-role="content">
@@ -169,6 +172,7 @@
         <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
           <a href="#upload" data-icon="gear" data-role="button">Upload</a>
           <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+          <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a>
         </div>
       </div>
       <div data-role="content">
@@ -223,6 +227,7 @@
         <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
           <a href="#upload" data-icon="gear" data-role="button">Upload</a>
           <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+          <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a>
         </div>
       </div>
       <div data-role="content">
@@ -279,6 +284,7 @@
         <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
           <a href="#upload" data-icon="gear" data-role="button">Upload</a>
           <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+          <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a>
         </div>
       </div>
       <div data-role="content">
@@ -468,6 +474,48 @@
       </div>
     </div>
 
+    
+    <div data-role="page" id="jobs" >
+      <div data-role="header" >
+	<h1><span class="orthanc-name"></span>Jobs</h1>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left">
+          <a href="#find-patients" data-icon="home" data-role="button" data-direction="reverse">Patients</a>
+          <a href="#find-studies" data-icon="arrow-r" data-role="button" data-direction="reverse">Studies</a>
+        </div>
+      </div>
+      <div data-role="content">
+        <ul id="all-jobs" data-role="listview" data-inset="true" data-filter="true">
+        </ul>
+      </div>
+    </div>
+
+    <div data-role="page" id="job" >
+      <div data-role="header" >
+	<h1><span class="orthanc-name"></span>Job</h1>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left">
+          <a href="#find-patients" data-icon="home" data-role="button" data-direction="reverse">Patients</a>
+          <a href="#find-studies" data-icon="arrow-r" data-role="button" data-direction="reverse">Studies</a>
+        </div>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
+          <a href="#jobs" data-icon="refresh" data-role="button" data-direction="reverse">Jobs</a>
+        </div>
+      </div>
+      <div data-role="content">
+        <ul data-role="listview" data-inset="true" data-filter="true" id="job-info">
+        </ul>
+
+        <fieldset class="ui-grid-b">
+          <div class="ui-block-a"></div>
+	  <div class="ui-block-b">
+            <button id="job-cancel" data-theme="b">Cancel job</button>         
+            <button id="job-resubmit" data-theme="b">Resubmit job</button>         
+            <button id="job-pause" data-theme="b">Pause job</button>         
+            <button id="job-resume" data-theme="b">Resume job</button>         
+          </div>
+          <div class="ui-block-c"></div>
+	</fieldset>
+      </div>
+    </div>
 
     <div id="peer-store" style="display:none;" class="ui-body-c">
       <p align="center"><b>Sending to Orthanc peer...</b></p>
--- a/OrthancExplorer/explorer.js	Wed May 23 10:08:04 2018 +0200
+++ b/OrthancExplorer/explorer.js	Wed May 23 14:28:25 2018 +0200
@@ -1156,3 +1156,209 @@
     }
   });
 });
+
+
+
+function ParseJobTime(s)
+{
+  var t = (s.substr(0, 4) + '-' +
+           s.substr(4, 2) + '-' +
+           s.substr(6, 5) + ':' +
+           s.substr(11, 2) + ':' +
+           s.substr(13));
+  var utc = new Date(t);
+
+  // Convert from UTC to local time
+  return new Date(utc.getTime() - utc.getTimezoneOffset() * 60000);
+}
+
+
+function AddJobField(target, description, field)
+{
+  if (!(typeof field === 'undefined')) {
+    target.append($('<p>')
+                  .text(description)
+                  .append($('<strong>').text(field)));
+  }
+}
+
+
+function AddJobDateField(target, description, field)
+{
+  if (!(typeof field === 'undefined')) {
+    target.append($('<p>')
+                  .text(description)
+                  .append($('<strong>').text(ParseJobTime(field))));
+  }
+}
+
+
+$('#jobs').live('pagebeforeshow', function() {
+  $.ajax({
+    url: '../jobs?expand',
+    dataType: 'json',
+    async: false,
+    cache: false,
+    success: function(jobs) {
+      var target = $('#all-jobs');
+      $('li', target).remove();
+
+      var running = $('<li>')
+          .attr('data-role', 'list-divider')
+          .text('Currently running');
+
+      var pending = $('<li>')
+          .attr('data-role', 'list-divider')
+          .text('Pending jobs');
+
+      var inactive = $('<li>')
+          .attr('data-role', 'list-divider')
+          .text('Inactive jobs');
+
+      target.append(running);
+      target.append(pending);
+      target.append(inactive);
+
+      jobs.map(function(job) {
+        var li = $('<li>');
+        var item = $('<a>');
+        li.append(item);
+        item.attr('href', '#job?uuid=' + job.ID);
+        item.append($('<h1>').text(job.Type));
+        item.append($('<span>').addClass('ui-li-count').text(job.State));
+        AddJobField(item, 'ID: ', job.ID);
+        AddJobField(item, 'Local AET: ', job.PublicContent.LocalAet);
+        AddJobField(item, 'Remote AET: ', job.PublicContent.RemoteAet);
+        AddJobDateField(item, 'Creation time: ', job.CreationTime);
+        AddJobDateField(item, 'Completion time: ', job.CompletionTime);
+        AddJobDateField(item, 'ETA: ', job.EstimatedTimeOfArrival);
+
+        if (job.State == 'Running' ||
+            job.State == 'Pending' ||
+            job.State == 'Paused') {
+          AddJobField(item, 'Priority: ', job.Priority);
+          AddJobField(item, 'Progress: ', job.Progress);
+        }
+        
+        if (job.State == 'Running') {
+          li.insertAfter(running);
+        } else if (job.State == 'Pending' ||
+                   job.State == 'Paused') {
+          li.insertAfter(pending);
+        } else {
+          li.insertAfter(inactive);
+        }
+      });
+
+      target.listview('refresh');
+    }
+  });
+});
+
+
+$('#job').live('pagebeforeshow', function() {
+  if ($.mobile.pageData) {
+    var pageData = DeepCopy($.mobile.pageData);
+
+    $.ajax({
+      url: '../jobs/' + pageData.uuid,
+      dataType: 'json',
+      async: false,
+      cache: false,
+      success: function(job) {
+        var target = $('#job-info');
+        $('li', target).remove();
+
+        target.append($('<li>')
+                      .attr('data-role', 'list-divider')
+                      .text('General information about the job'));
+
+        var block = $('<li>');
+        for (var i in job) {
+          if (i == 'CreationTime' ||
+              i == 'CompletionTime' ||
+              i == 'EstimatedTimeOfArrival') {
+            AddJobDateField(block, i + ': ', job[i]);
+          } else if (i != 'InternalContent' &&
+                     i != 'PublicContent' &&
+                     i != 'Timestamp') {
+            AddJobField(block, i + ': ', job[i]);
+          }
+        }
+
+        target.append(block);
+        
+        target.append($('<li>')
+                      .attr('data-role', 'list-divider')
+                      .text('Detailed information'));
+
+        var block = $('<li>');
+
+        for (var item in job.PublicContent) {
+          var value = job.PublicContent[item];
+          if (typeof value !== 'string') {
+            value = JSON.stringify(value);
+          }
+          
+          AddJobField(block, item + ': ', value);
+        }
+
+        target.append(block);
+        
+        target.listview('refresh');
+
+        $('#job-cancel').closest('.ui-btn').hide();
+        $('#job-retry').closest('.ui-btn').hide();
+        $('#job-resubmit').closest('.ui-btn').hide();
+        $('#job-pause').closest('.ui-btn').hide();
+        $('#job-resume').closest('.ui-btn').hide();
+
+        if (job.State == 'Running' ||
+            job.State == 'Pending' ||
+            job.State == 'Retry') {
+          $('#job-cancel').closest('.ui-btn').show();
+          $('#job-pause').closest('.ui-btn').show();
+        }
+        else if (job.State == 'Success') {
+        }
+        else if (job.State == 'Failure') {
+          $('#job-resubmit').closest('.ui-btn').show();
+        }
+        else if (job.State == 'Paused') {
+          $('#job-resume').closest('.ui-btn').show();
+        }
+      }
+    });
+  }
+});
+
+
+
+function TriggerJobAction(action)
+{
+  $.ajax({
+    url: '../jobs/' + $.mobile.pageData.uuid + '/' + action,
+    type: 'POST',
+    async: false,
+    cache: false,
+    complete: function(s) {
+      window.location.reload();
+    }
+  });
+}
+
+$('#job-cancel').live('click', function() {
+  TriggerJobAction('cancel');
+});
+
+$('#job-resubmit').live('click', function() {
+  TriggerJobAction('resubmit');
+});
+
+$('#job-pause').live('click', function() {
+  TriggerJobAction('pause');
+});
+
+$('#job-resume').live('click', function() {
+  TriggerJobAction('resume');
+});
--- a/OrthancServer/IServerListener.h	Wed May 23 10:08:04 2018 +0200
+++ b/OrthancServer/IServerListener.h	Wed May 23 14:28:25 2018 +0200
@@ -40,7 +40,7 @@
 
 namespace Orthanc
 {
-  class IServerListener
+  class IServerListener : public boost::noncopyable
   {
   public:
     virtual ~IServerListener()
--- a/OrthancServer/LuaScripting.cpp	Wed May 23 10:08:04 2018 +0200
+++ b/OrthancServer/LuaScripting.cpp	Wed May 23 14:28:25 2018 +0200
@@ -34,24 +34,149 @@
 #include "PrecompiledHeadersServer.h"
 #include "LuaScripting.h"
 
+#include "OrthancInitialization.h"
+#include "OrthancRestApi/OrthancRestApi.h"
 #include "ServerContext.h"
-#include "OrthancInitialization.h"
-#include "../Core/Lua/LuaFunctionCall.h"
+
 #include "../Core/HttpServer/StringHttpOutput.h"
 #include "../Core/Logging.h"
-
-#include "Scheduler/DeleteInstanceCommand.h"
-#include "Scheduler/StoreScuCommand.h"
-#include "Scheduler/StorePeerCommand.h"
-#include "Scheduler/ModifyInstanceCommand.h"
-#include "Scheduler/CallSystemCommand.h"
-#include "OrthancRestApi/OrthancRestApi.h"
+#include "../Core/Lua/LuaFunctionCall.h"
 
 #include <EmbeddedResources.h>
 
 
 namespace Orthanc
 {
+  class LuaScripting::IEvent : public IDynamicObject
+  {
+  public:
+    virtual void Apply(LuaScripting& lock) = 0;
+  };
+
+
+  class LuaScripting::OnStoredInstanceEvent : public LuaScripting::IEvent
+  {
+  private:
+    std::string    instanceId_;
+    Json::Value    simplifiedTags_;
+    Json::Value    metadata_;
+    Json::Value    origin_;
+
+  public:
+    OnStoredInstanceEvent(const std::string& instanceId,
+                          const Json::Value& simplifiedTags,
+                          const Json::Value& metadata,
+                          const DicomInstanceToStore& instance) :
+      instanceId_(instanceId),
+      simplifiedTags_(simplifiedTags),
+      metadata_(metadata)
+    {
+      instance.GetOriginInformation(origin_);
+    }
+
+    virtual void Apply(LuaScripting& that)
+    {
+      static const char* NAME = "OnStoredInstance";
+
+      LuaScripting::Lock lock(that);
+
+      if (lock.GetLua().IsExistingFunction(NAME))
+      {
+        that.InitializeJob();
+
+        LuaFunctionCall call(lock.GetLua(), NAME);
+        call.PushString(instanceId_);
+        call.PushJson(simplifiedTags_);
+        call.PushJson(metadata_);
+        call.PushJson(origin_);
+        call.Execute();
+
+        that.SubmitJob();
+      }
+    }
+  };
+
+
+  class LuaScripting::ExecuteEvent : public LuaScripting::IEvent
+  {
+  private:
+    std::string    command_;
+
+  public:
+    ExecuteEvent(const std::string& command) :
+      command_(command)
+    {
+    }
+
+    virtual void Apply(LuaScripting& that)
+    {
+      LuaScripting::Lock lock(that);
+
+      if (lock.GetLua().IsExistingFunction(command_.c_str()))
+      {
+        LuaFunctionCall call(lock.GetLua(), command_.c_str());
+        call.Execute();
+      }
+    }
+  };
+
+
+  class LuaScripting::StableResourceEvent : public LuaScripting::IEvent
+  {
+  private:
+    ServerIndexChange  change_;
+
+  public:
+    StableResourceEvent(const ServerIndexChange& change) :
+    change_(change)
+    {
+    }
+
+    virtual void Apply(LuaScripting& that)
+    {
+      const char* name;
+
+      switch (change_.GetChangeType())
+      {
+        case ChangeType_StablePatient:
+          name = "OnStablePatient";
+          break;
+
+        case ChangeType_StableStudy:
+          name = "OnStableStudy";
+          break;
+
+        case ChangeType_StableSeries:
+          name = "OnStableSeries";
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      Json::Value tags, metadata;
+      if (that.context_.GetIndex().LookupResource(tags, change_.GetPublicId(), change_.GetResourceType()) &&
+          that.context_.GetIndex().GetMetadata(metadata, change_.GetPublicId()))
+      {
+        LuaScripting::Lock lock(that);
+
+        if (lock.GetLua().IsExistingFunction(name))
+        {
+          that.InitializeJob();
+
+          LuaFunctionCall call(lock.GetLua(), name);
+          call.PushString(change_.GetPublicId());
+          call.PushJson(tags["MainDicomTags"]);
+          call.PushJson(metadata);
+          call.Execute();
+
+          that.SubmitJob();
+        }
+      }
+    }
+  };
+
+
   ServerContext* LuaScripting::GetServerContext(lua_State *state)
   {
     const void* value = LuaContext::GetGlobalVariable(state, "_ServerContext");
@@ -211,7 +336,7 @@
     }
 
     LOG(ERROR) << "Lua: Error in RestApiDelete() for URI: " << uri;
-      lua_pushnil(state);
+    lua_pushnil(state);
 
     return 1;
   }
@@ -229,13 +354,14 @@
   }
 
 
-  IServerCommand* LuaScripting::ParseOperation(const std::string& operation,
-                                               const Json::Value& parameters)
+  size_t LuaScripting::ParseOperation(LuaJobManager::Lock& lock,
+                                      const std::string& operation,
+                                      const Json::Value& parameters)
   {
     if (operation == "delete")
     {
       LOG(INFO) << "Lua script to delete resource " << parameters["Resource"].asString();
-      return new DeleteInstanceCommand(context_);
+      return lock.AddDeleteResourceOperation(context_);
     }
 
     if (operation == "store-scu")
@@ -250,36 +376,29 @@
         localAet = context_.GetDefaultLocalApplicationEntityTitle();
       }
 
-      std::string modality = parameters["Modality"].asString();
-      LOG(INFO) << "Lua script to send resource " << parameters["Resource"].asString()
-                << " to modality " << modality << " using Store-SCU";
+      std::string name = parameters["Modality"].asString();
+      RemoteModalityParameters modality = Configuration::GetModalityUsingSymbolicName(name);
 
       // This is not a C-MOVE: No need to call "StoreScuCommand::SetMoveOriginator()"
-      return new StoreScuCommand(context_, localAet,
-                                 Configuration::GetModalityUsingSymbolicName(modality), true);
+      return lock.AddStoreScuOperation(localAet, modality);
     }
 
     if (operation == "store-peer")
     {
-      std::string peer = parameters["Peer"].asString();
-      LOG(INFO) << "Lua script to send resource " << parameters["Resource"].asString()
-                << " to peer " << peer << " using HTTP";
+      std::string name = parameters["Peer"].asString();
 
-      WebServiceParameters parameters;
-      Configuration::GetOrthancPeer(parameters, peer);
-      return new StorePeerCommand(context_, parameters, true);
+      WebServiceParameters peer;
+      Configuration::GetOrthancPeer(peer, name);
+
+      return lock.AddStorePeerOperation(peer);
     }
 
     if (operation == "modify")
     {
-      LOG(INFO) << "Lua script to modify resource " << parameters["Resource"].asString();
       std::auto_ptr<DicomModification> modification(new DicomModification);
       modification->ParseModifyRequest(parameters);
 
-      std::auto_ptr<ModifyInstanceCommand> command
-        (new ModifyInstanceCommand(context_, RequestOrigin_Lua, modification.release()));
-
-      return command.release();
+      return lock.AddModifyInstanceOperation(context_, modification.release());
     }
 
     if (operation == "call-system")
@@ -320,7 +439,10 @@
         }
       }
 
-      return new CallSystemCommand(context_, parameters["Command"].asString(), args);
+      std::string command = parameters["Command"].asString();
+      std::vector<std::string> postArgs;
+
+      return lock.AddSystemCallOperation(command, args, postArgs);
     }
 
     throw OrthancException(ErrorCode_ParameterOutOfRange);
@@ -333,7 +455,7 @@
   }
 
 
-  void LuaScripting::SubmitJob(const std::string& description)
+  void LuaScripting::SubmitJob()
   {
     Json::Value operations;
     LuaFunctionCall call2(lua_, "_AccessJob");
@@ -344,8 +466,10 @@
       throw OrthancException(ErrorCode_InternalError);
     }
 
-    ServerJob job;
-    ServerCommandInstance* previousCommand = NULL;
+    LuaJobManager::Lock lock(jobManager_, context_.GetJobsEngine());
+
+    bool isFirst = true;
+    size_t previous;
 
     for (Json::Value::ArrayIndex i = 0; i < operations.size(); ++i)
     {
@@ -356,34 +480,33 @@
       }
 
       const Json::Value& parameters = operations[i];
-      std::string operation = parameters["Operation"].asString();
-
-      ServerCommandInstance& command = job.AddCommand(ParseOperation(operation, operations[i]));
-        
       if (!parameters.isMember("Resource"))
       {
         throw OrthancException(ErrorCode_InternalError);
       }
 
+      std::string operation = parameters["Operation"].asString();
+      size_t index = ParseOperation(lock, operation, operations[i]);
+        
       std::string resource = parameters["Resource"].asString();
-      if (resource.empty())
+      if (!resource.empty())
       {
-        previousCommand->ConnectOutput(command);
+        lock.AddDicomInstanceInput(index, context_, resource);
       }
-      else 
+      else if (!isFirst)
       {
-        command.AddInput(resource);
+        lock.Connect(previous, index);
       }
 
-      previousCommand = &command;
+      isFirst = false;
+      previous = index;
     }
-
-    job.SetDescription(description);
-    context_.GetScheduler().Submit(job);
   }
 
 
-  LuaScripting::LuaScripting(ServerContext& context) : context_(context)
+  LuaScripting::LuaScripting(ServerContext& context) : 
+    context_(context),
+    continue_(true)
   {
     lua_.SetGlobalVariable("_ServerContext", &context);
     lua_.RegisterFunction("RestApiGet", RestApiGet);
@@ -391,34 +514,87 @@
     lua_.RegisterFunction("RestApiPut", RestApiPut);
     lua_.RegisterFunction("RestApiDelete", RestApiDelete);
     lua_.RegisterFunction("GetOrthancConfiguration", GetOrthancConfiguration);
+  }
 
-    lua_.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX);
+
+  LuaScripting::~LuaScripting()
+  {
+    if (continue_)
+    {
+      LOG(ERROR) << "INTERNAL ERROR: LuaScripting::Stop() should be invoked manually to avoid mess in the destruction order!";
+      Stop();
+    }
   }
 
 
-  void LuaScripting::ApplyOnStoredInstance(const std::string& instanceId,
-                                           const Json::Value& simplifiedTags,
-                                           const Json::Value& metadata,
-                                           const DicomInstanceToStore& instance)
+  void LuaScripting::EventThread(LuaScripting* that)
   {
-    static const char* NAME = "OnStoredInstance";
+    for (;;)
+    {
+      std::auto_ptr<IDynamicObject> event(that->pendingEvents_.Dequeue(100));
+
+      if (event.get() == NULL)
+      {
+        // The event queue is empty, check whether we should stop
+        boost::recursive_mutex::scoped_lock lock(that->mutex_);
 
-    if (lua_.IsExistingFunction(NAME))
-    {
-      InitializeJob();
+        if (!that->continue_)
+        {
+          return;
+        }
+      }
+      else
+      {
+        try
+        {
+          dynamic_cast<IEvent&>(*event).Apply(*that);
+        }
+        catch (OrthancException& e)
+        {
+          LOG(ERROR) << "Error while processing Lua events: " << e.What();
+        }
+      }
+    }
+  }
+
 
-      LuaFunctionCall call(lua_, NAME);
-      call.PushString(instanceId);
-      call.PushJson(simplifiedTags);
-      call.PushJson(metadata);
+  void LuaScripting::Start()
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
+
+    if (!continue_ ||
+        eventThread_.joinable()  /* already started */)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      LOG(INFO) << "Starting the Lua engine";
+      eventThread_ = boost::thread(EventThread, this);
+    }
+  }
+
 
-      Json::Value origin;
-      instance.GetOriginInformation(origin);
-      call.PushJson(origin);
+  void LuaScripting::Stop()
+  {
+    {
+      boost::recursive_mutex::scoped_lock lock(mutex_);
+
+      if (!continue_)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
 
-      call.Execute();
+      continue_ = false;
+    }
+
+    jobManager_.AwakeTrailingSleep();
 
-      SubmitJob(std::string("Lua script: ") + NAME);
+    if (eventThread_.joinable())
+    {
+      LOG(INFO) << "Stopping the Lua engine";
+      eventThread_.join();
+      LOG(INFO) << "The Lua engine has stopped";
     }
   }
 
@@ -427,8 +603,6 @@
                                           DicomInstanceToStore& instance,
                                           const Json::Value& simplifiedTags)
   {
-    boost::recursive_mutex::scoped_lock lock(mutex_);
-
     Json::Value metadata = Json::objectValue;
 
     for (ServerIndex::MetadataMap::const_iterator 
@@ -441,64 +615,17 @@
       }
     }
 
-    ApplyOnStoredInstance(publicId, simplifiedTags, metadata, instance);
+    pendingEvents_.Enqueue(new OnStoredInstanceEvent(publicId, simplifiedTags, metadata, instance));
   }
 
 
-  
-  void LuaScripting::OnStableResource(const ServerIndexChange& change)
-  {
-    const char* name;
-
-    switch (change.GetChangeType())
-    {
-      case ChangeType_StablePatient:
-        name = "OnStablePatient";
-        break;
-
-      case ChangeType_StableStudy:
-        name = "OnStableStudy";
-        break;
-
-      case ChangeType_StableSeries:
-        name = "OnStableSeries";
-        break;
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-
-
-    Json::Value tags, metadata;
-    if (context_.GetIndex().LookupResource(tags, change.GetPublicId(), change.GetResourceType()) &&
-        context_.GetIndex().GetMetadata(metadata, change.GetPublicId()))
-    {
-      boost::recursive_mutex::scoped_lock lock(mutex_);
-
-      if (lua_.IsExistingFunction(name))
-      {
-        InitializeJob();
-
-        LuaFunctionCall call(lua_, name);
-        call.PushString(change.GetPublicId());
-        call.PushJson(tags["MainDicomTags"]);
-        call.PushJson(metadata);
-        call.Execute();
-
-        SubmitJob(std::string("Lua script: ") + name);
-      }
-    }
-  }
-
-
-
   void LuaScripting::SignalChange(const ServerIndexChange& change)
   {
     if (change.GetChangeType() == ChangeType_StablePatient ||
         change.GetChangeType() == ChangeType_StableStudy ||
         change.GetChangeType() == ChangeType_StableSeries)
     {
-      OnStableResource(change);
+      pendingEvents_.Enqueue(new StableResourceEvent(change));
     }
   }
 
@@ -531,12 +658,28 @@
 
   void LuaScripting::Execute(const std::string& command)
   {
-    LuaScripting::Locker locker(*this);
-      
-    if (locker.GetLua().IsExistingFunction(command.c_str()))
+    pendingEvents_.Enqueue(new ExecuteEvent(command));
+  }
+
+
+  void LuaScripting::LoadGlobalConfiguration()
+  {
+    lua_.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX);
+
+    std::list<std::string> luaScripts;
+    Configuration::GetGlobalListOfStringsParameter(luaScripts, "LuaScripts");
+
+    LuaScripting::Lock lock(*this);
+
+    for (std::list<std::string>::const_iterator
+           it = luaScripts.begin(); it != luaScripts.end(); ++it)
     {
-      LuaFunctionCall call(locker.GetLua(), command.c_str());
-      call.Execute();
+      std::string path = Configuration::InterpretStringParameterAsPath(*it);
+      LOG(INFO) << "Installing the Lua scripts from: " << path;
+      std::string script;
+      SystemToolbox::ReadFile(script, path);
+
+      lock.GetLua().Execute(script);
     }
   }
 }
--- a/OrthancServer/LuaScripting.h	Wed May 23 10:08:04 2018 +0200
+++ b/OrthancServer/LuaScripting.h	Wed May 23 14:28:25 2018 +0200
@@ -34,8 +34,11 @@
 #pragma once
 
 #include "IServerListener.h"
+
+#include "ServerJobs/LuaJobManager.h"
+
+#include "../Core/MultiThreading/SharedMessageQueue.h"
 #include "../Core/Lua/LuaContext.h"
-#include "Scheduler/IServerCommand.h"
 
 namespace Orthanc
 {
@@ -44,6 +47,11 @@
   class LuaScripting : public IServerListener
   {
   private:
+    class ExecuteEvent;
+    class IEvent;
+    class OnStoredInstanceEvent;
+    class StableResourceEvent;
+
     static ServerContext* GetServerContext(lua_State *state);
 
     static int RestApiPostOrPut(lua_State *state,
@@ -54,33 +62,33 @@
     static int RestApiDelete(lua_State *state);
     static int GetOrthancConfiguration(lua_State *state);
 
-    void ApplyOnStoredInstance(const std::string& instanceId,
-                               const Json::Value& simplifiedDicom,
-                               const Json::Value& metadata,
-                               const DicomInstanceToStore& instance);
-
-    IServerCommand* ParseOperation(const std::string& operation,
-                                   const Json::Value& parameters);
+    size_t ParseOperation(LuaJobManager::Lock& lock,
+                          const std::string& operation,
+                          const Json::Value& parameters);
 
     void InitializeJob();
 
-    void SubmitJob(const std::string& description);
-
-    void OnStableResource(const ServerIndexChange& change);
+    void SubmitJob();
 
-    boost::recursive_mutex    mutex_;
-    LuaContext      lua_;
-    ServerContext&  context_;
+    boost::recursive_mutex   mutex_;
+    LuaContext               lua_;
+    ServerContext&           context_;
+    LuaJobManager            jobManager_;
+    bool                     continue_;
+    boost::thread            eventThread_;
+    SharedMessageQueue       pendingEvents_;
+
+    static void EventThread(LuaScripting* that);
 
   public:
-    class Locker : public boost::noncopyable
+    class Lock : public boost::noncopyable
     {
     private:
-      LuaScripting& that_;
-      boost::recursive_mutex::scoped_lock lock_;
+      LuaScripting&                        that_;
+      boost::recursive_mutex::scoped_lock  lock_;
 
     public:
-      Locker(LuaScripting& that) : 
+      explicit Lock(LuaScripting& that) : 
         that_(that), 
         lock_(that.mutex_)
       {
@@ -93,6 +101,12 @@
     };
 
     LuaScripting(ServerContext& context);
+
+    ~LuaScripting();
+
+    void Start();
+
+    void Stop();
     
     virtual void SignalStoredInstance(const std::string& publicId,
                                       DicomInstanceToStore& instance,
@@ -104,5 +118,7 @@
                                         const Json::Value& simplifiedTags);
 
     void Execute(const std::string& command);
+
+    void LoadGlobalConfiguration();
   };
 }
--- a/OrthancServer/OrthancFindRequestHandler.cpp	Wed May 23 10:08:04 2018 +0200
+++ b/OrthancServer/OrthancFindRequestHandler.cpp	Wed May 23 14:28:25 2018 +0200
@@ -485,9 +485,10 @@
                                                  const std::string& calledAet)
   {
     static const char* LUA_CALLBACK = "IncomingFindRequestFilter";
+    
+    LuaScripting::Lock lock(context_.GetLuaScripting());
 
-    LuaScripting::Locker locker(context_.GetLua());
-    if (!locker.GetLua().IsExistingFunction(LUA_CALLBACK))
+    if (!lock.GetLua().IsExistingFunction(LUA_CALLBACK))
     {
       return false;
     }
@@ -498,7 +499,7 @@
       origin["RemoteAet"] = remoteAet;
       origin["CalledAet"] = calledAet;
 
-      LuaFunctionCall call(locker.GetLua(), LUA_CALLBACK);
+      LuaFunctionCall call(lock.GetLua(), LUA_CALLBACK);
       call.PushDicom(source);
       call.PushJson(origin);
       FromDcmtkBridge::ExecuteToDicom(target, call);
@@ -508,6 +509,14 @@
   }
 
 
+  OrthancFindRequestHandler::OrthancFindRequestHandler(ServerContext& context) :
+    context_(context),
+    maxResults_(0),
+    maxInstances_(0)
+  {
+  }
+
+
   void OrthancFindRequestHandler::Handle(DicomFindAnswers& answers,
                                          const DicomMap& input,
                                          const std::list<DicomTag>& sequencesToReturn,
--- a/OrthancServer/OrthancFindRequestHandler.h	Wed May 23 10:08:04 2018 +0200
+++ b/OrthancServer/OrthancFindRequestHandler.h	Wed May 23 14:28:25 2018 +0200
@@ -42,8 +42,8 @@
   {
   private:
     ServerContext& context_;
-    unsigned int maxResults_;
-    unsigned int maxInstances_;
+    unsigned int   maxResults_;
+    unsigned int   maxInstances_;
 
     bool HasReachedLimit(const DicomFindAnswers& answers,
                          ResourceType level) const;
@@ -60,12 +60,7 @@
                         const std::string& calledAet);
 
   public:
-    OrthancFindRequestHandler(ServerContext& context) :
-      context_(context), 
-      maxResults_(0),
-      maxInstances_(0)
-    {
-    }
+    OrthancFindRequestHandler(ServerContext& context);
 
     virtual void Handle(DicomFindAnswers& answers,
                         const DicomMap& input,
--- a/OrthancServer/OrthancMoveRequestHandler.cpp	Wed May 23 10:08:04 2018 +0200
+++ b/OrthancServer/OrthancMoveRequestHandler.cpp	Wed May 23 14:28:25 2018 +0200
@@ -55,6 +55,7 @@
       RemoteModalityParameters remote_;
       std::string originatorAet_;
       uint16_t originatorId_;
+      std::auto_ptr<DicomUserConnection> connection_;
 
     public:
       OrthancMoveRequestIterator(ServerContext& context,
@@ -99,12 +100,13 @@
         std::string dicom;
         context_.ReadDicom(dicom, id);
 
+        if (connection_.get() == NULL)
         {
-          ReusableDicomUserConnection::Locker locker
-            (context_.GetReusableDicomUserConnection(), localAet_, remote_);
-          locker.GetConnection().Store(dicom, originatorAet_, originatorId_);
+          connection_.reset(new DicomUserConnection(localAet_, remote_));
         }
 
+        connection_->Store(dicom, originatorAet_, originatorId_);
+
         return Status_Success;
       }
     };
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Wed May 23 10:08:04 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Wed May 23 14:28:25 2018 +0200
@@ -34,16 +34,15 @@
 #include "../PrecompiledHeadersServer.h"
 #include "OrthancRestApi.h"
 
-#include "../OrthancInitialization.h"
-#include "../../Core/HttpClient.h"
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../../Core/Logging.h"
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
-#include "../Scheduler/ServerJob.h"
-#include "../Scheduler/StoreScuCommand.h"
-#include "../Scheduler/StorePeerCommand.h"
+#include "../OrthancInitialization.h"
 #include "../QueryRetrieveHandler.h"
+#include "../ServerJobs/DicomModalityStoreJob.h"
+#include "../ServerJobs/OrthancPeerStoreJob.h"
 #include "../ServerToolbox.h"
 
+
 namespace Orthanc
 {
   /***************************************************************************
@@ -55,12 +54,15 @@
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
-    RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
-    ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote);
+    RemoteModalityParameters remote =
+      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
     try
     {
-      if (locker.GetConnection().Echo())
+      DicomUserConnection connection(localAet, remote);
+      connection.Open();
+      
+      if (connection.Echo())
       {
         // Echo has succeeded
         call.GetOutput().AnswerBuffer("{}", "application/json");
@@ -176,11 +178,16 @@
     }
 
     const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
-    RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
-    ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote);
+    RemoteModalityParameters remote =
+      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+    
+    DicomFindAnswers answers(false);
 
-    DicomFindAnswers answers(false);
-    FindPatient(answers, locker.GetConnection(), fields);
+    {
+      DicomUserConnection connection(localAet, remote);
+      connection.Open();
+      FindPatient(answers, connection, fields);
+    }
 
     Json::Value result;
     answers.ToJson(result, true);
@@ -206,11 +213,16 @@
     }        
       
     const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
-    RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
-    ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote);
+    RemoteModalityParameters remote =
+      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
     DicomFindAnswers answers(false);
-    FindStudy(answers, locker.GetConnection(), fields);
+
+    {
+      DicomUserConnection connection(localAet, remote);
+      connection.Open();
+      FindStudy(answers, connection, fields);
+    }
 
     Json::Value result;
     answers.ToJson(result, true);
@@ -237,11 +249,16 @@
     }        
          
     const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
-    RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
-    ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote);
+    RemoteModalityParameters remote =
+      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
     DicomFindAnswers answers(false);
-    FindSeries(answers, locker.GetConnection(), fields);
+
+    {
+      DicomUserConnection connection(localAet, remote);
+      connection.Open();
+      FindSeries(answers, connection, fields);
+    }
 
     Json::Value result;
     answers.ToJson(result, true);
@@ -269,11 +286,16 @@
     }        
          
     const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
-    RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
-    ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote);
+    RemoteModalityParameters remote =
+      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
     DicomFindAnswers answers(false);
-    FindInstance(answers, locker.GetConnection(), fields);
+
+    {
+      DicomUserConnection connection(localAet, remote);
+      connection.Open();
+      FindInstance(answers, connection, fields);
+    }
 
     Json::Value result;
     answers.ToJson(result, true);
@@ -306,11 +328,14 @@
     }
  
     const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
-    RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
-    ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote);
+    RemoteModalityParameters remote =
+      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
+    DicomUserConnection connection(localAet, remote);
+    connection.Open();
+    
     DicomFindAnswers patients(false);
-    FindPatient(patients, locker.GetConnection(), m);
+    FindPatient(patients, connection, m);
 
     // Loop over the found patients
     Json::Value result = Json::arrayValue;
@@ -328,7 +353,7 @@
       CopyTagIfExists(m, patients.GetAnswer(i), DICOM_TAG_PATIENT_ID);
 
       DicomFindAnswers studies(false);
-      FindStudy(studies, locker.GetConnection(), m);
+      FindStudy(studies, connection, m);
 
       patient["Studies"] = Json::arrayValue;
       
@@ -348,7 +373,7 @@
         CopyTagIfExists(m, studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID);
 
         DicomFindAnswers series(false);
-        FindSeries(series, locker.GetConnection(), m);
+        FindSeries(series, connection, m);
 
         // Loop over the found series
         study["Series"] = Json::arrayValue;
@@ -671,6 +696,55 @@
   }
 
 
+  static void SubmitJob(RestApiPostCall& call,
+                        const Json::Value& request,
+                        const std::list<std::string>& instances,
+                        SetOfInstancesJob* jobRaw)
+  {
+    std::auto_ptr<SetOfInstancesJob> job(jobRaw);
+    
+    if (job.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    bool permissive = Toolbox::GetJsonBooleanField(request, "Permissive", false);
+    bool asynchronous = Toolbox::GetJsonBooleanField(request, "Asynchronous", false);
+    int priority = Toolbox::GetJsonIntegerField(request, "Priority", 0);
+
+    job->SetPermissive(permissive);
+    job->Reserve(instances.size());
+
+    for (std::list<std::string>::const_iterator 
+           it = instances.begin(); it != instances.end(); ++it)
+    {
+      job->AddInstance(*it);
+    }
+    
+    if (asynchronous)
+    {
+      // Asynchronous mode: Submit the job, but don't wait for its completion
+      std::string id;
+      context.GetJobsEngine().GetRegistry().Submit(id, job.release(), priority);
+
+      Json::Value v;
+      v["ID"] = id;
+      call.GetOutput().AnswerJson(v);
+    }
+    else if (context.GetJobsEngine().GetRegistry().SubmitAndWait(job.release(), priority))
+    {
+      // Synchronous mode: We have submitted and waited for completion
+      call.GetOutput().AnswerBuffer("{}", "application/json");
+    }
+    else
+    {
+      call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
+    }
+  }
+
+
   static void DicomStore(RestApiPostCall& call)
   {
     ServerContext& context = OrthancRestApi::GetContext(call);
@@ -684,51 +758,25 @@
       return;
     }
 
-    std::string localAet = Toolbox::GetJsonStringField(request, "LocalAet", context.GetDefaultLocalApplicationEntityTitle());
-    bool permissive = Toolbox::GetJsonBooleanField(request, "Permissive", false);
-    bool asynchronous = Toolbox::GetJsonBooleanField(request, "Asynchronous", false);
-    std::string moveOriginatorAET = Toolbox::GetJsonStringField(request, "MoveOriginatorAet", context.GetDefaultLocalApplicationEntityTitle());
-    int moveOriginatorID = Toolbox::GetJsonIntegerField(request, "MoveOriginatorID", 0 /* By default, not a C-MOVE */);
+    std::string localAet = Toolbox::GetJsonStringField
+      (request, "LocalAet", context.GetDefaultLocalApplicationEntityTitle());
+    std::string moveOriginatorAET = Toolbox::GetJsonStringField
+      (request, "MoveOriginatorAet", context.GetDefaultLocalApplicationEntityTitle());
+    int moveOriginatorID = Toolbox::GetJsonIntegerField
+      (request, "MoveOriginatorID", 0 /* By default, not a C-MOVE */);
 
-    if (moveOriginatorID < 0 || 
-        moveOriginatorID >= 65536)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    
     RemoteModalityParameters p = Configuration::GetModalityUsingSymbolicName(remote);
 
-    ServerJob job;
-    for (std::list<std::string>::const_iterator 
-           it = instances.begin(); it != instances.end(); ++it)
-    {
-      std::auto_ptr<StoreScuCommand> command(new StoreScuCommand(context, localAet, p, permissive));
+    std::auto_ptr<DicomModalityStoreJob> job(new DicomModalityStoreJob(context));
+    job->SetLocalAet(localAet);
+    job->SetRemoteModality(p);
 
-      if (moveOriginatorID != 0)
-      {
-        command->SetMoveOriginator(moveOriginatorAET, static_cast<uint16_t>(moveOriginatorID));
-      }
-
-      job.AddCommand(command.release()).AddInput(*it);
+    if (moveOriginatorID != 0)
+    {
+      job->SetMoveOriginator(moveOriginatorAET, moveOriginatorID);
     }
 
-    job.SetDescription("HTTP request: Store-SCU to peer \"" + remote + "\"");
-
-    if (asynchronous)
-    {
-      // Asynchronous mode: Submit the job, but don't wait for its completion
-      context.GetScheduler().Submit(job);
-      call.GetOutput().AnswerBuffer("{}", "application/json");
-    }
-    else if (context.GetScheduler().SubmitAndWait(job))
-    {
-      // Synchronous mode: We have submitted and waited for completion
-      call.GetOutput().AnswerBuffer("{}", "application/json");
-    }
-    else
-    {
-      call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
-    }
+    SubmitJob(call, request, instances, job.release());
   }
 
 
@@ -757,18 +805,23 @@
 
     ResourceType level = StringToResourceType(request["Level"].asCString());
     
-    std::string localAet = Toolbox::GetJsonStringField(request, "LocalAet", context.GetDefaultLocalApplicationEntityTitle());
-    std::string targetAet = Toolbox::GetJsonStringField(request, "TargetAet", context.GetDefaultLocalApplicationEntityTitle());
+    std::string localAet = Toolbox::GetJsonStringField
+      (request, "LocalAet", context.GetDefaultLocalApplicationEntityTitle());
+    std::string targetAet = Toolbox::GetJsonStringField
+      (request, "TargetAet", context.GetDefaultLocalApplicationEntityTitle());
 
-    const RemoteModalityParameters source = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
-      
+    const RemoteModalityParameters source =
+      Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+
+    DicomUserConnection connection(localAet, source);
+    connection.Open();
+    
     for (Json::Value::ArrayIndex i = 0; i < request[RESOURCES].size(); i++)
     {
       DicomMap resource;
       FromDcmtkBridge::FromJson(resource, request[RESOURCES][i]);
-
-      ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, source);
-      locker.GetConnection().Move(targetAet, level, resource);
+      
+      connection.Move(targetAet, level, resource);
     }
 
     // Move has succeeded
@@ -850,35 +903,13 @@
       return;
     }
 
-    bool asynchronous = Toolbox::GetJsonBooleanField(request, "Asynchronous", false);
-
     WebServiceParameters peer;
     Configuration::GetOrthancPeer(peer, remote);
 
-    ServerJob job;
-    for (std::list<std::string>::const_iterator 
-           it = instances.begin(); it != instances.end(); ++it)
-    {
-      job.AddCommand(new StorePeerCommand(context, peer, false)).AddInput(*it);
-    }
-
-    job.SetDescription("HTTP request: POST to peer \"" + remote + "\"");
+    std::auto_ptr<OrthancPeerStoreJob> job(new OrthancPeerStoreJob(context));
+    job->SetPeer(peer);    
 
-    if (asynchronous)
-    {
-      // Asynchronous mode: Submit the job, but don't wait for its completion
-      context.GetScheduler().Submit(job);
-      call.GetOutput().AnswerBuffer("{}", "application/json");
-    }
-    else if (context.GetScheduler().SubmitAndWait(job))
-    {
-      // Synchronous mode: We have submitted and waited for completion
-      call.GetOutput().AnswerBuffer("{}", "application/json");
-    }
-    else
-    {
-      call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
-    }
+    SubmitJob(call, request, instances, job.release());
   }
 
 
@@ -984,15 +1015,17 @@
     if (call.ParseJsonRequest(json))
     {
       const std::string& localAet = context.GetDefaultLocalApplicationEntityTitle();
-      RemoteModalityParameters remote = Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
+      RemoteModalityParameters remote =
+        Configuration::GetModalityUsingSymbolicName(call.GetUriComponent("id", ""));
 
       std::auto_ptr<ParsedDicomFile> query(ParsedDicomFile::CreateFromJson(json, static_cast<DicomFromJsonFlags>(0)));
 
       DicomFindAnswers answers(true);
 
       {
-        ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), localAet, remote);
-        locker.GetConnection().FindWorklist(answers, *query);
+        DicomUserConnection connection(localAet, remote);
+        connection.Open();
+        connection.FindWorklist(answers, *query);
       }
 
       Json::Value result;
--- a/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp	Wed May 23 10:08:04 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp	Wed May 23 14:28:25 2018 +0200
@@ -124,8 +124,8 @@
     call.BodyToString(command);
 
     {
-      LuaScripting::Locker locker(context.GetLua());
-      locker.GetLua().Execute(result, command);
+      LuaScripting::Lock lock(context.GetLuaScripting());
+      lock.GetLua().Execute(result, command);
     }
 
     call.GetOutput().AnswerBuffer(result, "text/plain");
@@ -146,6 +146,24 @@
   }
 
 
+  static void GetDefaultEncoding(RestApiGetCall& call)
+  {
+    Encoding encoding = GetDefaultDicomEncoding();
+    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain");
+  }
+
+
+  static void SetDefaultEncoding(RestApiPutCall& call)
+  {
+    Encoding encoding = StringToEncoding(call.GetBodyData());
+
+    Configuration::SetDefaultEncoding(encoding);
+
+    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain");
+  }
+
+
+  
   // Plugins information ------------------------------------------------------
 
   static void ListPlugins(RestApiGetCall& call)
@@ -251,23 +269,101 @@
   }
 
 
-  static void GetDefaultEncoding(RestApiGetCall& call)
+
+
+  // Jobs information ------------------------------------------------------
+
+  static void ListJobs(RestApiGetCall& call)
   {
-    Encoding encoding = GetDefaultDicomEncoding();
-    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain");
+    bool expand = call.HasArgument("expand");
+    bool internal = call.HasArgument("internal");
+
+    Json::Value v = Json::arrayValue;
+
+    std::set<std::string> jobs;
+    OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().ListJobs(jobs);
+
+    for (std::set<std::string>::const_iterator it = jobs.begin();
+         it != jobs.end(); ++it)
+    {
+      if (expand)
+      {
+        JobInfo info;
+        if (OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().GetJobInfo(info, *it))
+        {
+          Json::Value tmp;
+          info.Serialize(tmp, internal);
+          v.append(tmp);
+        }
+      }
+      else
+      {
+        v.append(*it);
+      }
+    }
+    
+    call.GetOutput().AnswerJson(v);
+  }
+
+  static void GetJobInfo(RestApiGetCall& call)
+  {
+    std::string id = call.GetUriComponent("id", "");
+    bool internal = call.HasArgument("internal");
+
+    JobInfo info;
+    if (OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().GetJobInfo(info, id))
+    {
+      Json::Value json;
+      info.Serialize(json, internal);
+      call.GetOutput().AnswerJson(json);
+    }
   }
 
 
-  static void SetDefaultEncoding(RestApiPutCall& call)
+  enum JobAction
   {
-    Encoding encoding = StringToEncoding(call.GetBodyData());
+    JobAction_Cancel,
+    JobAction_Pause,
+    JobAction_Resubmit,
+    JobAction_Resume
+  };
+
+  template <JobAction action>
+  static void ApplyJobAction(RestApiPostCall& call)
+  {
+    std::string id = call.GetUriComponent("id", "");
+
+    bool ok = false;
+
+    switch (action)
+    {
+      case JobAction_Cancel:
+        ok = OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().Cancel(id);
+        break;
 
-    Configuration::SetDefaultEncoding(encoding);
+      case JobAction_Pause:
+        ok = OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().Pause(id);
+        break;
+ 
+      case JobAction_Resubmit:
+        ok = OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().Resubmit(id);
+        break;
 
-    call.GetOutput().AnswerBuffer(EnumerationToString(encoding), "text/plain");
+      case JobAction_Resume:
+        ok = OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().Resume(id);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+    
+    if (ok)
+    {
+      call.GetOutput().AnswerBuffer("{}", "application/json");
+    }
   }
 
-
+  
   void OrthancRestApi::RegisterSystem()
   {
     Register("/", ServeRoot);
@@ -284,5 +380,12 @@
     Register("/plugins", ListPlugins);
     Register("/plugins/{id}", GetPlugin);
     Register("/plugins/explorer.js", GetOrthancExplorerPlugins);
+
+    Register("/jobs", ListJobs);
+    Register("/jobs/{id}", GetJobInfo);
+    Register("/jobs/{id}/cancel", ApplyJobAction<JobAction_Cancel>);
+    Register("/jobs/{id}/pause", ApplyJobAction<JobAction_Pause>);
+    Register("/jobs/{id}/resubmit", ApplyJobAction<JobAction_Resubmit>);
+    Register("/jobs/{id}/resume", ApplyJobAction<JobAction_Resume>);
   }
 }
--- a/OrthancServer/QueryRetrieveHandler.cpp	Wed May 23 10:08:04 2018 +0200
+++ b/OrthancServer/QueryRetrieveHandler.cpp	Wed May 23 14:28:25 2018 +0200
@@ -35,21 +35,24 @@
 #include "QueryRetrieveHandler.h"
 
 #include "OrthancInitialization.h"
+
 #include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/Logging.h"
 
 
 namespace Orthanc
 {
-  static void FixQuery(DicomMap& query,
-                       ServerContext& context,
-                       const std::string& modality)
+  static void FixQueryLua(DicomMap& query,
+                          ServerContext& context,
+                          const std::string& modality)
   {
     static const char* LUA_CALLBACK = "OutgoingFindRequestFilter";
 
-    LuaScripting::Locker locker(context.GetLua());
-    if (locker.GetLua().IsExistingFunction(LUA_CALLBACK))
+    LuaScripting::Lock lock(context.GetLuaScripting());
+
+    if (lock.GetLua().IsExistingFunction(LUA_CALLBACK))
     {
-      LuaFunctionCall call(locker.GetLua(), LUA_CALLBACK);
+      LuaFunctionCall call(lock.GetLua(), LUA_CALLBACK);
       call.PushDicom(query);
       call.PushJson(modality);
       FromDcmtkBridge::ExecuteToDicom(query, call);
@@ -57,8 +60,8 @@
   }
 
 
-  static void FixQuery(DicomMap& query,
-                       ModalityManufacturer manufacturer)
+  static void FixQueryBuiltin(DicomMap& query,
+                              ModalityManufacturer manufacturer)
   {
     /**
      * Introduce patches for specific manufacturers below.
@@ -76,6 +79,19 @@
   {
     done_ = false;
     answers_.Clear();
+    connection_.reset(NULL);
+  }
+
+
+  DicomUserConnection& QueryRetrieveHandler::GetConnection()
+  {
+    if (connection_.get() == NULL)
+    {
+      connection_.reset(new DicomUserConnection(localAet_, modality_));
+      connection_->Open();
+    }
+
+    return *connection_;
   }
 
 
@@ -86,16 +102,12 @@
       // Firstly, fix the content of the query for specific manufacturers
       DicomMap fixed;
       fixed.Assign(query_);
-      FixQuery(fixed, modality_.GetManufacturer());
+      FixQueryBuiltin(fixed, modality_.GetManufacturer());
 
       // Secondly, possibly fix the query with the user-provider Lua callback
-      FixQuery(fixed, context_, modality_.GetApplicationEntityTitle()); 
+      FixQueryLua(fixed, context_, modality_.GetApplicationEntityTitle()); 
 
-      {
-        // Finally, run the C-FIND SCU against the fixed query
-        ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_);
-        locker.GetConnection().Find(answers_, level_, fixed);
-      }
+      GetConnection().Find(answers_, level_, fixed);
 
       done_ = true;
     }
@@ -155,11 +167,7 @@
   {
     DicomMap map;
     GetAnswer(map, i);
-
-    {
-      ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_);
-      locker.GetConnection().Move(target, map);
-    }
+    GetConnection().Move(target, map);
   }
 
 
--- a/OrthancServer/QueryRetrieveHandler.h	Wed May 23 10:08:04 2018 +0200
+++ b/OrthancServer/QueryRetrieveHandler.h	Wed May 23 14:28:25 2018 +0200
@@ -49,8 +49,11 @@
     DicomFindAnswers           answers_;
     std::string                modalityName_;
 
+    std::auto_ptr<DicomUserConnection>  connection_;
+
     void Invalidate();
 
+    DicomUserConnection& GetConnection();
 
   public:
     QueryRetrieveHandler(ServerContext& context);
--- a/OrthancServer/Scheduler/CallSystemCommand.cpp	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,85 +0,0 @@
-/**
- * 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 "CallSystemCommand.h"
-
-#include "../../Core/Logging.h"
-#include "../../Core/Toolbox.h"
-#include "../../Core/TemporaryFile.h"
-
-namespace Orthanc
-{
-  CallSystemCommand::CallSystemCommand(ServerContext& context,
-                                       const std::string& command,
-                                       const std::vector<std::string>& arguments) : 
-    context_(context),
-    command_(command),
-    arguments_(arguments)
-  {
-  }
-
-  bool CallSystemCommand::Apply(ListOfStrings& outputs,
-                                const ListOfStrings& inputs)
-  {
-    for (ListOfStrings::const_iterator
-           it = inputs.begin(); it != inputs.end(); ++it)
-    {
-      LOG(INFO) << "Calling system command " << command_ << " on instance " << *it;
-
-      try
-      {
-        std::string dicom;
-        context_.ReadDicom(dicom, *it);
-
-        TemporaryFile tmp;
-        tmp.Write(dicom);
-
-        std::vector<std::string> args = arguments_;
-        args.push_back(tmp.GetPath());
-
-        SystemToolbox::ExecuteSystemCommand(command_, args);
-
-        // Only chain with other commands if this command succeeds
-        outputs.push_back(*it);
-      }
-      catch (OrthancException& e)
-      {
-        LOG(ERROR) << "Unable to call system command " << command_ 
-                   << " on instance " << *it << " in a Lua script: " << e.What();
-      }
-    }
-
-    return true;
-  }
-}
--- a/OrthancServer/Scheduler/CallSystemCommand.h	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,56 +0,0 @@
-/**
- * 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 "IServerCommand.h"
-#include "../ServerContext.h"
-
-namespace Orthanc
-{
-  class CallSystemCommand : public IServerCommand
-  {
-  private:
-    ServerContext& context_;
-    std::string command_;
-    std::vector<std::string> arguments_;
-
-  public:
-    CallSystemCommand(ServerContext& context,
-                      const std::string& command,
-                      const std::vector<std::string>& arguments);
-
-    virtual bool Apply(ListOfStrings& outputs,
-                       const ListOfStrings& inputs);
-  };
-}
--- a/OrthancServer/Scheduler/DeleteInstanceCommand.cpp	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-/**
- * 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 "DeleteInstanceCommand.h"
-
-#include "../../Core/Logging.h"
-
-namespace Orthanc
-{
-  bool DeleteInstanceCommand::Apply(ListOfStrings& outputs,
-                                    const ListOfStrings& inputs)
-  {
-    for (ListOfStrings::const_iterator
-           it = inputs.begin(); it != inputs.end(); ++it)
-    {
-      LOG(INFO) << "Deleting instance " << *it;
-
-      try
-      {
-        Json::Value tmp;
-        context_.DeleteResource(tmp, *it, ResourceType_Instance);
-      }
-      catch (OrthancException& e)
-      {
-        LOG(ERROR) << "Unable to delete instance " << *it << ": " << e.What();
-      }
-    }
-
-    return true;
-  }
-}
--- a/OrthancServer/Scheduler/DeleteInstanceCommand.h	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +0,0 @@
-/**
- * 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 "IServerCommand.h"
-#include "../ServerContext.h"
-
-namespace Orthanc
-{
-  class DeleteInstanceCommand : public IServerCommand
-  {
-  private:
-    ServerContext& context_;
-
-  public:
-    DeleteInstanceCommand(ServerContext& context) : context_(context)
-    {
-    }
-
-    virtual bool Apply(ListOfStrings& outputs,
-                       const ListOfStrings& inputs);
-  };
-}
--- a/OrthancServer/Scheduler/IServerCommand.h	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,54 +0,0 @@
-/**
- * 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 <list>
-#include <string>
-#include <boost/noncopyable.hpp>
-
-namespace Orthanc
-{
-  class IServerCommand : public boost::noncopyable
-  {
-  public:
-    typedef std::list<std::string>  ListOfStrings;
-
-    virtual ~IServerCommand()
-    {
-    }
-
-    virtual bool Apply(ListOfStrings& outputs,
-                       const ListOfStrings& inputs) = 0;
-  };
-}
--- a/OrthancServer/Scheduler/ModifyInstanceCommand.cpp	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,124 +0,0 @@
-/**
- * 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 "ModifyInstanceCommand.h"
-
-#include "../../Core/Logging.h"
-
-namespace Orthanc
-{
-  ModifyInstanceCommand::ModifyInstanceCommand(ServerContext& context,
-                                               RequestOrigin origin,
-                                               DicomModification* modification) :
-    context_(context),
-    origin_(origin),
-    modification_(modification)
-  {
-    modification_->SetAllowManualIdentifiers(true);
-
-    if (modification_->IsReplaced(DICOM_TAG_PATIENT_ID))
-    {
-      modification_->SetLevel(ResourceType_Patient);
-    }
-    else if (modification_->IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
-    {
-      modification_->SetLevel(ResourceType_Study);
-    }
-    else if (modification_->IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
-    {
-      modification_->SetLevel(ResourceType_Series);
-    }
-    else
-    {
-      modification_->SetLevel(ResourceType_Instance);
-    }
-
-    if (origin_ != RequestOrigin_Lua)
-    {
-      // TODO If issued from HTTP, "remoteIp" and "username" must be provided
-      throw OrthancException(ErrorCode_NotImplemented);
-    }
-  }
-
-
-  ModifyInstanceCommand::~ModifyInstanceCommand()
-  {
-    if (modification_)
-    {
-      delete modification_;
-    }
-  }
-
-
-  bool ModifyInstanceCommand::Apply(ListOfStrings& outputs,
-                                    const ListOfStrings& inputs)
-  {
-    for (ListOfStrings::const_iterator
-           it = inputs.begin(); it != inputs.end(); ++it)
-    {
-      LOG(INFO) << "Modifying resource " << *it;
-
-      try
-      {
-        std::auto_ptr<ParsedDicomFile> modified;
-
-        {
-          ServerContext::DicomCacheLocker lock(context_, *it);
-          modified.reset(lock.GetDicom().Clone(true));
-        }
-
-        modification_->Apply(*modified);
-
-        DicomInstanceToStore toStore;
-        assert(origin_ == RequestOrigin_Lua);
-        toStore.SetLuaOrigin();
-        toStore.SetParsedDicomFile(*modified);
-        // TODO other metadata
-        toStore.AddMetadata(ResourceType_Instance, MetadataType_ModifiedFrom, *it);
-
-        std::string modifiedId;
-        context_.Store(modifiedId, toStore);
-
-        // Only chain with other commands if this command succeeds
-        outputs.push_back(modifiedId);
-      }
-      catch (OrthancException& e)
-      {
-        LOG(ERROR) << "Unable to modify instance " << *it << ": " << e.What();
-      }
-    }
-
-    return true;
-  }
-}
--- a/OrthancServer/Scheduler/ModifyInstanceCommand.h	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,64 +0,0 @@
-/**
- * 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 "IServerCommand.h"
-#include "../ServerContext.h"
-#include "../../Core/DicomParsing/DicomModification.h"
-
-namespace Orthanc
-{
-  class ModifyInstanceCommand : public IServerCommand
-  {
-  private:
-    ServerContext& context_;
-    RequestOrigin origin_;
-    DicomModification* modification_;
-
-  public:
-    ModifyInstanceCommand(ServerContext& context,
-                          RequestOrigin origin,
-                          DicomModification* modification);  // takes the ownership
-
-    virtual ~ModifyInstanceCommand();
-
-    const DicomModification& GetModification() const
-    {
-      return *modification_;
-    }
-
-    virtual bool Apply(ListOfStrings& outputs,
-                       const ListOfStrings& inputs);
-  };
-}
--- a/OrthancServer/Scheduler/ServerCommandInstance.cpp	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,99 +0,0 @@
-/**
- * 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 "ServerCommandInstance.h"
-
-#include "../../Core/OrthancException.h"
-
-namespace Orthanc
-{
-  bool ServerCommandInstance::Execute(IListener& listener)
-  {
-    ListOfStrings outputs;
-
-    bool success = false;
-
-    try
-    {
-      if (command_->Apply(outputs, inputs_))
-      {
-        success = true;
-      }
-    }
-    catch (OrthancException&)
-    {
-    }
-
-    if (!success)
-    {
-      listener.SignalFailure(jobId_);
-      return true;
-    }
-
-    for (std::list<ServerCommandInstance*>::iterator
-           it = next_.begin(); it != next_.end(); ++it)
-    {
-      for (ListOfStrings::const_iterator
-             output = outputs.begin(); output != outputs.end(); ++output)
-      {
-        (*it)->AddInput(*output);
-      }
-    }
-
-    listener.SignalSuccess(jobId_);
-    return true;
-  }
-
-
-  ServerCommandInstance::ServerCommandInstance(IServerCommand *command,
-                                               const std::string& jobId) : 
-    command_(command), 
-    jobId_(jobId),
-    connectedToSink_(false)
-  {
-    if (command_ == NULL)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-  }
-
-
-  ServerCommandInstance::~ServerCommandInstance()
-  {
-    if (command_ != NULL)
-    {
-      delete command_;
-    }
-  }
-}
--- a/OrthancServer/Scheduler/ServerCommandInstance.h	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,105 +0,0 @@
-/**
- * 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/IDynamicObject.h"
-#include "IServerCommand.h"
-
-namespace Orthanc
-{
-  class ServerCommandInstance : public IDynamicObject
-  {
-    friend class ServerScheduler;
-
-  public:
-    class IListener
-    {
-    public:
-      virtual ~IListener()
-      {
-      }
-
-      virtual void SignalSuccess(const std::string& jobId) = 0;
-
-      virtual void SignalFailure(const std::string& jobId) = 0;
-    };
-
-  private:
-    typedef IServerCommand::ListOfStrings  ListOfStrings;
-
-    IServerCommand *command_;
-    std::string jobId_;
-    ListOfStrings inputs_;
-    std::list<ServerCommandInstance*> next_;
-    bool connectedToSink_;
-
-    bool Execute(IListener& listener);
-
-  public:
-    ServerCommandInstance(IServerCommand *command,
-                          const std::string& jobId);
-
-    virtual ~ServerCommandInstance();
-
-    const std::string& GetJobId() const
-    {
-      return jobId_;
-    }
-
-    void AddInput(const std::string& input)
-    {
-      inputs_.push_back(input);
-    }
-
-    void ConnectOutput(ServerCommandInstance& next)
-    {
-      next_.push_back(&next);
-    }
-
-    void SetConnectedToSink(bool connected = true)
-    {
-      connectedToSink_ = connected;
-    }
-
-    bool IsConnectedToSink() const
-    {
-      return connectedToSink_;
-    }
-
-    const std::list<ServerCommandInstance*>& GetNextCommands() const
-    {
-      return next_;
-    }
-  };
-}
--- a/OrthancServer/Scheduler/ServerJob.cpp	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,147 +0,0 @@
-/**
- * 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 "ServerJob.h"
-
-#include "../../Core/OrthancException.h"
-#include "../../Core/Toolbox.h"
-
-namespace Orthanc
-{
-  void ServerJob::CheckOrdering()
-  {
-    std::map<ServerCommandInstance*, unsigned int> index;
-
-    unsigned int count = 0;
-    for (std::list<ServerCommandInstance*>::const_iterator
-           it = filters_.begin(); it != filters_.end(); ++it)
-    {
-      index[*it] = count++;
-    }
-
-    for (std::list<ServerCommandInstance*>::const_iterator
-           it = filters_.begin(); it != filters_.end(); ++it)
-    {
-      const std::list<ServerCommandInstance*>& nextCommands = (*it)->GetNextCommands();
-
-      for (std::list<ServerCommandInstance*>::const_iterator
-             next = nextCommands.begin(); next != nextCommands.end(); ++next)
-      {
-        if (index.find(*next) == index.end() ||
-            index[*next] <= index[*it])
-        {
-          // You must reorder your calls to "ServerJob::AddCommand"
-          throw OrthancException(ErrorCode_BadJobOrdering);
-        }
-      }
-    }
-  }
-
-
-  size_t ServerJob::Submit(SharedMessageQueue& target,
-                           ServerCommandInstance::IListener& listener)
-  {
-    if (submitted_)
-    {
-      // This job has already been submitted
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    CheckOrdering();
-
-    size_t size = filters_.size();
-
-    for (std::list<ServerCommandInstance*>::iterator 
-           it = filters_.begin(); it != filters_.end(); ++it)
-    {
-      target.Enqueue(*it);
-    }
-
-    filters_.clear();
-    submitted_ = true;
-
-    return size;
-  }
-
-
-  ServerJob::ServerJob() :
-    jobId_(Toolbox::GenerateUuid()),
-    submitted_(false),
-    description_("no description")
-  {
-  }
-
-
-  ServerJob::~ServerJob()
-  {
-    for (std::list<ServerCommandInstance*>::iterator
-           it = filters_.begin(); it != filters_.end(); ++it)
-    {
-      delete *it;
-    }
-
-    for (std::list<IDynamicObject*>::iterator
-           it = payloads_.begin(); it != payloads_.end(); ++it)
-    {
-      delete *it;
-    }
-  }
-
-
-  ServerCommandInstance& ServerJob::AddCommand(IServerCommand* filter)
-  {
-    if (submitted_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    filters_.push_back(new ServerCommandInstance(filter, jobId_));
-      
-    return *filters_.back();
-  }
-
-
-  IDynamicObject& ServerJob::AddPayload(IDynamicObject* payload)
-  {
-    if (submitted_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    payloads_.push_back(payload);
-      
-    return *filters_.back();
-  }
-
-}
--- a/OrthancServer/Scheduler/ServerJob.h	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,83 +0,0 @@
-/**
- * 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 "ServerCommandInstance.h"
-#include "../../Core/MultiThreading/SharedMessageQueue.h"
-
-namespace Orthanc
-{
-  class ServerJob
-  {
-    friend class ServerScheduler;
-
-  private:
-    std::list<ServerCommandInstance*> filters_;
-    std::list<IDynamicObject*> payloads_;
-    std::string jobId_;
-    bool submitted_;
-    std::string description_;
-
-    void CheckOrdering();
-
-    size_t Submit(SharedMessageQueue& target,
-                  ServerCommandInstance::IListener& listener);
-
-  public:
-    ServerJob();
-
-    ~ServerJob();
-
-    const std::string& GetId() const
-    {
-      return jobId_;
-    }
-
-    void SetDescription(const std::string& description)
-    {
-      description_ = description;
-    }
-
-    const std::string& GetDescription() const
-    {
-      return description_;
-    }
-
-    ServerCommandInstance& AddCommand(IServerCommand* filter);
-
-    // Take the ownership of a payload to a job. This payload will be
-    // automatically freed when the job succeeds or fails.
-    IDynamicObject& AddPayload(IDynamicObject* payload);
-  };
-}
--- a/OrthancServer/Scheduler/ServerScheduler.cpp	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,359 +0,0 @@
-/**
- * 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 "ServerScheduler.h"
-
-#include "../../Core/OrthancException.h"
-#include "../../Core/Logging.h"
-
-namespace Orthanc
-{
-  namespace
-  {
-    // Anonymous namespace to avoid clashes between compilation modules
-    class Sink : public IServerCommand
-    {
-    private:
-      ListOfStrings& target_;
-
-    public:
-      explicit Sink(ListOfStrings& target) : target_(target)
-      {
-      }
-
-      virtual bool Apply(ListOfStrings& outputs,
-                         const ListOfStrings& inputs)
-      {
-        for (ListOfStrings::const_iterator 
-               it = inputs.begin(); it != inputs.end(); ++it)
-        {
-          target_.push_back(*it);
-        }
-
-        return true;
-      }    
-    };
-  }
-
-
-  ServerScheduler::JobInfo& ServerScheduler::GetJobInfo(const std::string& jobId)
-  {
-    Jobs::iterator info = jobs_.find(jobId);
-
-    if (info == jobs_.end())
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    return info->second;
-  }
-
-
-  void ServerScheduler::SignalSuccess(const std::string& jobId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    JobInfo& info = GetJobInfo(jobId);
-    info.success_++;
-
-    assert(info.failures_ == 0);
-
-    if (info.success_ >= info.size_)
-    {
-      if (info.watched_)
-      {
-        watchedJobStatus_[jobId] = JobStatus_Success;
-        watchedJobFinished_.notify_all();
-      }
-
-      LOG(INFO) << "Job successfully finished (" << info.description_ << ")";
-      jobs_.erase(jobId);
-
-      availableJob_.Release();
-    }
-  }
-
-
-  void ServerScheduler::SignalFailure(const std::string& jobId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    JobInfo& info = GetJobInfo(jobId);
-    info.failures_++;
-
-    if (info.success_ + info.failures_ >= info.size_)
-    {
-      if (info.watched_)
-      {
-        watchedJobStatus_[jobId] = JobStatus_Failure;
-        watchedJobFinished_.notify_all();
-      }
-
-      LOG(ERROR) << "Job has failed (" << info.description_ << ")";
-      jobs_.erase(jobId);
-
-      availableJob_.Release();
-    }
-  }
-
-
-  void ServerScheduler::Worker(ServerScheduler* that)
-  {
-    static const int32_t TIMEOUT = 100;
-
-    LOG(WARNING) << "The server scheduler has started";
-
-    while (!that->finish_)
-    {
-      std::auto_ptr<IDynamicObject> object(that->queue_.Dequeue(TIMEOUT));
-      if (object.get() != NULL)
-      {
-        ServerCommandInstance& filter = dynamic_cast<ServerCommandInstance&>(*object);
-
-        // Skip the execution of this filter if its parent job has
-        // previously failed.
-        bool jobHasFailed;
-        {
-          boost::mutex::scoped_lock lock(that->mutex_);
-          JobInfo& info = that->GetJobInfo(filter.GetJobId());
-          jobHasFailed = (info.failures_ > 0 || info.cancel_); 
-        }
-
-        if (jobHasFailed)
-        {
-          that->SignalFailure(filter.GetJobId());
-        }
-        else
-        {
-          filter.Execute(*that);
-        }
-      }
-    }
-  }
-
-
-  void ServerScheduler::SubmitInternal(ServerJob& job,
-                                       bool watched)
-  {
-    availableJob_.Acquire();
-
-    boost::mutex::scoped_lock lock(mutex_);
-
-    JobInfo info;
-    info.size_ = job.Submit(queue_, *this);
-    info.cancel_ = false;
-    info.success_ = 0;
-    info.failures_ = 0;
-    info.description_ = job.GetDescription();
-    info.watched_ = watched;
-
-    assert(info.size_ > 0);
-
-    if (watched)
-    {
-      watchedJobStatus_[job.GetId()] = JobStatus_Running;
-    }
-
-    jobs_[job.GetId()] = info;
-
-    LOG(INFO) << "New job submitted (" << job.description_ << ")";
-  }
-
-
-  ServerScheduler::ServerScheduler(unsigned int maxJobs) : availableJob_(maxJobs)
-  {
-    if (maxJobs == 0)
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-    finish_ = false;
-    worker_ = boost::thread(Worker, this);
-  }
-
-
-  ServerScheduler::~ServerScheduler()
-  {
-    if (!finish_)
-    {
-      LOG(ERROR) << "INTERNAL ERROR: ServerScheduler::Finalize() should be invoked manually to avoid mess in the destruction order!";
-      Stop();
-    }
-  }
-
-
-  void ServerScheduler::Stop()
-  {
-    if (!finish_)
-    {
-      finish_ = true;
-
-      if (worker_.joinable())
-      {
-        worker_.join();
-      }
-    }
-  }
-
-
-  void ServerScheduler::Submit(ServerJob& job)
-  {
-    if (job.filters_.empty())
-    {
-      return;
-    }
-
-    SubmitInternal(job, false);
-  }
-
-
-  bool ServerScheduler::SubmitAndWait(ListOfStrings& outputs,
-                                      ServerJob& job)
-  {
-    std::string jobId = job.GetId();
-
-    outputs.clear();
-
-    if (job.filters_.empty())
-    {
-      return true;
-    }
-
-    // Add a sink filter to collect all the results of the filters
-    // that have no next filter.
-    ServerCommandInstance& sink = job.AddCommand(new Sink(outputs));
-
-    for (std::list<ServerCommandInstance*>::iterator
-           it = job.filters_.begin(); it != job.filters_.end(); ++it)
-    {
-      if ((*it) != &sink &&
-          (*it)->IsConnectedToSink())
-      {
-        (*it)->ConnectOutput(sink);
-      }
-    }
-
-    // Submit the job
-    SubmitInternal(job, true);
-
-    // Wait for the job to complete (either success or failure)
-    JobStatus status;
-
-    {
-      boost::mutex::scoped_lock lock(mutex_);
-
-      assert(watchedJobStatus_.find(jobId) != watchedJobStatus_.end());
-        
-      while (watchedJobStatus_[jobId] == JobStatus_Running)
-      {
-        watchedJobFinished_.wait(lock);
-      }
-
-      status = watchedJobStatus_[jobId];
-      watchedJobStatus_.erase(jobId);
-    }
-
-    return (status == JobStatus_Success);
-  }
-
-
-  bool ServerScheduler::SubmitAndWait(ServerJob& job)
-  {
-    ListOfStrings ignoredSink;
-    return SubmitAndWait(ignoredSink, job);
-  }
-
-
-  bool ServerScheduler::IsRunning(const std::string& jobId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-    return jobs_.find(jobId) != jobs_.end();
-  }
-
-
-  void ServerScheduler::Cancel(const std::string& jobId)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    Jobs::iterator job = jobs_.find(jobId);
-
-    if (job != jobs_.end())
-    {
-      job->second.cancel_ = true;
-      LOG(WARNING) << "Canceling a job (" << job->second.description_ << ")";
-    }
-  }
-
-
-  float ServerScheduler::GetProgress(const std::string& jobId) 
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    Jobs::iterator job = jobs_.find(jobId);
-
-    if (job == jobs_.end() || 
-        job->second.size_ == 0  /* should never happen */)
-    {
-      // This job is not running
-      return 1;
-    }
-
-    if (job->second.failures_ != 0)
-    {
-      return 1;
-    }
-
-    if (job->second.size_ == 1)
-    {
-      return static_cast<float>(job->second.success_);
-    }
-
-    return (static_cast<float>(job->second.success_) / 
-            static_cast<float>(job->second.size_ - 1));
-  }
-
-
-  void ServerScheduler::GetListOfJobs(ListOfStrings& jobs)
-  {
-    boost::mutex::scoped_lock lock(mutex_);
-
-    jobs.clear();
-
-    for (Jobs::const_iterator 
-           it = jobs_.begin(); it != jobs_.end(); ++it)
-    {
-      jobs.push_back(it->first);
-    }
-  }
-}
--- a/OrthancServer/Scheduler/ServerScheduler.h	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,123 +0,0 @@
-/**
- * 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 "ServerJob.h"
-
-#include "../../Core/MultiThreading/Semaphore.h"
-
-namespace Orthanc
-{
-  class ServerScheduler : public ServerCommandInstance::IListener
-  {
-  private:
-    struct JobInfo
-    {
-      bool watched_;
-      bool cancel_;
-      size_t size_;
-      size_t success_;
-      size_t failures_;
-      std::string description_;
-    };
-
-    enum JobStatus
-    {
-      JobStatus_Running = 1,
-      JobStatus_Success = 2,
-      JobStatus_Failure = 3
-    };
-
-    typedef IServerCommand::ListOfStrings  ListOfStrings;
-    typedef std::map<std::string, JobInfo> Jobs;
-
-    boost::mutex mutex_;
-    boost::condition_variable watchedJobFinished_;
-    Jobs jobs_;
-    SharedMessageQueue queue_;
-    bool finish_;
-    boost::thread worker_;
-    std::map<std::string, JobStatus> watchedJobStatus_;
-    Semaphore availableJob_;
-
-    JobInfo& GetJobInfo(const std::string& jobId);
-
-    virtual void SignalSuccess(const std::string& jobId);
-
-    virtual void SignalFailure(const std::string& jobId);
-
-    static void Worker(ServerScheduler* that);
-
-    void SubmitInternal(ServerJob& job,
-                        bool watched);
-
-  public:
-    explicit ServerScheduler(unsigned int maxjobs);
-
-    ~ServerScheduler();
-
-    void Stop();
-
-    void Submit(ServerJob& job);
-
-    bool SubmitAndWait(ListOfStrings& outputs,
-                       ServerJob& job);
-
-    bool SubmitAndWait(ServerJob& job);
-
-    bool IsRunning(const std::string& jobId);
-
-    void Cancel(const std::string& jobId);
-
-    // Returns a number between 0 and 1
-    float GetProgress(const std::string& jobId);
-
-    bool IsRunning(const ServerJob& job)
-    {
-      return IsRunning(job.GetId());
-    }
-
-    void Cancel(const ServerJob& job) 
-    {
-      Cancel(job.GetId());
-    }
-
-    float GetProgress(const ServerJob& job) 
-    {
-      return GetProgress(job.GetId());
-    }
-
-    void GetListOfJobs(ListOfStrings& jobs);
-  };
-}
--- a/OrthancServer/Scheduler/StorePeerCommand.cpp	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,92 +0,0 @@
-/**
- * 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 "StorePeerCommand.h"
-
-#include "../../Core/Logging.h"
-#include "../../Core/HttpClient.h"
-
-namespace Orthanc
-{
-  StorePeerCommand::StorePeerCommand(ServerContext& context,
-                                     const WebServiceParameters& peer,
-                                     bool ignoreExceptions) : 
-    context_(context),
-    peer_(peer),
-    ignoreExceptions_(ignoreExceptions)
-  {
-  }
-
-  bool StorePeerCommand::Apply(ListOfStrings& outputs,
-                               const ListOfStrings& inputs)
-  {
-    // Configure the HTTP client
-    HttpClient client(peer_, "instances");
-    client.SetMethod(HttpMethod_Post);
-
-    for (ListOfStrings::const_iterator
-           it = inputs.begin(); it != inputs.end(); ++it)
-    {
-      LOG(INFO) << "Sending resource " << *it << " to peer \"" 
-                << peer_.GetUrl() << "\"";
-
-      try
-      {
-        context_.ReadDicom(client.GetBody(), *it);
-
-        std::string answer;
-        if (!client.Apply(answer))
-        {
-          LOG(ERROR) << "Unable to send resource " << *it << " to peer \"" << peer_.GetUrl() << "\"";
-          throw OrthancException(ErrorCode_NetworkProtocol);
-        }
-
-        // Only chain with other commands if this command succeeds
-        outputs.push_back(*it);
-      }
-      catch (OrthancException& e)
-      {
-        LOG(ERROR) << "Unable to forward to an Orthanc peer in (instance "
-                   << *it << ", peer " << peer_.GetUrl() << "): " << e.What();
-
-        if (!ignoreExceptions_)
-        {
-          throw;
-        }
-      }
-    }
-
-    return true;
-  }
-}
--- a/OrthancServer/Scheduler/StorePeerCommand.h	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-/**
- * 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 "IServerCommand.h"
-#include "../ServerContext.h"
-#include "../OrthancInitialization.h"
-
-namespace Orthanc
-{
-  class StorePeerCommand : public IServerCommand
-  {
-  private:
-    ServerContext& context_;
-    WebServiceParameters peer_;
-    bool ignoreExceptions_;
-
-  public:
-    StorePeerCommand(ServerContext& context,
-                     const WebServiceParameters& peer,
-                     bool ignoreExceptions);
-    
-    virtual bool Apply(ListOfStrings& outputs,
-                       const ListOfStrings& inputs);
-  };
-}
--- a/OrthancServer/Scheduler/StoreScuCommand.cpp	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,99 +0,0 @@
-/**
- * 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 "StoreScuCommand.h"
-
-#include "../../Core/Logging.h"
-
-namespace Orthanc
-{
-  StoreScuCommand::StoreScuCommand(ServerContext& context,
-                                   const std::string& localAet,
-                                   const RemoteModalityParameters& modality,
-                                   bool ignoreExceptions) : 
-    context_(context),
-    modality_(modality),
-    ignoreExceptions_(ignoreExceptions),
-    localAet_(localAet),
-    moveOriginatorID_(0)
-  {
-  }
-
-
-  void StoreScuCommand::SetMoveOriginator(const std::string& aet,
-                                          uint16_t id)
-  {
-    moveOriginatorAET_ = aet;
-    moveOriginatorID_ = id;
-  }
-
-
-  bool StoreScuCommand::Apply(ListOfStrings& outputs,
-                             const ListOfStrings& inputs)
-  {
-    ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_);
-
-    for (ListOfStrings::const_iterator
-           it = inputs.begin(); it != inputs.end(); ++it)
-    {
-      LOG(INFO) << "Sending resource " << *it << " to modality \"" 
-                << modality_.GetApplicationEntityTitle() << "\"";
-
-      try
-      {
-        std::string dicom;
-        context_.ReadDicom(dicom, *it);
-
-        locker.GetConnection().Store(dicom, moveOriginatorAET_, moveOriginatorID_);
-
-        // Only chain with other commands if this command succeeds
-        outputs.push_back(*it);
-      }
-      catch (OrthancException& e)
-      {
-        // Ignore transmission errors (e.g. if the remote modality is
-        // powered off)
-        LOG(ERROR) << "Unable to forward to a modality in (instance "
-                   << *it << "): " << e.What();
-
-        if (!ignoreExceptions_)
-        {
-          throw;
-        }
-      }
-    }
-
-    return true;
-  }
-}
--- a/OrthancServer/Scheduler/StoreScuCommand.h	Wed May 23 10:08:04 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,63 +0,0 @@
-/**
- * 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 "IServerCommand.h"
-#include "../ServerContext.h"
-
-namespace Orthanc
-{
-  class StoreScuCommand : public IServerCommand
-  {
-  private:
-    ServerContext& context_;
-    RemoteModalityParameters modality_;
-    bool ignoreExceptions_;
-    std::string localAet_;
-    std::string moveOriginatorAET_;
-    uint16_t moveOriginatorID_;
-
-  public:
-    StoreScuCommand(ServerContext& context,
-                    const std::string& localAet,
-                    const RemoteModalityParameters& modality,
-                    bool ignoreExceptions);
-
-    void SetMoveOriginator(const std::string& aet,
-                           uint16_t id);
-
-    virtual bool Apply(ListOfStrings& outputs,
-                       const ListOfStrings& inputs);
-  };
-}
--- a/OrthancServer/ServerContext.cpp	Wed May 23 10:08:04 2018 +0200
+++ b/OrthancServer/ServerContext.cpp	Wed May 23 14:28:25 2018 +0200
@@ -45,12 +45,6 @@
 #include <EmbeddedResources.h>
 #include <dcmtk/dcmdata/dcfilefo.h>
 
-
-#include "Scheduler/CallSystemCommand.h"
-#include "Scheduler/DeleteInstanceCommand.h"
-#include "Scheduler/ModifyInstanceCommand.h"
-#include "Scheduler/StoreScuCommand.h"
-#include "Scheduler/StorePeerCommand.h"
 #include "OrthancRestApi/OrthancRestApi.h"
 #include "../Plugins/Engine/OrthancPlugins.h"
 #include "Search/LookupResource.h"
@@ -120,7 +114,6 @@
     storeMD5_(true),
     provider_(*this),
     dicomCache_(provider_, DICOM_CACHE_SIZE),
-    scheduler_(Configuration::GetGlobalUnsignedIntegerParameter("LimitJobs", 10)),
     lua_(*this),
 #if ORTHANC_ENABLE_PLUGINS == 1
     plugins_(NULL),
@@ -129,10 +122,11 @@
     queryRetrieveArchive_(Configuration::GetGlobalUnsignedIntegerParameter("QueryRetrieveSize", 10)),
     defaultLocalAet_(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC"))
   {
-    uint64_t s = Configuration::GetGlobalUnsignedIntegerParameter("DicomAssociationCloseDelay", 5);  // In seconds
-    scu_.SetMillisecondsBeforeClose(s * 1000);  // Milliseconds are expected here
+    listeners_.push_back(ServerListener(lua_, "Lua"));
 
-    listeners_.push_back(ServerListener(lua_, "Lua"));
+    jobsEngine_.SetWorkersCount(Configuration::GetGlobalUnsignedIntegerParameter("ConcurrentJobs", 2));
+    //jobsEngine_.SetMaxCompleted   // TODO
+    jobsEngine_.Start();
 
     changeThread_ = boost::thread(ChangeThread, this);
   }
@@ -165,10 +159,8 @@
         changeThread_.join();
       }
 
-      scu_.Finalize();
-
       // Do not change the order below!
-      scheduler_.Stop();
+      jobsEngine_.Stop();
       index_.Stop();
     }
   }
--- a/OrthancServer/ServerContext.h	Wed May 23 10:08:04 2018 +0200
+++ b/OrthancServer/ServerContext.h	Wed May 23 14:28:25 2018 +0200
@@ -33,21 +33,20 @@
 
 #pragma once
 
-#include "../Core/MultiThreading/SharedMessageQueue.h"
+#include "DicomInstanceToStore.h"
+#include "IServerListener.h"
+#include "LuaScripting.h"
+#include "OrthancHttpHandler.h"
+#include "ServerIndex.h"
+
 #include "../Core/Cache/MemoryCache.h"
 #include "../Core/Cache/SharedArchive.h"
+#include "../Core/DicomParsing/ParsedDicomFile.h"
 #include "../Core/FileStorage/IStorageArea.h"
-#include "../Core/Lua/LuaContext.h"
+#include "../Core/JobsEngine/JobsEngine.h"
+#include "../Core/MultiThreading/SharedMessageQueue.h"
 #include "../Core/RestApi/RestApiOutput.h"
 #include "../Plugins/Engine/OrthancPlugins.h"
-#include "DicomInstanceToStore.h"
-#include "../Core/DicomNetworking/ReusableDicomUserConnection.h"
-#include "IServerListener.h"
-#include "LuaScripting.h"
-#include "../Core/DicomParsing/ParsedDicomFile.h"
-#include "Scheduler/ServerScheduler.h"
-#include "ServerIndex.h"
-#include "OrthancHttpHandler.h"
 
 #include <boost/filesystem.hpp>
 #include <boost/thread.hpp>
@@ -118,8 +117,7 @@
     DicomCacheProvider provider_;
     boost::mutex dicomCacheMutex_;
     MemoryCache dicomCache_;
-    ReusableDicomUserConnection scu_;
-    ServerScheduler scheduler_;
+    JobsEngine jobsEngine_;
 
     LuaScripting lua_;
 
@@ -238,14 +236,9 @@
       return storeMD5_;
     }
 
-    ReusableDicomUserConnection& GetReusableDicomUserConnection()
+    JobsEngine& GetJobsEngine()
     {
-      return scu_;
-    }
-
-    ServerScheduler& GetScheduler()
-    {
-      return scheduler_;
+      return jobsEngine_;
     }
 
     bool DeleteResource(Json::Value& target,
@@ -264,7 +257,7 @@
       return defaultLocalAet_;
     }
 
-    LuaScripting& GetLua()
+    LuaScripting& GetLuaScripting()
     {
       return lua_;
     }
--- a/OrthancServer/ServerIndex.cpp	Wed May 23 10:08:04 2018 +0200
+++ b/OrthancServer/ServerIndex.cpp	Wed May 23 14:28:25 2018 +0200
@@ -1212,7 +1212,7 @@
     ResourceType type;
     if (!db_.LookupResource(id, type, publicId))
     {
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InexistentItem);
     }
 
     std::string patientId;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/DeleteResourceOperation.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,72 @@
+/**
+ * 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 "DeleteResourceOperation.h"
+
+#include "DicomInstanceOperationValue.h"
+
+#include "../../Core/Logging.h"
+#include "../../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  void DeleteResourceOperation::Apply(JobOperationValues& outputs,
+                                      const JobOperationValue& input,
+                                      IDicomConnectionManager& connectionManager)
+  {
+    switch (input.GetType())
+    {
+      case JobOperationValue::Type_DicomInstance:
+      {
+        const DicomInstanceOperationValue& instance = dynamic_cast<const DicomInstanceOperationValue&>(input);
+        LOG(INFO) << "Lua: Deleting instance: " << instance.GetId();
+
+        try
+        {
+          Json::Value tmp;
+          context_.DeleteResource(tmp, instance.GetId(), ResourceType_Instance);
+        }
+        catch (OrthancException& e)
+        {
+          LOG(ERROR) << "Lua: Unable to delete instance " << instance.GetId() << ": " << e.What();
+        }
+
+        break;
+      }
+
+      default:
+        break;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/DeleteResourceOperation.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,63 @@
+/**
+ * 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/JobsEngine/Operations/IJobOperation.h"
+
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class DeleteResourceOperation : public IJobOperation
+  {
+  private:
+    ServerContext&  context_;
+
+  public:
+    DeleteResourceOperation(ServerContext& context) :
+    context_(context)
+    {
+    }
+
+    virtual void Apply(JobOperationValues& outputs,
+                       const JobOperationValue& input,
+                       IDicomConnectionManager& connectionManager);
+
+    virtual void Serialize(Json::Value& result) const
+    {
+      result["Type"] = "DeleteResource";
+    }
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/DicomInstanceOperationValue.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,83 @@
+/**
+ * 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/JobsEngine/Operations/JobOperationValue.h"
+
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class DicomInstanceOperationValue : public JobOperationValue
+  {
+  private:
+    ServerContext&   context_;
+    std::string      id_;
+
+  public:
+    DicomInstanceOperationValue(ServerContext& context,
+                                const std::string& id) :
+      JobOperationValue(Type_DicomInstance),
+      context_(context),
+      id_(id)
+    {
+    }
+
+    ServerContext& GetServerContext() const
+    {
+      return context_;
+    }
+
+    const std::string& GetId() const
+    {
+      return id_;
+    }
+
+    void ReadContent(std::string& dicom) const
+    {
+      context_.ReadDicom(dicom, id_);
+    }
+
+    virtual JobOperationValue* Clone() const
+    {
+      return new DicomInstanceOperationValue(context_, id_);
+    }
+
+    virtual void Serialize(Json::Value& target) const
+    {
+      target["Type"] = "DicomInstance";
+      target["ID"] = id_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,176 @@
+/**
+ * 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 "DicomModalityStoreJob.h"
+
+#include "../../Core/Logging.h"
+
+namespace Orthanc
+{
+  void DicomModalityStoreJob::OpenConnection()
+  {
+    if (connection_.get() == NULL)
+    {
+      connection_.reset(new DicomUserConnection);
+      connection_->SetLocalApplicationEntityTitle(localAet_);
+      connection_->SetRemoteModality(remote_);
+    }
+  }
+
+
+  bool DicomModalityStoreJob::HandleInstance(const std::string& instance)
+  {
+    OpenConnection();
+
+    LOG(INFO) << "Sending instance " << instance << " to modality \"" 
+              << remote_.GetApplicationEntityTitle() << "\"";
+
+    std::string dicom;
+    context_.ReadDicom(dicom, instance);
+
+    if (HasMoveOriginator())
+    {
+      connection_->Store(dicom, moveOriginatorAet_, moveOriginatorId_);
+    }
+    else
+    {
+      connection_->Store(dicom);
+    }
+
+    //boost::this_thread::sleep(boost::posix_time::milliseconds(500));
+
+    return true;
+  }
+    
+
+  DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context) :
+    context_(context),
+    localAet_("ORTHANC"),
+    moveOriginatorId_(0)  // By default, not a C-MOVE
+  {
+  }
+
+
+  void DicomModalityStoreJob::SetLocalAet(const std::string& aet)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      localAet_ = aet;
+    }
+  }
+
+
+  void DicomModalityStoreJob::SetRemoteModality(const RemoteModalityParameters& remote)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      remote_ = remote;
+    }
+  }
+
+    
+  const std::string& DicomModalityStoreJob::GetMoveOriginatorAet() const
+  {
+    if (HasMoveOriginator())
+    {
+      return moveOriginatorAet_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+    
+  uint16_t DicomModalityStoreJob::GetMoveOriginatorId() const
+  {
+    if (HasMoveOriginator())
+    {
+      return moveOriginatorId_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+
+  void DicomModalityStoreJob::SetMoveOriginator(const std::string& aet,
+                                                int id)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else if (id < 0 || 
+             id >= 65536)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      moveOriginatorId_ = static_cast<uint16_t>(id);
+      moveOriginatorAet_ = aet;
+    }
+  }
+
+  void DicomModalityStoreJob::ReleaseResources()   // For pausing jobs
+  {
+    connection_.reset(NULL);
+  }
+
+
+  void DicomModalityStoreJob::GetPublicContent(Json::Value& value)
+  {
+    value["LocalAet"] = localAet_;
+    value["RemoteAet"] = remote_.GetApplicationEntityTitle();
+
+    if (HasMoveOriginator())
+    {
+      value["MoveOriginatorAET"] = GetMoveOriginatorAet();
+      value["MoveOriginatorID"] = GetMoveOriginatorId();
+    }
+
+    value["InstancesCount"] = static_cast<uint32_t>(GetInstances().size());
+    value["FailedInstancesCount"] = static_cast<uint32_t>(GetFailedInstances().size());
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,96 @@
+/**
+ * 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/JobsEngine/SetOfInstancesJob.h"
+#include "../../Core/DicomNetworking/DicomUserConnection.h"
+
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class DicomModalityStoreJob : public SetOfInstancesJob
+  {
+  private:
+    ServerContext&                      context_;
+    std::string                         localAet_;
+    RemoteModalityParameters            remote_;
+    std::string                         moveOriginatorAet_;
+    uint16_t                            moveOriginatorId_;
+    std::auto_ptr<DicomUserConnection>  connection_;
+
+    void OpenConnection();
+
+  protected:
+    virtual bool HandleInstance(const std::string& instance);
+    
+  public:
+    DicomModalityStoreJob(ServerContext& context);
+
+    const std::string& GetLocalAet() const
+    {
+      return localAet_;
+    }
+
+    void SetLocalAet(const std::string& aet);
+
+    const RemoteModalityParameters& GetRemoteModality() const
+    {
+      return remote_;
+    }
+
+    void SetRemoteModality(const RemoteModalityParameters& remote);
+
+    bool HasMoveOriginator() const
+    {
+      return moveOriginatorId_ != 0;
+    }
+    
+    const std::string& GetMoveOriginatorAet() const;
+    
+    uint16_t GetMoveOriginatorId() const;
+
+    void SetMoveOriginator(const std::string& aet,
+                           int id);
+
+    virtual void ReleaseResources();
+
+    virtual void GetJobType(std::string& target)
+    {
+      target = "DicomModalityStore";
+    }
+
+    virtual void GetPublicContent(Json::Value& value);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/LuaJobManager.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,253 @@
+/**
+ * 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 "LuaJobManager.h"
+
+#include "../OrthancInitialization.h"
+#include "../../Core/Logging.h"
+
+#include "../../Core/JobsEngine/Operations/LogJobOperation.h"
+#include "DeleteResourceOperation.h"
+#include "ModifyInstanceOperation.h"
+#include "StorePeerOperation.h"
+#include "StoreScuOperation.h"
+#include "SystemCallOperation.h"
+
+#include "../../Core/JobsEngine/Operations/NullOperationValue.h"
+#include "../../Core/JobsEngine/Operations/StringOperationValue.h"
+#include "DicomInstanceOperationValue.h"
+
+namespace Orthanc
+{
+  void LuaJobManager::SignalDone(const SequenceOfOperationsJob& job)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    if (&job == currentJob_)
+    {
+      currentId_.clear();
+      currentJob_ = NULL;
+    }
+  }
+
+
+  LuaJobManager::LuaJobManager() :
+    currentJob_(NULL),
+    maxOperations_(1000),
+    priority_(0),
+    trailingTimeout_(5000)
+  {
+    dicomTimeout_ = Configuration::GetGlobalUnsignedIntegerParameter("DicomAssociationCloseDelay", 5);
+    LOG(INFO) << "Lua: DICOM associations will be closed after "
+              << dicomTimeout_ << " seconds of inactivity";
+  }
+
+
+  void LuaJobManager::SetMaxOperationsPerJob(size_t count)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    maxOperations_ = count;
+  }
+
+
+  void LuaJobManager::SetPriority(int priority)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    priority_ = priority;
+  }
+
+
+  void LuaJobManager::SetTrailingOperationTimeout(unsigned int timeout)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    trailingTimeout_ = timeout;
+  }
+
+
+  void LuaJobManager::AwakeTrailingSleep()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    LOG(INFO) << "Awaking trailing sleep";
+
+    if (currentJob_ != NULL)
+    {
+      currentJob_->AwakeTrailingSleep();
+    }
+  }
+
+
+  LuaJobManager::Lock::Lock(LuaJobManager& that,
+                            JobsEngine& engine) :
+    that_(that),
+    lock_(that.mutex_),
+    engine_(engine)
+  {
+    if (that_.currentJob_ == NULL)
+    {
+      isNewJob_ = true;
+    }
+    else
+    {
+      jobLock_.reset(new SequenceOfOperationsJob::Lock(*that_.currentJob_));
+
+      if (jobLock_->IsDone() ||
+          jobLock_->GetOperationsCount() >= that_.maxOperations_)
+      {
+        jobLock_.reset(NULL);
+        isNewJob_ = true;
+      }
+      else
+      {
+        isNewJob_ = false;
+      }
+    }
+
+    if (isNewJob_)
+    {
+      // Need to create a new job, as the previous one is either
+      // finished, or is getting too long
+      that_.currentJob_ = new SequenceOfOperationsJob;
+      that_.currentJob_->SetDescription("Lua");
+
+      {
+        jobLock_.reset(new SequenceOfOperationsJob::Lock(*that_.currentJob_));
+        jobLock_->SetTrailingOperationTimeout(that_.trailingTimeout_);
+        jobLock_->SetDicomAssociationTimeout(that_.dicomTimeout_ * 1000);  // Milliseconds expected
+      }
+    }
+
+    assert(jobLock_.get() != NULL);
+  }
+
+
+  LuaJobManager::Lock::~Lock()
+  {
+    assert(jobLock_.get() != NULL);
+    jobLock_.reset(NULL);
+
+    if (isNewJob_)
+    {
+      engine_.GetRegistry().Submit(that_.currentId_, that_.currentJob_, that_.priority_);
+    }
+  }
+
+
+  size_t LuaJobManager::Lock::AddDeleteResourceOperation(ServerContext& context)
+  {
+    assert(jobLock_.get() != NULL);
+    return jobLock_->AddOperation(new DeleteResourceOperation(context));
+  }
+
+
+  size_t LuaJobManager::Lock::AddLogOperation()
+  {
+    assert(jobLock_.get() != NULL);
+    return jobLock_->AddOperation(new LogJobOperation);
+  }
+
+
+  size_t LuaJobManager::Lock::AddStoreScuOperation(const std::string& localAet,
+                                                   const RemoteModalityParameters& modality)
+  {
+    assert(jobLock_.get() != NULL);
+    return jobLock_->AddOperation(new StoreScuOperation(localAet, modality));    
+  }
+
+
+  size_t LuaJobManager::Lock::AddStorePeerOperation(const WebServiceParameters& peer)
+  {
+    assert(jobLock_.get() != NULL);
+    return jobLock_->AddOperation(new StorePeerOperation(peer));    
+  }
+
+
+  size_t LuaJobManager::Lock::AddSystemCallOperation(const std::string& command)
+  {
+    assert(jobLock_.get() != NULL);
+    return jobLock_->AddOperation(new SystemCallOperation(command));    
+  }
+ 
+
+  size_t LuaJobManager::Lock::AddSystemCallOperation
+  (const std::string& command,
+   const std::vector<std::string>& preArguments,
+   const std::vector<std::string>& postArguments)
+  {
+    assert(jobLock_.get() != NULL);
+    return jobLock_->AddOperation
+      (new SystemCallOperation(command, preArguments, postArguments));
+  }
+
+
+  size_t LuaJobManager::Lock::AddModifyInstanceOperation(ServerContext& context,
+                                                         DicomModification* modification)
+  {
+    assert(jobLock_.get() != NULL);
+    return jobLock_->AddOperation
+      (new ModifyInstanceOperation(context, RequestOrigin_Lua, modification));
+  }
+
+
+  void LuaJobManager::Lock::AddNullInput(size_t operation)
+  {
+    assert(jobLock_.get() != NULL);
+    jobLock_->AddInput(operation, NullOperationValue());
+  }
+
+
+  void LuaJobManager::Lock::AddStringInput(size_t operation,
+                                           const std::string& content)
+  {
+    assert(jobLock_.get() != NULL);
+    jobLock_->AddInput(operation, StringOperationValue(content));
+  }
+
+
+  void LuaJobManager::Lock::AddDicomInstanceInput(size_t operation,
+                                                  ServerContext& context,
+                                                  const std::string& instanceId)
+  {
+    assert(jobLock_.get() != NULL);
+    jobLock_->AddInput(operation, DicomInstanceOperationValue(context, instanceId));
+  }
+
+
+  void LuaJobManager::Lock::Connect(size_t operation1,
+                                    size_t operation2)
+  {
+    assert(jobLock_.get() != NULL);
+    jobLock_->Connect(operation1, operation2);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/LuaJobManager.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,115 @@
+/**
+ * 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/DicomParsing/DicomModification.h"
+#include "../../Core/JobsEngine/JobsEngine.h"
+#include "../../Core/JobsEngine/Operations/SequenceOfOperationsJob.h"
+#include "../../Core/WebServiceParameters.h"
+
+namespace Orthanc
+{
+  class ServerContext;
+  
+  class LuaJobManager : private SequenceOfOperationsJob::IObserver
+  {
+  private:
+    boost::mutex              mutex_;
+    std::string               currentId_;
+    SequenceOfOperationsJob*  currentJob_;
+    size_t                    maxOperations_;
+    int                       priority_;
+    unsigned int              trailingTimeout_;
+    unsigned int              dicomTimeout_;
+
+    virtual void SignalDone(const SequenceOfOperationsJob& job);
+
+  public:
+    LuaJobManager();
+
+    void SetMaxOperationsPerJob(size_t count);
+
+    void SetPriority(int priority);
+
+    void SetTrailingOperationTimeout(unsigned int timeout);
+
+    void AwakeTrailingSleep();
+
+    class Lock : public boost::noncopyable
+    {
+    private:
+      LuaJobManager&                                that_;
+      boost::mutex::scoped_lock                     lock_;
+      JobsEngine&                                   engine_;
+      std::auto_ptr<SequenceOfOperationsJob::Lock>  jobLock_;
+      bool                                          isNewJob_;
+
+    public:
+      Lock(LuaJobManager& that,
+           JobsEngine& engine);
+
+      ~Lock();
+
+      size_t AddLogOperation();
+
+      size_t AddDeleteResourceOperation(ServerContext& context);
+
+      size_t AddStoreScuOperation(const std::string& localAet,
+                                  const RemoteModalityParameters& modality);
+
+      size_t AddStorePeerOperation(const WebServiceParameters& peer);
+
+      size_t AddSystemCallOperation(const std::string& command);
+
+      size_t AddSystemCallOperation(const std::string& command,
+                                    const std::vector<std::string>& preArguments,
+                                    const std::vector<std::string>& postArguments);
+
+      size_t AddModifyInstanceOperation(ServerContext& context,
+                                        DicomModification* modification);
+
+      void AddNullInput(size_t operation); 
+
+      void AddStringInput(size_t operation,
+                          const std::string& content);
+
+      void AddDicomInstanceInput(size_t operation,
+                                 ServerContext& context,
+                                 const std::string& instanceId);
+
+      void Connect(size_t operation1,
+                   size_t operation2);
+    };
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/ModifyInstanceOperation.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,135 @@
+/**
+ * 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 "ModifyInstanceOperation.h"
+
+#include "DicomInstanceOperationValue.h"
+
+#include "../../Core/Logging.h"
+
+namespace Orthanc
+{
+  ModifyInstanceOperation::ModifyInstanceOperation(ServerContext& context,
+                                                   RequestOrigin origin,
+                                                   DicomModification* modification) :
+    context_(context),
+    origin_(origin),
+    modification_(modification)
+  {
+    if (modification == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    
+    modification_->SetAllowManualIdentifiers(true);
+
+    if (modification_->IsReplaced(DICOM_TAG_PATIENT_ID))
+    {
+      modification_->SetLevel(ResourceType_Patient);
+    }
+    else if (modification_->IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+    {
+      modification_->SetLevel(ResourceType_Study);
+    }
+    else if (modification_->IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+    {
+      modification_->SetLevel(ResourceType_Series);
+    }
+    else
+    {
+      modification_->SetLevel(ResourceType_Instance);
+    }
+
+    if (origin_ != RequestOrigin_Lua)
+    {
+      // TODO If issued from HTTP, "remoteIp" and "username" must be provided
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+  void ModifyInstanceOperation::Apply(JobOperationValues& outputs,
+                                      const JobOperationValue& input,
+                                      IDicomConnectionManager& connectionManager)
+  {
+    if (input.GetType() != JobOperationValue::Type_DicomInstance)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    const DicomInstanceOperationValue& instance =
+      dynamic_cast<const DicomInstanceOperationValue&>(input);
+
+    LOG(INFO) << "Lua: Modifying instance " << instance.GetId();
+
+    std::auto_ptr<ParsedDicomFile> modified;
+    
+    {
+      ServerContext::DicomCacheLocker lock(context_, instance.GetId());
+      modified.reset(lock.GetDicom().Clone(true));
+    }
+
+    try
+    {
+      modification_->Apply(*modified);
+
+      DicomInstanceToStore toStore;
+      assert(origin_ == RequestOrigin_Lua);
+      toStore.SetLuaOrigin();
+      toStore.SetParsedDicomFile(*modified);
+
+      // TODO other metadata
+      toStore.AddMetadata(ResourceType_Instance, MetadataType_ModifiedFrom, instance.GetId());
+
+      std::string modifiedId;
+      context_.Store(modifiedId, toStore);
+
+      // Only chain with other commands if this command succeeds
+      outputs.Append(new DicomInstanceOperationValue(instance.GetServerContext(), modifiedId));
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "Lua: Unable to modify instance " << instance.GetId()
+                 << ": " << e.What();
+    }
+  }
+
+
+  void ModifyInstanceOperation::Serialize(Json::Value& target) const
+  {
+    target["Type"] = "ModifyInstance";
+    target["Origin"] = EnumerationToString(origin_);
+    modification_->Serialize(target["Modification"]);
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/ModifyInstanceOperation.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,67 @@
+/**
+ * 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/JobsEngine/Operations/IJobOperation.h"
+#include "../../Core/DicomParsing/DicomModification.h"
+
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class ModifyInstanceOperation : public IJobOperation
+  {
+  private:
+    ServerContext&                    context_;
+    RequestOrigin                     origin_;
+    std::auto_ptr<DicomModification>  modification_;
+    
+  public:
+    ModifyInstanceOperation(ServerContext& context,
+                            RequestOrigin origin,
+                            DicomModification* modification);  // Takes ownership
+
+    const DicomModification& GetModification() const
+    {
+      return *modification_;
+    }
+
+    virtual void Apply(JobOperationValues& outputs,
+                       const JobOperationValue& input,
+                       IDicomConnectionManager& connectionManager);
+
+    virtual void Serialize(Json::Value& target) const;
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,73 @@
+/**
+ * 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 "OrthancJobUnserializer.h"
+
+#include "../../Core/Logging.h"
+#include "../../Core/OrthancException.h"
+
+#include "DicomInstanceOperationValue.h"
+
+namespace Orthanc
+{
+  IJob* OrthancJobUnserializer::UnserializeJob(const Json::Value& source)
+  {
+    const std::string type = GetString(source, "Type");
+
+    return GenericJobUnserializer::UnserializeJob(source);
+  }
+
+
+  IJobOperation* OrthancJobUnserializer::UnserializeOperation(const Json::Value& source)
+  {
+    const std::string type = GetString(source, "Type");
+
+    return GenericJobUnserializer::UnserializeOperation(source);
+  }
+
+
+  JobOperationValue* OrthancJobUnserializer::UnserializeValue(const Json::Value& source)
+  {
+    const std::string type = GetString(source, "Type");
+
+    if (type == "DicomInstance")
+    {
+      return new DicomInstanceOperationValue(context_, GetString(source, "ID"));
+    }
+    else
+    {
+      return GenericJobUnserializer::UnserializeValue(source);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,58 @@
+/**
+ * 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 "../ServerContext.h"
+#include "../../Core/JobsEngine/GenericJobUnserializer.h"
+
+namespace Orthanc
+{
+  class OrthancJobUnserializer : public GenericJobUnserializer
+  {
+  private:
+    ServerContext&  context_;
+
+  public:
+    OrthancJobUnserializer(ServerContext& context) :
+      context_(context)
+    {
+    }
+
+    virtual IJob* UnserializeJob(const Json::Value& source);
+
+    virtual IJobOperation* UnserializeOperation(const Json::Value& source);
+
+    virtual JobOperationValue* UnserializeValue(const Json::Value& source);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,97 @@
+/**
+ * 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 "OrthancPeerStoreJob.h"
+
+#include "../../Core/Logging.h"
+
+
+namespace Orthanc
+{
+  bool OrthancPeerStoreJob::HandleInstance(const std::string& instance)
+  {
+    //boost::this_thread::sleep(boost::posix_time::milliseconds(500));
+
+    if (client_.get() == NULL)
+    {
+      client_.reset(new HttpClient(peer_, "instances"));
+      client_->SetMethod(HttpMethod_Post);
+    }
+      
+    LOG(INFO) << "Sending instance " << instance << " to peer \"" 
+              << peer_.GetUrl() << "\"";
+
+    context_.ReadDicom(client_->GetBody(), instance);
+
+    std::string answer;
+    if (client_->Apply(answer))
+    {
+      return true;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_NetworkProtocol);
+    }
+  }
+    
+
+  void OrthancPeerStoreJob::SetPeer(const WebServiceParameters& peer)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      peer_ = peer;
+    }
+  }
+
+
+  void OrthancPeerStoreJob::ReleaseResources()   // For pausing jobs
+  {
+    client_.reset(NULL);
+  }
+
+
+  void OrthancPeerStoreJob::GetPublicContent(Json::Value& value)
+  {
+    Json::Value v;
+    peer_.ToJson(v);
+    value["Peer"] = v;
+        
+    value["InstancesCount"] = static_cast<uint32_t>(GetInstances().size());
+    value["FailedInstancesCount"] = static_cast<uint32_t>(GetFailedInstances().size());
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,76 @@
+/**
+ * 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/JobsEngine/SetOfInstancesJob.h"
+#include "../../Core/HttpClient.h"
+
+#include "../ServerContext.h"
+
+
+namespace Orthanc
+{
+  class OrthancPeerStoreJob : public SetOfInstancesJob
+  {
+  private:
+    ServerContext&             context_;
+    WebServiceParameters       peer_;
+    std::auto_ptr<HttpClient>  client_;
+
+  protected:
+    virtual bool HandleInstance(const std::string& instance);
+    
+  public:
+    OrthancPeerStoreJob(ServerContext& context) :
+      context_(context)
+    {
+    }
+
+    void SetPeer(const WebServiceParameters& peer);
+
+    const WebServiceParameters& GetPeer() const
+    {
+      return peer_;
+    }
+
+    virtual void ReleaseResources();   // For pausing jobs
+
+    virtual void GetJobType(std::string& target)
+    {
+      target = "OrthancPeerStore";
+    }
+
+    virtual void GetPublicContent(Json::Value& value);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/StorePeerOperation.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,90 @@
+/**
+ * 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 "StorePeerOperation.h"
+
+#include "DicomInstanceOperationValue.h"
+
+#include "../../Core/Logging.h"
+#include "../../Core/OrthancException.h"
+#include "../../Core/HttpClient.h"
+
+namespace Orthanc
+{
+  void StorePeerOperation::Apply(JobOperationValues& outputs,
+                                const JobOperationValue& input,
+                                 IDicomConnectionManager& connectionManager)
+  {
+    // Configure the HTTP client
+    HttpClient client(peer_, "instances");
+    client.SetMethod(HttpMethod_Post);
+
+    if (input.GetType() != JobOperationValue::Type_DicomInstance)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    const DicomInstanceOperationValue& instance =
+      dynamic_cast<const DicomInstanceOperationValue&>(input);
+
+    LOG(INFO) << "Lua: Sending instance " << instance.GetId() << " to Orthanc peer \"" 
+              << peer_.GetUrl() << "\"";
+
+    try
+    {
+      instance.ReadContent(client.GetBody());
+
+      std::string answer;
+      if (!client.Apply(answer))
+      {
+        LOG(ERROR) << "Lua: Unable to send instance " << instance.GetId()
+                   << " to Orthanc peer \"" << peer_.GetUrl();
+      }
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "Lua: Unable to send instance " << instance.GetId()
+                 << " to Orthanc peer \"" << peer_.GetUrl() << "\": " << e.What();
+    }
+
+    outputs.Append(input.Clone());
+  }
+
+  
+  void StorePeerOperation::Serialize(Json::Value& result) const
+  {
+    result["Type"] = "StorePeer";
+    peer_.ToJson(result["Remote"]);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/StorePeerOperation.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,59 @@
+/**
+ * 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/JobsEngine/Operations/IJobOperation.h"
+#include "../../Core/WebServiceParameters.h"
+
+namespace Orthanc
+{
+  class StorePeerOperation : public IJobOperation
+  {
+  private:
+    WebServiceParameters peer_;
+
+  public:
+    StorePeerOperation(const WebServiceParameters& peer) :
+    peer_(peer)
+    {
+    }
+
+    virtual void Apply(JobOperationValues& outputs,
+                       const JobOperationValue& input,
+                       IDicomConnectionManager& connectionManager);
+
+    virtual void Serialize(Json::Value& result) const;
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/StoreScuOperation.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,90 @@
+/**
+ * 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 "StoreScuOperation.h"
+
+#include "DicomInstanceOperationValue.h"
+
+#include "../../Core/Logging.h"
+#include "../../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  void StoreScuOperation::Apply(JobOperationValues& outputs,
+                                const JobOperationValue& input,
+                                IDicomConnectionManager& connectionManager)
+  {
+    std::auto_ptr<IDicomConnectionManager::IResource> resource
+      (connectionManager.AcquireConnection(localAet_, modality_));
+
+    if (resource.get() == NULL)
+    {
+      LOG(ERROR) << "Lua: Cannot connect to modality: " << modality_.GetApplicationEntityTitle();
+      return;
+    }
+
+    if (input.GetType() != JobOperationValue::Type_DicomInstance)
+    {
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    const DicomInstanceOperationValue& instance =
+      dynamic_cast<const DicomInstanceOperationValue&>(input);
+
+    LOG(INFO) << "Lua: Sending instance " << instance.GetId() << " to modality \"" 
+              << modality_.GetApplicationEntityTitle() << "\"";
+
+    try
+    {
+      std::string dicom;
+      instance.ReadContent(dicom);
+      resource->GetConnection().Store(dicom);
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "Lua: Unable to send instance " << instance.GetId() << " to modality \"" 
+                 << modality_.GetApplicationEntityTitle() << "\": " << e.What();
+    }
+
+    outputs.Append(input.Clone());
+  }
+
+  
+  void StoreScuOperation::Serialize(Json::Value& result) const
+  {
+    result["Type"] = "StoreScu";
+    result["LocalAET"] = localAet_;
+    modality_.ToJson(result["Modality"]);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/StoreScuOperation.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,63 @@
+/**
+ * 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/JobsEngine/Operations/IJobOperation.h"
+
+#include "../../Core/DicomNetworking/RemoteModalityParameters.h"
+
+namespace Orthanc
+{
+  class StoreScuOperation : public IJobOperation
+  {
+  private:
+    std::string               localAet_;
+    RemoteModalityParameters  modality_;
+    
+  public:
+    StoreScuOperation(const std::string& localAet,
+                      const RemoteModalityParameters& modality) :
+      localAet_(localAet),
+      modality_(modality)
+    {
+    }
+
+    virtual void Apply(JobOperationValues& outputs,
+                       const JobOperationValue& input,
+                       IDicomConnectionManager& manager);
+
+    virtual void Serialize(Json::Value& result) const;
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/SystemCallOperation.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,140 @@
+/**
+ * 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 "SystemCallOperation.h"
+
+#include "DicomInstanceOperationValue.h"
+
+#include "../../Core/JobsEngine/Operations/StringOperationValue.h"
+#include "../../Core/Logging.h"
+#include "../../Core/TemporaryFile.h"
+#include "../../Core/Toolbox.h"
+
+namespace Orthanc
+{
+  void SystemCallOperation::Apply(JobOperationValues& outputs,
+                                  const JobOperationValue& input,
+                                  IDicomConnectionManager& connectionManager)
+  {
+    std::vector<std::string> arguments = preArguments_;
+
+    arguments.reserve(arguments.size() + postArguments_.size() + 1);
+
+    std::auto_ptr<TemporaryFile> tmp;
+    
+    switch (input.GetType())
+    {
+      case JobOperationValue::Type_DicomInstance:
+      {
+        const DicomInstanceOperationValue& instance =
+          dynamic_cast<const DicomInstanceOperationValue&>(input);
+
+        std::string dicom;
+        instance.ReadContent(dicom);
+
+        tmp.reset(new TemporaryFile);
+        tmp->Write(dicom);
+        
+        arguments.push_back(tmp->GetPath());
+        break;
+      }
+
+      case JobOperationValue::Type_String:
+      {
+        const StringOperationValue& value =
+          dynamic_cast<const StringOperationValue&>(input);
+
+        arguments.push_back(value.GetContent());
+        break;
+      }
+
+      case JobOperationValue::Type_Null:
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    for (size_t i = 0; i < postArguments_.size(); i++)
+    {
+      arguments.push_back(postArguments_[i]);
+    }
+
+    std::string info = command_;
+    for (size_t i = 0; i < arguments.size(); i++)
+    {
+      info += " " + arguments[i];
+    }
+    
+    LOG(INFO) << "Lua: System call: \"" << info << "\"";
+
+    try
+    {
+      SystemToolbox::ExecuteSystemCommand(command_, arguments);
+
+      // Only chain with other commands if this operation succeeds
+      outputs.Append(input.Clone());
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "Lua: Failed system call - \"" << info << "\": " << e.What();
+    }
+  }
+
+
+  void SystemCallOperation::Serialize(Json::Value& result) const
+  {
+    result["Type"] = "SystemCall";
+    result["Command"] = command_;
+
+    Json::Value tmp;
+
+    tmp = Json::arrayValue;
+    for (size_t i = 0; i < preArguments_.size(); i++)
+    {
+      tmp.append(preArguments_[i]);
+    }
+
+    result["PreArguments"] = tmp;
+
+    tmp = Json::arrayValue;
+    for (size_t i = 0; i < postArguments_.size(); i++)
+    {
+      tmp.append(postArguments_[i]);
+    }
+
+    result["PostArguments"] = tmp;
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/SystemCallOperation.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,81 @@
+/**
+ * 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/JobsEngine/Operations/IJobOperation.h"
+
+#include <string>
+
+namespace Orthanc
+{
+  class SystemCallOperation : public IJobOperation
+  {
+  private:
+    std::string               command_;
+    std::vector<std::string>  preArguments_;
+    std::vector<std::string>  postArguments_;
+    
+  public:
+    SystemCallOperation(const std::string& command) :
+      command_(command)
+    {
+    }
+
+    SystemCallOperation(const std::string& command,
+                        const std::vector<std::string>& preArguments,
+                        const std::vector<std::string>& postArguments) :
+      command_(command),
+      preArguments_(preArguments),
+      postArguments_(postArguments)
+    {
+    }
+
+    void AddPreArgument(const std::string& argument)
+    {
+      preArguments_.push_back(argument);
+    }
+
+    void AddPostArgument(const std::string& argument)
+    {
+      postArguments_.push_back(argument);
+    }
+
+    virtual void Apply(JobOperationValues& outputs,
+                       const JobOperationValue& input,
+                       IDicomConnectionManager& connectionManager);
+
+    virtual void Serialize(Json::Value& result) const;
+  };
+}
+
--- a/OrthancServer/main.cpp	Wed May 23 10:08:04 2018 +0200
+++ b/OrthancServer/main.cpp	Wed May 23 14:28:25 2018 +0200
@@ -42,7 +42,6 @@
 #include "../Core/Lua/LuaFunctionCall.h"
 #include "../Core/DicomFormat/DicomArray.h"
 #include "../Core/DicomNetworking/DicomServer.h"
-#include "../Core/DicomNetworking/ReusableDicomUserConnection.h"
 #include "OrthancInitialization.h"
 #include "ServerContext.h"
 #include "OrthancFindRequestHandler.h"
@@ -168,9 +167,9 @@
 class OrthancApplicationEntityFilter : public IApplicationEntityFilter
 {
 private:
-  ServerContext& context_;
-  bool           alwaysAllowEcho_;
-  bool           alwaysAllowStore_;
+  ServerContext&  context_;
+  bool            alwaysAllowEcho_;
+  bool            alwaysAllowStore_;
 
 public:
   OrthancApplicationEntityFilter(ServerContext& context) :
@@ -264,13 +263,13 @@
     }
 
     {
-      std::string lua = "Is" + configuration;
+      std::string name = "Is" + configuration;
 
-      LuaScripting::Locker locker(context_.GetLua());
+      LuaScripting::Lock lock(context_.GetLuaScripting());
       
-      if (locker.GetLua().IsExistingFunction(lua.c_str()))
+      if (lock.GetLua().IsExistingFunction(name.c_str()))
       {
-        LuaFunctionCall call(locker.GetLua(), lua.c_str());
+        LuaFunctionCall call(lock.GetLua(), name.c_str());
         call.PushString(remoteAet);
         call.PushString(remoteIp);
         call.PushString(calledAet);
@@ -291,11 +290,11 @@
     {
       std::string lua = "Is" + std::string(configuration);
 
-      LuaScripting::Locker locker(context_.GetLua());
+      LuaScripting::Lock lock(context_.GetLuaScripting());
       
-      if (locker.GetLua().IsExistingFunction(lua.c_str()))
+      if (lock.GetLua().IsExistingFunction(lua.c_str()))
       {
-        LuaFunctionCall call(locker.GetLua(), lua.c_str());
+        LuaFunctionCall call(lock.GetLua(), lua.c_str());
         call.PushString(remoteAet);
         call.PushString(remoteIp);
         call.PushString(calledAet);
@@ -311,7 +310,7 @@
 class MyIncomingHttpRequestFilter : public IIncomingHttpRequestFilter
 {
 private:
-  ServerContext& context_;
+  ServerContext&   context_;
   OrthancPlugins*  plugins_;
 
 public:
@@ -327,7 +326,7 @@
                          const char* ip,
                          const char* username,
                          const IHttpHandler::Arguments& httpHeaders,
-                         const IHttpHandler::GetArguments& getArguments) const
+                         const IHttpHandler::GetArguments& getArguments)
   {
     if (plugins_ != NULL &&
         !plugins_->IsAllowed(method, uri, ip, username, httpHeaders, getArguments))
@@ -337,12 +336,12 @@
 
     static const char* HTTP_FILTER = "IncomingHttpRequestFilter";
 
-    LuaScripting::Locker locker(context_.GetLua());
+    LuaScripting::Lock lock(context_.GetLuaScripting());
 
     // Test if the instance must be filtered out
-    if (locker.GetLua().IsExistingFunction(HTTP_FILTER))
+    if (lock.GetLua().IsExistingFunction(HTTP_FILTER))
     {
-      LuaFunctionCall call(locker.GetLua(), HTTP_FILTER);
+      LuaFunctionCall call(lock.GetLua(), HTTP_FILTER);
 
       switch (method)
       {
@@ -574,6 +573,7 @@
     PrintErrorCode(ErrorCode_NotAcceptable, "Cannot send a response which is acceptable according to the Accept HTTP header");
     PrintErrorCode(ErrorCode_NullPointer, "Cannot handle a NULL pointer");
     PrintErrorCode(ErrorCode_DatabaseUnavailable, "The database is currently not available (probably a transient situation)");
+    PrintErrorCode(ErrorCode_CanceledJob, "This job was canceled");
     PrintErrorCode(ErrorCode_SQLiteNotOpened, "SQLite: The database is not opened");
     PrintErrorCode(ErrorCode_SQLiteAlreadyOpened, "SQLite: Connection is already open");
     PrintErrorCode(ErrorCode_SQLiteCannotOpen, "SQLite: Unable to open the database");
@@ -640,25 +640,6 @@
 
 
 
-static void LoadLuaScripts(ServerContext& context)
-{
-  std::list<std::string> luaScripts;
-  Configuration::GetGlobalListOfStringsParameter(luaScripts, "LuaScripts");
-  for (std::list<std::string>::const_iterator
-         it = luaScripts.begin(); it != luaScripts.end(); ++it)
-  {
-    std::string path = Configuration::InterpretStringParameterAsPath(*it);
-    LOG(WARNING) << "Installing the Lua scripts from: " << path;
-    std::string script;
-    SystemToolbox::ReadFile(script, path);
-
-    LuaScripting::Locker locker(context.GetLua());
-    locker.GetLua().Execute(script);
-  }
-}
-
-
-
 #if ORTHANC_ENABLE_PLUGINS == 1
 static void LoadPlugins(OrthancPlugins& plugins)
 {
@@ -689,7 +670,8 @@
   }
 #endif
 
-  context.GetLua().Execute("Initialize");
+  context.GetLuaScripting().Start();
+  context.GetLuaScripting().Execute("Initialize");
 
   bool restart;
 
@@ -723,7 +705,8 @@
     }
   }
 
-  context.GetLua().Execute("Finalize");
+  context.GetLuaScripting().Execute("Finalize");
+  context.GetLuaScripting().Stop();
 
 #if ORTHANC_ENABLE_PLUGINS == 1
   if (context.HasPlugins())
@@ -1011,7 +994,11 @@
     context.GetIndex().SetMaximumStorageSize(0);
   }
 
-  LoadLuaScripts(context);
+  LOG(INFO) << "Initializing Lua for the event handler";
+  context.GetLuaScripting().LoadGlobalConfiguration();
+
+  context.GetJobsEngine().GetRegistry().SetMaxCompletedJobs
+    (Configuration::GetGlobalUnsignedIntegerParameter("JobsHistorySize", 10));
 
 #if ORTHANC_ENABLE_PLUGINS == 1
   if (plugins)
--- a/Plugins/Engine/OrthancPlugins.cpp	Wed May 23 10:08:04 2018 +0200
+++ b/Plugins/Engine/OrthancPlugins.cpp	Wed May 23 14:28:25 2018 +0200
@@ -3115,7 +3115,7 @@
                                  const char* ip,
                                  const char* username,
                                  const IHttpHandler::Arguments& httpHeaders,
-                                 const IHttpHandler::GetArguments& getArguments) const
+                                 const IHttpHandler::GetArguments& getArguments)
   {
     OrthancPluginHttpMethod cMethod = Plugins::Convert(method);
 
--- a/Plugins/Engine/OrthancPlugins.h	Wed May 23 10:08:04 2018 +0200
+++ b/Plugins/Engine/OrthancPlugins.h	Wed May 23 14:28:25 2018 +0200
@@ -284,7 +284,7 @@
                            const char* ip,
                            const char* username,
                            const IHttpHandler::Arguments& httpHeaders,
-                           const IHttpHandler::GetArguments& getArguments) const;
+                           const IHttpHandler::GetArguments& getArguments);
 
     bool HasFindHandler();
 
--- a/Plugins/Include/orthanc/OrthancCPlugin.h	Wed May 23 10:08:04 2018 +0200
+++ b/Plugins/Include/orthanc/OrthancCPlugin.h	Wed May 23 14:28:25 2018 +0200
@@ -235,6 +235,7 @@
     OrthancPluginErrorCode_NotAcceptable = 34    /*!< Cannot send a response which is acceptable according to the Accept HTTP header */,
     OrthancPluginErrorCode_NullPointer = 35    /*!< Cannot handle a NULL pointer */,
     OrthancPluginErrorCode_DatabaseUnavailable = 36    /*!< The database is currently not available (probably a transient situation) */,
+    OrthancPluginErrorCode_CanceledJob = 37    /*!< This job was canceled */,
     OrthancPluginErrorCode_SQLiteNotOpened = 1000    /*!< SQLite: The database is not opened */,
     OrthancPluginErrorCode_SQLiteAlreadyOpened = 1001    /*!< SQLite: Connection is already open */,
     OrthancPluginErrorCode_SQLiteCannotOpen = 1002    /*!< SQLite: Unable to open the database */,
--- a/Resources/CMake/OrthancFrameworkConfiguration.cmake	Wed May 23 10:08:04 2018 +0200
+++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake	Wed May 23 14:28:25 2018 +0200
@@ -423,13 +423,12 @@
       ${ORTHANC_ROOT}/Core/DicomNetworking/DicomFindAnswers.cpp
       ${ORTHANC_ROOT}/Core/DicomNetworking/DicomServer.cpp
       ${ORTHANC_ROOT}/Core/DicomNetworking/DicomUserConnection.cpp
-      ${ORTHANC_ROOT}/Core/DicomNetworking/RemoteModalityParameters.cpp
-      ${ORTHANC_ROOT}/Core/DicomNetworking/ReusableDicomUserConnection.cpp
-
       ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/CommandDispatcher.cpp
       ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/FindScp.cpp
       ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/MoveScp.cpp
       ${ORTHANC_ROOT}/Core/DicomNetworking/Internals/StoreScp.cpp
+      ${ORTHANC_ROOT}/Core/DicomNetworking/RemoteModalityParameters.cpp
+      ${ORTHANC_ROOT}/Core/DicomNetworking/TimeoutDicomConnectionManager.cpp
       )
   else()
     add_definitions(-DORTHANC_ENABLE_DCMTK_NETWORKING=0)
@@ -498,11 +497,18 @@
   list(APPEND ORTHANC_CORE_SOURCES_INTERNAL
     ${ORTHANC_ROOT}/Core/Cache/SharedArchive.cpp
     ${ORTHANC_ROOT}/Core/FileStorage/FilesystemStorage.cpp
-    ${ORTHANC_ROOT}/Core/MultiThreading/BagOfTasksProcessor.cpp
-    ${ORTHANC_ROOT}/Core/MultiThreading/Mutex.cpp
-    ${ORTHANC_ROOT}/Core/MultiThreading/ReaderWriterLock.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/GenericJobUnserializer.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/IJobUnserializer.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/JobInfo.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/JobStatus.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/JobStepResult.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/JobsEngine.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/JobsRegistry.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/Operations/JobOperationValues.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/Operations/LogJobOperation.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/SetOfInstancesJob.cpp
     ${ORTHANC_ROOT}/Core/MultiThreading/RunnableWorkersPool.cpp
-    ${ORTHANC_ROOT}/Core/MultiThreading/Semaphore.cpp
     ${ORTHANC_ROOT}/Core/MultiThreading/SharedMessageQueue.cpp
     ${ORTHANC_ROOT}/Core/SharedLibrary.cpp
     ${ORTHANC_ROOT}/Core/SystemToolbox.cpp
--- a/Resources/Configuration.json	Wed May 23 10:08:04 2018 +0200
+++ b/Resources/Configuration.json	Wed May 23 14:28:25 2018 +0200
@@ -43,6 +43,11 @@
   "Plugins" : [
   ],
 
+  // Maximum number of processing jobs that are simultanously running
+  // at any given time. A value of "0" indicates to use all the
+  // available CPU logical cores. To emulate Orthanc <= 1.3.2, set
+  // this value to "1".
+  "ConcurrentJobs" : 2,
 
 
   /**
@@ -310,10 +315,11 @@
   // this option might prevent the upgrade to newer versions of Orthanc.
   "StoreDicom" : true,
 
-  // DICOM associations are kept open as long as new DICOM commands
-  // are issued. This option sets the number of seconds of inactivity
-  // to wait before automatically closing a DICOM association. If set
-  // to 0, the connection is closed immediately.
+  // DICOM associations initiated by Lua scripts are kept open as long
+  // as new DICOM commands are issued. This option sets the number of
+  // seconds of inactivity to wait before automatically closing a
+  // DICOM association used by Lua. If set to 0, the connection is
+  // closed immediately.
   "DicomAssociationCloseDelay" : 5,
 
   // Maximum number of query/retrieve DICOM requests that are
@@ -371,5 +377,10 @@
     // "00e1,10c2" : [ "UI", "PET-CT Multi Modality Name", 1, 1, "ELSCINT1" ]
     // "7053,1003" : [ "ST", "Original Image Filename", 1, 1, "Philips PET Private Group" ]
     // "2001,5f" : [ "SQ", "StackSequence", 1, 1, "Philips Imaging DD 001" ]
-  }
+  },
+
+  // Maximum number of completed jobs that are kept in memory. A
+  // processing job is considered as complete once it is tagged as
+  // "Success" or "Failure".
+  "JobsHistorySize" : 10
 }
--- a/Resources/ErrorCodes.json	Wed May 23 10:08:04 2018 +0200
+++ b/Resources/ErrorCodes.json	Wed May 23 14:28:25 2018 +0200
@@ -207,6 +207,11 @@
     "Name": "DatabaseUnavailable", 
     "Description": "The database is currently not available (probably a transient situation)"
   }, 
+  {
+    "Code": 37, 
+    "Name": "CanceledJob", 
+    "Description": "This job was canceled"
+  }, 
 
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/BagOfTasks.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,84 @@
+/**
+ * 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 "../ICommand.h"
+
+#include <list>
+#include <cstddef>
+
+namespace Orthanc
+{
+  class BagOfTasks : public boost::noncopyable
+  {
+  private:
+    typedef std::list<ICommand*>  Tasks;
+
+    Tasks  tasks_;
+
+  public:
+    ~BagOfTasks()
+    {
+      for (Tasks::iterator it = tasks_.begin(); it != tasks_.end(); ++it)
+      {
+        delete *it;
+      }
+    }
+
+    ICommand* Pop()
+    {
+      ICommand* task = tasks_.front();
+      tasks_.pop_front();
+      return task;
+    }
+
+    void Push(ICommand* task)   // Takes ownership
+    {
+      if (task != NULL)
+      {
+        tasks_.push_back(task);
+      }
+    }
+
+    size_t GetSize() const
+    {
+      return tasks_.size();
+    }
+
+    bool IsEmpty() const
+    {
+      return tasks_.empty();
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/BagOfTasksProcessor.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,277 @@
+/**
+ * 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 "../PrecompiledHeaders.h"
+#include "BagOfTasksProcessor.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+
+#include <stdio.h>
+
+namespace Orthanc
+{
+  class BagOfTasksProcessor::Task : public IDynamicObject
+  {
+  private:
+    uint64_t                 bag_;
+    std::auto_ptr<ICommand>  command_;
+
+  public:
+    Task(uint64_t  bag,
+         ICommand* command) :
+      bag_(bag),
+      command_(command)
+    {
+    }
+
+    bool Execute()
+    {
+      try
+      {
+        return command_->Execute();
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Exception while processing a bag of tasks: " << e.What();
+        return false;
+      }
+      catch (std::runtime_error& e)
+      {
+        LOG(ERROR) << "Runtime exception while processing a bag of tasks: " << e.what();
+        return false;
+      }
+      catch (...)
+      {
+        LOG(ERROR) << "Native exception while processing a bag of tasks";
+        return false;
+      }
+    }
+
+    uint64_t GetBag()
+    {
+      return bag_;
+    }
+  };
+
+
+  void BagOfTasksProcessor::SignalProgress(Task& task,
+                                           Bag& bag)
+  {
+    assert(bag.done_ < bag.size_);
+
+    bag.done_ += 1;
+
+    if (bag.done_ == bag.size_)
+    {
+      exitStatus_[task.GetBag()] = (bag.status_ == BagStatus_Running);
+      bagFinished_.notify_all();
+    }
+  }
+
+  void BagOfTasksProcessor::Worker(BagOfTasksProcessor* that)
+  {
+    while (that->continue_)
+    {
+      std::auto_ptr<IDynamicObject> obj(that->queue_.Dequeue(100));
+      if (obj.get() != NULL)
+      {
+        Task& task = *dynamic_cast<Task*>(obj.get());
+
+        {
+          boost::mutex::scoped_lock lock(that->mutex_);
+
+          Bags::iterator bag = that->bags_.find(task.GetBag());
+          assert(bag != that->bags_.end());
+          assert(bag->second.done_ < bag->second.size_);
+
+          if (bag->second.status_ != BagStatus_Running)
+          {
+            // Do not execute this task, as its parent bag of tasks
+            // has failed or is tagged as canceled
+            that->SignalProgress(task, bag->second);
+            continue;
+          }
+        }
+
+        bool success = task.Execute();
+
+        {
+          boost::mutex::scoped_lock lock(that->mutex_);
+
+          Bags::iterator bag = that->bags_.find(task.GetBag());
+          assert(bag != that->bags_.end());
+
+          if (!success)
+          {
+            bag->second.status_ = BagStatus_Failed;
+          }
+
+          that->SignalProgress(task, bag->second);
+        }
+      }
+    }
+  }
+
+
+  void BagOfTasksProcessor::Cancel(int64_t bag)
+  {
+    boost::mutex::scoped_lock  lock(mutex_);
+
+    Bags::iterator it = bags_.find(bag);
+    if (it != bags_.end())
+    {
+      it->second.status_ = BagStatus_Canceled;
+    }
+  }
+
+
+  bool BagOfTasksProcessor::Join(int64_t bag)
+  {
+    boost::mutex::scoped_lock  lock(mutex_);
+
+    while (continue_)
+    {
+      ExitStatus::iterator it = exitStatus_.find(bag);
+      if (it == exitStatus_.end())  // The bag is still running
+      {
+        bagFinished_.wait(lock);
+      }
+      else
+      {
+        bool status = it->second;
+        exitStatus_.erase(it);
+        return status;
+      }
+    }
+
+    return false;   // The processor is stopping
+  }
+
+
+  float BagOfTasksProcessor::GetProgress(int64_t bag)
+  {
+    boost::mutex::scoped_lock  lock(mutex_);
+
+    Bags::const_iterator it = bags_.find(bag);
+    if (it == bags_.end())
+    {
+      // The bag of tasks has finished
+      return 1.0f;
+    }
+    else
+    {
+      return (static_cast<float>(it->second.done_) / 
+              static_cast<float>(it->second.size_));
+    }
+  }
+
+
+  bool BagOfTasksProcessor::Handle::Join()
+  {
+    if (hasJoined_)
+    {
+      return status_;
+    }
+    else
+    {
+      status_ = that_.Join(bag_);
+      hasJoined_ = true;
+      return status_;
+    }
+  }
+
+
+  BagOfTasksProcessor::BagOfTasksProcessor(size_t countThreads) : 
+    countBags_(0),
+    continue_(true)
+  {
+    if (countThreads == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    threads_.resize(countThreads);
+
+    for (size_t i = 0; i < threads_.size(); i++)
+    {
+      threads_[i] = new boost::thread(Worker, this);
+    }
+  }
+
+
+  BagOfTasksProcessor::~BagOfTasksProcessor()
+  {
+    continue_ = false;
+
+    bagFinished_.notify_all();   // Wakes up all the pending "Join()"
+
+    for (size_t i = 0; i < threads_.size(); i++)
+    {
+      if (threads_[i])
+      {
+        if (threads_[i]->joinable())
+        {
+          threads_[i]->join();
+        }
+
+        delete threads_[i];
+        threads_[i] = NULL;
+      }
+    }
+  }
+
+
+  BagOfTasksProcessor::Handle* BagOfTasksProcessor::Submit(BagOfTasks& tasks)
+  {
+    if (tasks.GetSize() == 0)
+    {
+      return new Handle(*this, 0, true);
+    }
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    uint64_t id = countBags_;
+    countBags_ += 1;
+
+    Bag bag(tasks.GetSize());
+    bags_[id] = bag;
+
+    while (!tasks.IsEmpty())
+    {
+      queue_.Enqueue(new Task(id, tasks.Pop()));
+    }
+
+    return new Handle(*this, id, false);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/BagOfTasksProcessor.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,150 @@
+/**
+ * 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 "BagOfTasks.h"
+#include "SharedMessageQueue.h"
+
+#include <stdint.h>
+#include <map>
+
+namespace Orthanc
+{
+  class BagOfTasksProcessor : public boost::noncopyable
+  {
+  private:
+    enum BagStatus
+    {
+      BagStatus_Running,
+      BagStatus_Canceled,
+      BagStatus_Failed
+    };
+
+
+    struct Bag
+    {
+      size_t    size_;
+      size_t    done_;
+      BagStatus status_;
+
+      Bag() :
+        size_(0),
+        done_(0),
+        status_(BagStatus_Failed)
+      {
+      }
+
+      explicit Bag(size_t size) : 
+        size_(size),
+        done_(0),
+        status_(BagStatus_Running)
+      {
+      }
+    };
+
+    class Task;
+
+
+    typedef std::map<uint64_t, Bag>   Bags;
+    typedef std::map<uint64_t, bool>  ExitStatus;
+
+    SharedMessageQueue  queue_;
+
+    boost::mutex  mutex_;
+    uint64_t  countBags_;
+    Bags bags_;
+    std::vector<boost::thread*>   threads_;
+    ExitStatus  exitStatus_;
+    bool continue_;
+
+    boost::condition_variable  bagFinished_;
+
+    static void Worker(BagOfTasksProcessor* that);
+
+    void Cancel(int64_t bag);
+
+    bool Join(int64_t bag);
+
+    float GetProgress(int64_t bag);
+
+    void SignalProgress(Task& task,
+                        Bag& bag);
+
+  public:
+    class Handle : public boost::noncopyable
+    {
+      friend class BagOfTasksProcessor;
+
+    private:
+      BagOfTasksProcessor&  that_;
+      uint64_t              bag_;
+      bool                  hasJoined_;
+      bool                  status_;
+ 
+      Handle(BagOfTasksProcessor&  that,
+             uint64_t bag,
+             bool empty) : 
+        that_(that),
+        bag_(bag),
+        hasJoined_(empty)
+      {
+      }
+
+    public:
+      ~Handle()
+      {
+        Join();
+      }
+
+      void Cancel()
+      {
+        that_.Cancel(bag_);
+      }
+
+      bool Join();
+
+      float GetProgress()
+      {
+        return that_.GetProgress(bag_);
+      }
+    };
+  
+
+    explicit BagOfTasksProcessor(size_t countThreads);
+
+    ~BagOfTasksProcessor();
+
+    Handle* Submit(BagOfTasks& tasks);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/ILockable.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,54 @@
+/**
+ * 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 <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class ILockable : public boost::noncopyable
+  {
+    friend class Locker;
+
+  protected:
+    virtual void Lock() = 0;
+
+    virtual void Unlock() = 0;
+
+  public:
+    virtual ~ILockable()
+    {
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/Locker.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,56 @@
+/**
+ * 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 "ILockable.h"
+
+namespace Orthanc
+{
+  class Locker : public boost::noncopyable
+  {
+  private:
+    ILockable& lockable_;
+
+  public:
+    Locker(ILockable& lockable) : lockable_(lockable)
+    {
+      lockable_.Lock();
+    }
+
+    virtual ~Locker()
+    {
+      lockable_.Unlock();
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/Mutex.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,122 @@
+/**
+ * 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 "../PrecompiledHeaders.h"
+#include "Mutex.h"
+
+#include "../OrthancException.h"
+
+#if defined(_WIN32)
+#include <windows.h>
+#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__)
+#include <pthread.h>
+#else
+#error Support your platform here
+#endif
+
+namespace Orthanc
+{
+#if defined (_WIN32)
+
+  struct Mutex::PImpl
+  {
+    CRITICAL_SECTION criticalSection_;
+  };
+
+  Mutex::Mutex()
+  {
+    pimpl_ = new PImpl;
+    ::InitializeCriticalSection(&pimpl_->criticalSection_);
+  }
+
+  Mutex::~Mutex()
+  {
+    ::DeleteCriticalSection(&pimpl_->criticalSection_);
+    delete pimpl_;
+  }
+
+  void Mutex::Lock()
+  {
+    ::EnterCriticalSection(&pimpl_->criticalSection_);
+  }
+
+  void Mutex::Unlock()
+  {
+    ::LeaveCriticalSection(&pimpl_->criticalSection_);
+  }
+
+
+#elif defined(__linux__) || defined(__FreeBSD_kernel__) || defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__)
+
+  struct Mutex::PImpl
+  {
+    pthread_mutex_t mutex_;
+  };
+
+  Mutex::Mutex()
+  {
+    pimpl_ = new PImpl;
+
+    if (pthread_mutex_init(&pimpl_->mutex_, NULL) != 0)
+    {
+      delete pimpl_;
+      throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+  Mutex::~Mutex()
+  {
+    pthread_mutex_destroy(&pimpl_->mutex_);
+    delete pimpl_;
+  }
+
+  void Mutex::Lock()
+  {
+    if (pthread_mutex_lock(&pimpl_->mutex_) != 0)
+    {
+      throw OrthancException(ErrorCode_InternalError);    
+    }
+  }
+
+  void Mutex::Unlock()
+  {
+    if (pthread_mutex_unlock(&pimpl_->mutex_) != 0)
+    {
+      throw OrthancException(ErrorCode_InternalError);    
+    }
+  }
+
+#else
+#error Support your plateform here
+#endif
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/Mutex.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,57 @@
+/**
+ * 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 "ILockable.h"
+
+namespace Orthanc
+{
+  class Mutex : public ILockable
+  {
+  private:
+    struct PImpl;
+
+    PImpl *pimpl_;
+
+  protected:
+    virtual void Lock();
+
+    virtual void Unlock();
+    
+  public:
+    Mutex();
+
+    ~Mutex();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/ReaderWriterLock.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,126 @@
+/**
+ * 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 "../PrecompiledHeaders.h"
+#include "ReaderWriterLock.h"
+
+#include <boost/thread/shared_mutex.hpp>
+
+namespace Orthanc
+{
+  namespace
+  {
+    // Anonymous namespace to avoid clashes between compilation
+    // modules.
+
+    class ReaderLockable : public ILockable
+    {
+    private:
+      boost::shared_mutex& lock_;
+
+    protected:
+      virtual void Lock()
+      {
+        lock_.lock_shared();
+      }
+
+      virtual void Unlock()
+      {
+        lock_.unlock_shared();        
+      }
+
+    public:
+      explicit ReaderLockable(boost::shared_mutex& lock) : lock_(lock)
+      {
+      }
+    };
+
+
+    class WriterLockable : public ILockable
+    {
+    private:
+      boost::shared_mutex& lock_;
+
+    protected:
+      virtual void Lock()
+      {
+        lock_.lock();
+      }
+
+      virtual void Unlock()
+      {
+        lock_.unlock();        
+      }
+
+    public:
+      explicit WriterLockable(boost::shared_mutex& lock) : lock_(lock)
+      {
+      }
+    };
+  }
+
+  struct ReaderWriterLock::PImpl
+  {
+    boost::shared_mutex lock_;
+    ReaderLockable reader_;
+    WriterLockable writer_;
+
+    PImpl() : reader_(lock_), writer_(lock_)
+    {
+    }
+  };
+
+
+  ReaderWriterLock::ReaderWriterLock()
+  {
+    pimpl_ = new PImpl;
+  }
+
+
+  ReaderWriterLock::~ReaderWriterLock()
+  {
+    delete pimpl_;
+  }
+
+
+  ILockable&  ReaderWriterLock::ForReader()
+  {
+    return pimpl_->reader_;
+  }
+
+
+  ILockable&  ReaderWriterLock::ForWriter()
+  {
+    return pimpl_->writer_;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/ReaderWriterLock.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,58 @@
+/**
+ * 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 "ILockable.h"
+
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class ReaderWriterLock : public boost::noncopyable
+  {
+  private:
+    struct PImpl;
+
+    PImpl *pimpl_;
+
+  public:
+    ReaderWriterLock();
+
+    virtual ~ReaderWriterLock();
+
+    ILockable& ForReader();
+
+    ILockable& ForWriter();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/Semaphore.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,65 @@
+/**
+ * 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 "../PrecompiledHeaders.h"
+#include "Semaphore.h"
+
+#include "../OrthancException.h"
+
+
+namespace Orthanc
+{
+  Semaphore::Semaphore(unsigned int count) : count_(count)
+  {
+  }
+
+  void Semaphore::Release()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    count_++;
+    condition_.notify_one(); 
+  }
+
+  void Semaphore::Acquire()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    while (count_ == 0)
+    {
+      condition_.wait(lock);
+    }
+
+    count_++;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/Semaphore.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,73 @@
+/**
+ * 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 <boost/noncopyable.hpp>
+#include <boost/thread.hpp>
+
+namespace Orthanc
+{
+  class Semaphore : public boost::noncopyable
+  {
+  private:
+    unsigned int count_;
+    boost::mutex mutex_;
+    boost::condition_variable condition_;
+
+  public:
+    explicit Semaphore(unsigned int count);
+
+    void Release();
+
+    void Acquire();
+
+    class Locker : public boost::noncopyable
+    {
+    private:
+      Semaphore&  that_;
+
+    public:
+      explicit Locker(Semaphore& that) :
+        that_(that)
+      {
+        that_.Acquire();
+      }
+
+      ~Locker()
+      {
+        that_.Release();
+      }
+    };
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/CallSystemCommand.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,85 @@
+/**
+ * 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 "CallSystemCommand.h"
+
+#include "../../Core/Logging.h"
+#include "../../Core/Toolbox.h"
+#include "../../Core/TemporaryFile.h"
+
+namespace Orthanc
+{
+  CallSystemCommand::CallSystemCommand(ServerContext& context,
+                                       const std::string& command,
+                                       const std::vector<std::string>& arguments) : 
+    context_(context),
+    command_(command),
+    arguments_(arguments)
+  {
+  }
+
+  bool CallSystemCommand::Apply(ListOfStrings& outputs,
+                                const ListOfStrings& inputs)
+  {
+    for (ListOfStrings::const_iterator
+           it = inputs.begin(); it != inputs.end(); ++it)
+    {
+      LOG(INFO) << "Calling system command " << command_ << " on instance " << *it;
+
+      try
+      {
+        std::string dicom;
+        context_.ReadDicom(dicom, *it);
+
+        TemporaryFile tmp;
+        tmp.Write(dicom);
+
+        std::vector<std::string> args = arguments_;
+        args.push_back(tmp.GetPath());
+
+        SystemToolbox::ExecuteSystemCommand(command_, args);
+
+        // Only chain with other commands if this command succeeds
+        outputs.push_back(*it);
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Unable to call system command " << command_ 
+                   << " on instance " << *it << " in a Lua script: " << e.What();
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/CallSystemCommand.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,56 @@
+/**
+ * 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 "IServerCommand.h"
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class CallSystemCommand : public IServerCommand
+  {
+  private:
+    ServerContext& context_;
+    std::string command_;
+    std::vector<std::string> arguments_;
+
+  public:
+    CallSystemCommand(ServerContext& context,
+                      const std::string& command,
+                      const std::vector<std::string>& arguments);
+
+    virtual bool Apply(ListOfStrings& outputs,
+                       const ListOfStrings& inputs);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/DeleteInstanceCommand.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,62 @@
+/**
+ * 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 "DeleteInstanceCommand.h"
+
+#include "../../Core/Logging.h"
+
+namespace Orthanc
+{
+  bool DeleteInstanceCommand::Apply(ListOfStrings& outputs,
+                                    const ListOfStrings& inputs)
+  {
+    for (ListOfStrings::const_iterator
+           it = inputs.begin(); it != inputs.end(); ++it)
+    {
+      LOG(INFO) << "Deleting instance " << *it;
+
+      try
+      {
+        Json::Value tmp;
+        context_.DeleteResource(tmp, *it, ResourceType_Instance);
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Unable to delete instance " << *it << ": " << e.What();
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/DeleteInstanceCommand.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,54 @@
+/**
+ * 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 "IServerCommand.h"
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class DeleteInstanceCommand : public IServerCommand
+  {
+  private:
+    ServerContext& context_;
+
+  public:
+    DeleteInstanceCommand(ServerContext& context) : context_(context)
+    {
+    }
+
+    virtual bool Apply(ListOfStrings& outputs,
+                       const ListOfStrings& inputs);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/IServerCommand.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,54 @@
+/**
+ * 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 <list>
+#include <string>
+#include <boost/noncopyable.hpp>
+
+namespace Orthanc
+{
+  class IServerCommand : public boost::noncopyable
+  {
+  public:
+    typedef std::list<std::string>  ListOfStrings;
+
+    virtual ~IServerCommand()
+    {
+    }
+
+    virtual bool Apply(ListOfStrings& outputs,
+                       const ListOfStrings& inputs) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ModifyInstanceCommand.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,124 @@
+/**
+ * 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 "ModifyInstanceCommand.h"
+
+#include "../../Core/Logging.h"
+
+namespace Orthanc
+{
+  ModifyInstanceCommand::ModifyInstanceCommand(ServerContext& context,
+                                               RequestOrigin origin,
+                                               DicomModification* modification) :
+    context_(context),
+    origin_(origin),
+    modification_(modification)
+  {
+    modification_->SetAllowManualIdentifiers(true);
+
+    if (modification_->IsReplaced(DICOM_TAG_PATIENT_ID))
+    {
+      modification_->SetLevel(ResourceType_Patient);
+    }
+    else if (modification_->IsReplaced(DICOM_TAG_STUDY_INSTANCE_UID))
+    {
+      modification_->SetLevel(ResourceType_Study);
+    }
+    else if (modification_->IsReplaced(DICOM_TAG_SERIES_INSTANCE_UID))
+    {
+      modification_->SetLevel(ResourceType_Series);
+    }
+    else
+    {
+      modification_->SetLevel(ResourceType_Instance);
+    }
+
+    if (origin_ != RequestOrigin_Lua)
+    {
+      // TODO If issued from HTTP, "remoteIp" and "username" must be provided
+      throw OrthancException(ErrorCode_NotImplemented);
+    }
+  }
+
+
+  ModifyInstanceCommand::~ModifyInstanceCommand()
+  {
+    if (modification_)
+    {
+      delete modification_;
+    }
+  }
+
+
+  bool ModifyInstanceCommand::Apply(ListOfStrings& outputs,
+                                    const ListOfStrings& inputs)
+  {
+    for (ListOfStrings::const_iterator
+           it = inputs.begin(); it != inputs.end(); ++it)
+    {
+      LOG(INFO) << "Modifying resource " << *it;
+
+      try
+      {
+        std::auto_ptr<ParsedDicomFile> modified;
+
+        {
+          ServerContext::DicomCacheLocker lock(context_, *it);
+          modified.reset(lock.GetDicom().Clone(true));
+        }
+
+        modification_->Apply(*modified);
+
+        DicomInstanceToStore toStore;
+        assert(origin_ == RequestOrigin_Lua);
+        toStore.SetLuaOrigin();
+        toStore.SetParsedDicomFile(*modified);
+        // TODO other metadata
+        toStore.AddMetadata(ResourceType_Instance, MetadataType_ModifiedFrom, *it);
+
+        std::string modifiedId;
+        context_.Store(modifiedId, toStore);
+
+        // Only chain with other commands if this command succeeds
+        outputs.push_back(modifiedId);
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Unable to modify instance " << *it << ": " << e.What();
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ModifyInstanceCommand.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,64 @@
+/**
+ * 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 "IServerCommand.h"
+#include "../ServerContext.h"
+#include "../../Core/DicomParsing/DicomModification.h"
+
+namespace Orthanc
+{
+  class ModifyInstanceCommand : public IServerCommand
+  {
+  private:
+    ServerContext& context_;
+    RequestOrigin origin_;
+    DicomModification* modification_;
+
+  public:
+    ModifyInstanceCommand(ServerContext& context,
+                          RequestOrigin origin,
+                          DicomModification* modification);  // takes the ownership
+
+    virtual ~ModifyInstanceCommand();
+
+    const DicomModification& GetModification() const
+    {
+      return *modification_;
+    }
+
+    virtual bool Apply(ListOfStrings& outputs,
+                       const ListOfStrings& inputs);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ReusableDicomUserConnection.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,188 @@
+/**
+ * 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 "../PrecompiledHeaders.h"
+#include "ReusableDicomUserConnection.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  static boost::posix_time::ptime Now()
+  {
+    return boost::posix_time::microsec_clock::local_time();
+  }
+
+  void ReusableDicomUserConnection::Open(const std::string& localAet,
+                                         const RemoteModalityParameters& remote)
+  {
+    if (connection_ != NULL &&
+        connection_->GetLocalApplicationEntityTitle() == localAet &&
+        connection_->GetRemoteApplicationEntityTitle() == remote.GetApplicationEntityTitle() &&
+        connection_->GetRemoteHost() == remote.GetHost() &&
+        connection_->GetRemotePort() == remote.GetPort() &&
+        connection_->GetRemoteManufacturer() == remote.GetManufacturer())
+    {
+      // The current connection can be reused
+      LOG(INFO) << "Reusing the previous SCU connection";
+      return;
+    }
+
+    Close();
+
+    connection_ = new DicomUserConnection();
+    connection_->SetLocalApplicationEntityTitle(localAet);
+    connection_->SetRemoteModality(remote);
+    connection_->Open();
+  }
+    
+  void ReusableDicomUserConnection::Close()
+  {
+    if (connection_ != NULL)
+    {
+      delete connection_;
+      connection_ = NULL;
+    }
+  }
+
+  void ReusableDicomUserConnection::CloseThread(ReusableDicomUserConnection* that)
+  {
+    for (;;)
+    {
+      boost::this_thread::sleep(boost::posix_time::milliseconds(100));
+      if (!that->continue_)
+      {
+        //LOG(INFO) << "Finishing the thread watching the global SCU connection";
+        return;
+      }
+
+      {
+        boost::mutex::scoped_lock lock(that->mutex_);
+        if (that->connection_ != NULL &&
+            Now() >= that->lastUse_ + that->timeBeforeClose_)
+        {
+          LOG(INFO) << "Closing the global SCU connection after timeout";
+          that->Close();
+        }
+      }
+    }
+  }
+    
+
+  ReusableDicomUserConnection::Locker::Locker(ReusableDicomUserConnection& that,
+                                              const std::string& localAet,
+                                              const RemoteModalityParameters& remote) :
+    ::Orthanc::Locker(that)
+  {
+    that.Open(localAet, remote);
+    connection_ = that.connection_;    
+  }
+
+
+  DicomUserConnection& ReusableDicomUserConnection::Locker::GetConnection()
+  {
+    if (connection_ == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    return *connection_;
+  }      
+
+  ReusableDicomUserConnection::ReusableDicomUserConnection() : 
+    connection_(NULL), 
+    timeBeforeClose_(boost::posix_time::seconds(5))  // By default, close connection after 5 seconds
+  {
+    lastUse_ = Now();
+    continue_ = true;
+    closeThread_ = boost::thread(CloseThread, this);
+  }
+
+  ReusableDicomUserConnection::~ReusableDicomUserConnection()
+  {
+    if (continue_)
+    {
+      LOG(ERROR) << "INTERNAL ERROR: ReusableDicomUserConnection::Finalize() should be invoked manually to avoid mess in the destruction order!";
+      Finalize();
+    }
+  }
+
+  void ReusableDicomUserConnection::SetMillisecondsBeforeClose(uint64_t ms)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    if (ms == 0)
+    {
+      ms = 1;
+    }
+
+    timeBeforeClose_ = boost::posix_time::milliseconds(ms);
+  }
+
+  void ReusableDicomUserConnection::Lock()
+  {
+    mutex_.lock();
+  }
+
+  void ReusableDicomUserConnection::Unlock()
+  {
+    if (connection_ != NULL &&
+        connection_->GetRemoteManufacturer() == ModalityManufacturer_StoreScp)
+    {
+      // "storescp" from DCMTK has problems when reusing a
+      // connection. Always close.
+      Close();
+    }
+
+    lastUse_ = Now();
+    mutex_.unlock();
+  }
+
+  
+  void ReusableDicomUserConnection::Finalize()
+  {
+    if (continue_)
+    {
+      continue_ = false;
+
+      if (closeThread_.joinable())
+      {
+        closeThread_.join();
+      }
+
+      Close();
+    }
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ReusableDicomUserConnection.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,89 @@
+/**
+ * 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 "DicomUserConnection.h"
+#include "../../Core/MultiThreading/Locker.h"
+
+#include <boost/thread.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
+
+namespace Orthanc
+{
+  class ReusableDicomUserConnection : public ILockable
+  {
+  private:
+    boost::mutex mutex_;
+    DicomUserConnection* connection_;
+    bool continue_;
+    boost::posix_time::time_duration timeBeforeClose_;
+    boost::posix_time::ptime lastUse_;
+    boost::thread closeThread_;
+
+    void Open(const std::string& localAet,
+              const RemoteModalityParameters& remote);
+    
+    void Close();
+
+    static void CloseThread(ReusableDicomUserConnection* that);
+
+  protected:
+    virtual void Lock();
+
+    virtual void Unlock();
+    
+  public:
+    class Locker : public ::Orthanc::Locker
+    {
+    private:
+      DicomUserConnection* connection_;
+
+    public:
+      Locker(ReusableDicomUserConnection& that,
+             const std::string& localAet,
+             const RemoteModalityParameters& remote);
+
+      DicomUserConnection& GetConnection();
+    };
+
+    ReusableDicomUserConnection();
+
+    virtual ~ReusableDicomUserConnection();
+
+    void SetMillisecondsBeforeClose(uint64_t ms);
+
+    void Finalize();
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ServerCommandInstance.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,99 @@
+/**
+ * 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 "ServerCommandInstance.h"
+
+#include "../../Core/OrthancException.h"
+
+namespace Orthanc
+{
+  bool ServerCommandInstance::Execute(IListener& listener)
+  {
+    ListOfStrings outputs;
+
+    bool success = false;
+
+    try
+    {
+      if (command_->Apply(outputs, inputs_))
+      {
+        success = true;
+      }
+    }
+    catch (OrthancException&)
+    {
+    }
+
+    if (!success)
+    {
+      listener.SignalFailure(jobId_);
+      return true;
+    }
+
+    for (std::list<ServerCommandInstance*>::iterator
+           it = next_.begin(); it != next_.end(); ++it)
+    {
+      for (ListOfStrings::const_iterator
+             output = outputs.begin(); output != outputs.end(); ++output)
+      {
+        (*it)->AddInput(*output);
+      }
+    }
+
+    listener.SignalSuccess(jobId_);
+    return true;
+  }
+
+
+  ServerCommandInstance::ServerCommandInstance(IServerCommand *command,
+                                               const std::string& jobId) : 
+    command_(command), 
+    jobId_(jobId),
+    connectedToSink_(false)
+  {
+    if (command_ == NULL)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  ServerCommandInstance::~ServerCommandInstance()
+  {
+    if (command_ != NULL)
+    {
+      delete command_;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ServerCommandInstance.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,105 @@
+/**
+ * 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/IDynamicObject.h"
+#include "IServerCommand.h"
+
+namespace Orthanc
+{
+  class ServerCommandInstance : public IDynamicObject
+  {
+    friend class ServerScheduler;
+
+  public:
+    class IListener
+    {
+    public:
+      virtual ~IListener()
+      {
+      }
+
+      virtual void SignalSuccess(const std::string& jobId) = 0;
+
+      virtual void SignalFailure(const std::string& jobId) = 0;
+    };
+
+  private:
+    typedef IServerCommand::ListOfStrings  ListOfStrings;
+
+    IServerCommand *command_;
+    std::string jobId_;
+    ListOfStrings inputs_;
+    std::list<ServerCommandInstance*> next_;
+    bool connectedToSink_;
+
+    bool Execute(IListener& listener);
+
+  public:
+    ServerCommandInstance(IServerCommand *command,
+                          const std::string& jobId);
+
+    virtual ~ServerCommandInstance();
+
+    const std::string& GetJobId() const
+    {
+      return jobId_;
+    }
+
+    void AddInput(const std::string& input)
+    {
+      inputs_.push_back(input);
+    }
+
+    void ConnectOutput(ServerCommandInstance& next)
+    {
+      next_.push_back(&next);
+    }
+
+    void SetConnectedToSink(bool connected = true)
+    {
+      connectedToSink_ = connected;
+    }
+
+    bool IsConnectedToSink() const
+    {
+      return connectedToSink_;
+    }
+
+    const std::list<ServerCommandInstance*>& GetNextCommands() const
+    {
+      return next_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ServerJob.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,147 @@
+/**
+ * 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 "ServerJob.h"
+
+#include "../../Core/OrthancException.h"
+#include "../../Core/Toolbox.h"
+
+namespace Orthanc
+{
+  void ServerJob::CheckOrdering()
+  {
+    std::map<ServerCommandInstance*, unsigned int> index;
+
+    unsigned int count = 0;
+    for (std::list<ServerCommandInstance*>::const_iterator
+           it = filters_.begin(); it != filters_.end(); ++it)
+    {
+      index[*it] = count++;
+    }
+
+    for (std::list<ServerCommandInstance*>::const_iterator
+           it = filters_.begin(); it != filters_.end(); ++it)
+    {
+      const std::list<ServerCommandInstance*>& nextCommands = (*it)->GetNextCommands();
+
+      for (std::list<ServerCommandInstance*>::const_iterator
+             next = nextCommands.begin(); next != nextCommands.end(); ++next)
+      {
+        if (index.find(*next) == index.end() ||
+            index[*next] <= index[*it])
+        {
+          // You must reorder your calls to "ServerJob::AddCommand"
+          throw OrthancException(ErrorCode_BadJobOrdering);
+        }
+      }
+    }
+  }
+
+
+  size_t ServerJob::Submit(SharedMessageQueue& target,
+                           ServerCommandInstance::IListener& listener)
+  {
+    if (submitted_)
+    {
+      // This job has already been submitted
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    CheckOrdering();
+
+    size_t size = filters_.size();
+
+    for (std::list<ServerCommandInstance*>::iterator 
+           it = filters_.begin(); it != filters_.end(); ++it)
+    {
+      target.Enqueue(*it);
+    }
+
+    filters_.clear();
+    submitted_ = true;
+
+    return size;
+  }
+
+
+  ServerJob::ServerJob() :
+    jobId_(Toolbox::GenerateUuid()),
+    submitted_(false),
+    description_("no description")
+  {
+  }
+
+
+  ServerJob::~ServerJob()
+  {
+    for (std::list<ServerCommandInstance*>::iterator
+           it = filters_.begin(); it != filters_.end(); ++it)
+    {
+      delete *it;
+    }
+
+    for (std::list<IDynamicObject*>::iterator
+           it = payloads_.begin(); it != payloads_.end(); ++it)
+    {
+      delete *it;
+    }
+  }
+
+
+  ServerCommandInstance& ServerJob::AddCommand(IServerCommand* filter)
+  {
+    if (submitted_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    filters_.push_back(new ServerCommandInstance(filter, jobId_));
+      
+    return *filters_.back();
+  }
+
+
+  IDynamicObject& ServerJob::AddPayload(IDynamicObject* payload)
+  {
+    if (submitted_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    payloads_.push_back(payload);
+      
+    return *filters_.back();
+  }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ServerJob.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,83 @@
+/**
+ * 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 "ServerCommandInstance.h"
+#include "../../Core/MultiThreading/SharedMessageQueue.h"
+
+namespace Orthanc
+{
+  class ServerJob
+  {
+    friend class ServerScheduler;
+
+  private:
+    std::list<ServerCommandInstance*> filters_;
+    std::list<IDynamicObject*> payloads_;
+    std::string jobId_;
+    bool submitted_;
+    std::string description_;
+
+    void CheckOrdering();
+
+    size_t Submit(SharedMessageQueue& target,
+                  ServerCommandInstance::IListener& listener);
+
+  public:
+    ServerJob();
+
+    ~ServerJob();
+
+    const std::string& GetId() const
+    {
+      return jobId_;
+    }
+
+    void SetDescription(const std::string& description)
+    {
+      description_ = description;
+    }
+
+    const std::string& GetDescription() const
+    {
+      return description_;
+    }
+
+    ServerCommandInstance& AddCommand(IServerCommand* filter);
+
+    // Take the ownership of a payload to a job. This payload will be
+    // automatically freed when the job succeeds or fails.
+    IDynamicObject& AddPayload(IDynamicObject* payload);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ServerScheduler.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,359 @@
+/**
+ * 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 "ServerScheduler.h"
+
+#include "../../Core/OrthancException.h"
+#include "../../Core/Logging.h"
+
+namespace Orthanc
+{
+  namespace
+  {
+    // Anonymous namespace to avoid clashes between compilation modules
+    class Sink : public IServerCommand
+    {
+    private:
+      ListOfStrings& target_;
+
+    public:
+      explicit Sink(ListOfStrings& target) : target_(target)
+      {
+      }
+
+      virtual bool Apply(ListOfStrings& outputs,
+                         const ListOfStrings& inputs)
+      {
+        for (ListOfStrings::const_iterator 
+               it = inputs.begin(); it != inputs.end(); ++it)
+        {
+          target_.push_back(*it);
+        }
+
+        return true;
+      }    
+    };
+  }
+
+
+  ServerScheduler::JobInfo& ServerScheduler::GetJobInfo(const std::string& jobId)
+  {
+    Jobs::iterator info = jobs_.find(jobId);
+
+    if (info == jobs_.end())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    return info->second;
+  }
+
+
+  void ServerScheduler::SignalSuccess(const std::string& jobId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    JobInfo& info = GetJobInfo(jobId);
+    info.success_++;
+
+    assert(info.failures_ == 0);
+
+    if (info.success_ >= info.size_)
+    {
+      if (info.watched_)
+      {
+        watchedJobStatus_[jobId] = JobStatus_Success;
+        watchedJobFinished_.notify_all();
+      }
+
+      LOG(INFO) << "Job successfully finished (" << info.description_ << ")";
+      jobs_.erase(jobId);
+
+      availableJob_.Release();
+    }
+  }
+
+
+  void ServerScheduler::SignalFailure(const std::string& jobId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    JobInfo& info = GetJobInfo(jobId);
+    info.failures_++;
+
+    if (info.success_ + info.failures_ >= info.size_)
+    {
+      if (info.watched_)
+      {
+        watchedJobStatus_[jobId] = JobStatus_Failure;
+        watchedJobFinished_.notify_all();
+      }
+
+      LOG(ERROR) << "Job has failed (" << info.description_ << ")";
+      jobs_.erase(jobId);
+
+      availableJob_.Release();
+    }
+  }
+
+
+  void ServerScheduler::Worker(ServerScheduler* that)
+  {
+    static const int32_t TIMEOUT = 100;
+
+    LOG(WARNING) << "The server scheduler has started";
+
+    while (!that->finish_)
+    {
+      std::auto_ptr<IDynamicObject> object(that->queue_.Dequeue(TIMEOUT));
+      if (object.get() != NULL)
+      {
+        ServerCommandInstance& filter = dynamic_cast<ServerCommandInstance&>(*object);
+
+        // Skip the execution of this filter if its parent job has
+        // previously failed.
+        bool jobHasFailed;
+        {
+          boost::mutex::scoped_lock lock(that->mutex_);
+          JobInfo& info = that->GetJobInfo(filter.GetJobId());
+          jobHasFailed = (info.failures_ > 0 || info.cancel_); 
+        }
+
+        if (jobHasFailed)
+        {
+          that->SignalFailure(filter.GetJobId());
+        }
+        else
+        {
+          filter.Execute(*that);
+        }
+      }
+    }
+  }
+
+
+  void ServerScheduler::SubmitInternal(ServerJob& job,
+                                       bool watched)
+  {
+    availableJob_.Acquire();
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    JobInfo info;
+    info.size_ = job.Submit(queue_, *this);
+    info.cancel_ = false;
+    info.success_ = 0;
+    info.failures_ = 0;
+    info.description_ = job.GetDescription();
+    info.watched_ = watched;
+
+    assert(info.size_ > 0);
+
+    if (watched)
+    {
+      watchedJobStatus_[job.GetId()] = JobStatus_Running;
+    }
+
+    jobs_[job.GetId()] = info;
+
+    LOG(INFO) << "New job submitted (" << job.description_ << ")";
+  }
+
+
+  ServerScheduler::ServerScheduler(unsigned int maxJobs) : availableJob_(maxJobs)
+  {
+    if (maxJobs == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    finish_ = false;
+    worker_ = boost::thread(Worker, this);
+  }
+
+
+  ServerScheduler::~ServerScheduler()
+  {
+    if (!finish_)
+    {
+      LOG(ERROR) << "INTERNAL ERROR: ServerScheduler::Finalize() should be invoked manually to avoid mess in the destruction order!";
+      Stop();
+    }
+  }
+
+
+  void ServerScheduler::Stop()
+  {
+    if (!finish_)
+    {
+      finish_ = true;
+
+      if (worker_.joinable())
+      {
+        worker_.join();
+      }
+    }
+  }
+
+
+  void ServerScheduler::Submit(ServerJob& job)
+  {
+    if (job.filters_.empty())
+    {
+      return;
+    }
+
+    SubmitInternal(job, false);
+  }
+
+
+  bool ServerScheduler::SubmitAndWait(ListOfStrings& outputs,
+                                      ServerJob& job)
+  {
+    std::string jobId = job.GetId();
+
+    outputs.clear();
+
+    if (job.filters_.empty())
+    {
+      return true;
+    }
+
+    // Add a sink filter to collect all the results of the filters
+    // that have no next filter.
+    ServerCommandInstance& sink = job.AddCommand(new Sink(outputs));
+
+    for (std::list<ServerCommandInstance*>::iterator
+           it = job.filters_.begin(); it != job.filters_.end(); ++it)
+    {
+      if ((*it) != &sink &&
+          (*it)->IsConnectedToSink())
+      {
+        (*it)->ConnectOutput(sink);
+      }
+    }
+
+    // Submit the job
+    SubmitInternal(job, true);
+
+    // Wait for the job to complete (either success or failure)
+    JobStatus status;
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      assert(watchedJobStatus_.find(jobId) != watchedJobStatus_.end());
+        
+      while (watchedJobStatus_[jobId] == JobStatus_Running)
+      {
+        watchedJobFinished_.wait(lock);
+      }
+
+      status = watchedJobStatus_[jobId];
+      watchedJobStatus_.erase(jobId);
+    }
+
+    return (status == JobStatus_Success);
+  }
+
+
+  bool ServerScheduler::SubmitAndWait(ServerJob& job)
+  {
+    ListOfStrings ignoredSink;
+    return SubmitAndWait(ignoredSink, job);
+  }
+
+
+  bool ServerScheduler::IsRunning(const std::string& jobId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    return jobs_.find(jobId) != jobs_.end();
+  }
+
+
+  void ServerScheduler::Cancel(const std::string& jobId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Jobs::iterator job = jobs_.find(jobId);
+
+    if (job != jobs_.end())
+    {
+      job->second.cancel_ = true;
+      LOG(WARNING) << "Canceling a job (" << job->second.description_ << ")";
+    }
+  }
+
+
+  float ServerScheduler::GetProgress(const std::string& jobId) 
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Jobs::iterator job = jobs_.find(jobId);
+
+    if (job == jobs_.end() || 
+        job->second.size_ == 0  /* should never happen */)
+    {
+      // This job is not running
+      return 1;
+    }
+
+    if (job->second.failures_ != 0)
+    {
+      return 1;
+    }
+
+    if (job->second.size_ == 1)
+    {
+      return static_cast<float>(job->second.success_);
+    }
+
+    return (static_cast<float>(job->second.success_) / 
+            static_cast<float>(job->second.size_ - 1));
+  }
+
+
+  void ServerScheduler::GetListOfJobs(ListOfStrings& jobs)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    jobs.clear();
+
+    for (Jobs::const_iterator 
+           it = jobs_.begin(); it != jobs_.end(); ++it)
+    {
+      jobs.push_back(it->first);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/ServerScheduler.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,123 @@
+/**
+ * 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 "ServerJob.h"
+
+#include "../../Core/MultiThreading/Semaphore.h"
+
+namespace Orthanc
+{
+  class ServerScheduler : public ServerCommandInstance::IListener
+  {
+  private:
+    struct JobInfo
+    {
+      bool watched_;
+      bool cancel_;
+      size_t size_;
+      size_t success_;
+      size_t failures_;
+      std::string description_;
+    };
+
+    enum JobStatus
+    {
+      JobStatus_Running = 1,
+      JobStatus_Success = 2,
+      JobStatus_Failure = 3
+    };
+
+    typedef IServerCommand::ListOfStrings  ListOfStrings;
+    typedef std::map<std::string, JobInfo> Jobs;
+
+    boost::mutex mutex_;
+    boost::condition_variable watchedJobFinished_;
+    Jobs jobs_;
+    SharedMessageQueue queue_;
+    bool finish_;
+    boost::thread worker_;
+    std::map<std::string, JobStatus> watchedJobStatus_;
+    Semaphore availableJob_;
+
+    JobInfo& GetJobInfo(const std::string& jobId);
+
+    virtual void SignalSuccess(const std::string& jobId);
+
+    virtual void SignalFailure(const std::string& jobId);
+
+    static void Worker(ServerScheduler* that);
+
+    void SubmitInternal(ServerJob& job,
+                        bool watched);
+
+  public:
+    explicit ServerScheduler(unsigned int maxjobs);
+
+    ~ServerScheduler();
+
+    void Stop();
+
+    void Submit(ServerJob& job);
+
+    bool SubmitAndWait(ListOfStrings& outputs,
+                       ServerJob& job);
+
+    bool SubmitAndWait(ServerJob& job);
+
+    bool IsRunning(const std::string& jobId);
+
+    void Cancel(const std::string& jobId);
+
+    // Returns a number between 0 and 1
+    float GetProgress(const std::string& jobId);
+
+    bool IsRunning(const ServerJob& job)
+    {
+      return IsRunning(job.GetId());
+    }
+
+    void Cancel(const ServerJob& job) 
+    {
+      Cancel(job.GetId());
+    }
+
+    float GetProgress(const ServerJob& job) 
+    {
+      return GetProgress(job.GetId());
+    }
+
+    void GetListOfJobs(ListOfStrings& jobs);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/StorePeerCommand.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,92 @@
+/**
+ * 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 "StorePeerCommand.h"
+
+#include "../../Core/Logging.h"
+#include "../../Core/HttpClient.h"
+
+namespace Orthanc
+{
+  StorePeerCommand::StorePeerCommand(ServerContext& context,
+                                     const WebServiceParameters& peer,
+                                     bool ignoreExceptions) : 
+    context_(context),
+    peer_(peer),
+    ignoreExceptions_(ignoreExceptions)
+  {
+  }
+
+  bool StorePeerCommand::Apply(ListOfStrings& outputs,
+                               const ListOfStrings& inputs)
+  {
+    // Configure the HTTP client
+    HttpClient client(peer_, "instances");
+    client.SetMethod(HttpMethod_Post);
+
+    for (ListOfStrings::const_iterator
+           it = inputs.begin(); it != inputs.end(); ++it)
+    {
+      LOG(INFO) << "Sending resource " << *it << " to peer \"" 
+                << peer_.GetUrl() << "\"";
+
+      try
+      {
+        context_.ReadDicom(client.GetBody(), *it);
+
+        std::string answer;
+        if (!client.Apply(answer))
+        {
+          LOG(ERROR) << "Unable to send resource " << *it << " to peer \"" << peer_.GetUrl() << "\"";
+          throw OrthancException(ErrorCode_NetworkProtocol);
+        }
+
+        // Only chain with other commands if this command succeeds
+        outputs.push_back(*it);
+      }
+      catch (OrthancException& e)
+      {
+        LOG(ERROR) << "Unable to forward to an Orthanc peer in (instance "
+                   << *it << ", peer " << peer_.GetUrl() << "): " << e.What();
+
+        if (!ignoreExceptions_)
+        {
+          throw;
+        }
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/StorePeerCommand.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,57 @@
+/**
+ * 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 "IServerCommand.h"
+#include "../ServerContext.h"
+#include "../OrthancInitialization.h"
+
+namespace Orthanc
+{
+  class StorePeerCommand : public IServerCommand
+  {
+  private:
+    ServerContext& context_;
+    WebServiceParameters peer_;
+    bool ignoreExceptions_;
+
+  public:
+    StorePeerCommand(ServerContext& context,
+                     const WebServiceParameters& peer,
+                     bool ignoreExceptions);
+    
+    virtual bool Apply(ListOfStrings& outputs,
+                       const ListOfStrings& inputs);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/StoreScuCommand.cpp	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,99 @@
+/**
+ * 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 "StoreScuCommand.h"
+
+#include "../../Core/Logging.h"
+
+namespace Orthanc
+{
+  StoreScuCommand::StoreScuCommand(ServerContext& context,
+                                   const std::string& localAet,
+                                   const RemoteModalityParameters& modality,
+                                   bool ignoreExceptions) : 
+    context_(context),
+    modality_(modality),
+    ignoreExceptions_(ignoreExceptions),
+    localAet_(localAet),
+    moveOriginatorID_(0)
+  {
+  }
+
+
+  void StoreScuCommand::SetMoveOriginator(const std::string& aet,
+                                          uint16_t id)
+  {
+    moveOriginatorAET_ = aet;
+    moveOriginatorID_ = id;
+  }
+
+
+  bool StoreScuCommand::Apply(ListOfStrings& outputs,
+                             const ListOfStrings& inputs)
+  {
+    ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), localAet_, modality_);
+
+    for (ListOfStrings::const_iterator
+           it = inputs.begin(); it != inputs.end(); ++it)
+    {
+      LOG(INFO) << "Sending resource " << *it << " to modality \"" 
+                << modality_.GetApplicationEntityTitle() << "\"";
+
+      try
+      {
+        std::string dicom;
+        context_.ReadDicom(dicom, *it);
+
+        locker.GetConnection().Store(dicom, moveOriginatorAET_, moveOriginatorID_);
+
+        // Only chain with other commands if this command succeeds
+        outputs.push_back(*it);
+      }
+      catch (OrthancException& e)
+      {
+        // Ignore transmission errors (e.g. if the remote modality is
+        // powered off)
+        LOG(ERROR) << "Unable to forward to a modality in (instance "
+                   << *it << "): " << e.What();
+
+        if (!ignoreExceptions_)
+        {
+          throw;
+        }
+      }
+    }
+
+    return true;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/OldScheduler/StoreScuCommand.h	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,63 @@
+/**
+ * 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 "IServerCommand.h"
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class StoreScuCommand : public IServerCommand
+  {
+  private:
+    ServerContext& context_;
+    RemoteModalityParameters modality_;
+    bool ignoreExceptions_;
+    std::string localAet_;
+    std::string moveOriginatorAET_;
+    uint16_t moveOriginatorID_;
+
+  public:
+    StoreScuCommand(ServerContext& context,
+                    const std::string& localAet,
+                    const RemoteModalityParameters& modality,
+                    bool ignoreExceptions);
+
+    void SetMoveOriginator(const std::string& aet,
+                           uint16_t id);
+
+    virtual bool Apply(ListOfStrings& outputs,
+                       const ListOfStrings& inputs);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/ImplementationNotes/JobsEngineStates.dot	Wed May 23 14:28:25 2018 +0200
@@ -0,0 +1,28 @@
+// dot -Tpdf JobsEngineStates.dot -o JobsEngineStates.pdf
+
+digraph G
+{
+  rankdir="LR";
+  init [shape=point];
+  failure, success [shape=doublecircle];
+
+  // Internal transitions
+  init -> pending;
+  pending -> running;
+  running -> success;
+  running -> failure;
+  running -> retry;
+  retry -> pending [label="timeout"];
+
+  // External actions
+  failure -> pending  [label="Resubmit()" fontcolor="red"];
+  paused -> pending  [label="Resume()" fontcolor="red"];
+  pending -> paused  [label="Pause()" fontcolor="red"];
+  retry -> paused  [label="Pause()" fontcolor="red"];
+  running -> paused  [label="Pause()" fontcolor="red"];
+
+  paused -> failure  [label="Cancel()" fontcolor="red"];
+  pending -> failure  [label="Cancel()" fontcolor="red"];
+  retry -> failure  [label="Cancel()" fontcolor="red"];
+  running -> failure  [label="Cancel()" fontcolor="red"];
+}
Binary file Resources/ImplementationNotes/JobsEngineStates.pdf has changed
--- a/UnitTestsSources/MultiThreadingTests.cpp	Wed May 23 10:08:04 2018 +0200
+++ b/UnitTestsSources/MultiThreadingTests.cpp	Wed May 23 14:28:25 2018 +0200
@@ -34,13 +34,11 @@
 #include "PrecompiledHeadersUnitTests.h"
 #include "gtest/gtest.h"
 
-#include "../OrthancServer/Scheduler/ServerScheduler.h"
+#include "../Core/JobsEngine/JobsEngine.h"
+#include "../Core/MultiThreading/SharedMessageQueue.h"
 #include "../Core/OrthancException.h"
 #include "../Core/SystemToolbox.h"
 #include "../Core/Toolbox.h"
-#include "../Core/MultiThreading/Locker.h"
-#include "../Core/MultiThreading/Mutex.h"
-#include "../Core/MultiThreading/ReaderWriterLock.h"
 
 using namespace Orthanc;
 
@@ -106,156 +104,576 @@
 }
 
 
-TEST(MultiThreading, Mutex)
+
+
+class DummyJob : public Orthanc::IJob
 {
-  Mutex mutex;
-  Locker locker(mutex);
+private:
+  bool         fails_;
+  unsigned int count_;
+  unsigned int steps_;
+
+public:
+  DummyJob() :
+    fails_(false),
+    count_(0),
+    steps_(4)
+  {
+  }
+
+  explicit DummyJob(bool fails) :
+  fails_(fails),
+    count_(0),
+    steps_(4)
+  {
+  }
+
+  virtual void Start()
+  {
+  }
+
+  virtual void SignalResubmit()
+  {
+  }
+    
+  virtual JobStepResult ExecuteStep()
+  {
+    if (fails_)
+    {
+      return JobStepResult::Failure(ErrorCode_ParameterOutOfRange);
+    }
+    else if (count_ == steps_ - 1)
+    {
+      return JobStepResult::Success();
+    }
+    else
+    {
+      count_++;
+      return JobStepResult::Continue();
+    }
+  }
+
+  virtual void ReleaseResources()
+  {
+  }
+
+  virtual float GetProgress()
+  {
+    return static_cast<float>(count_) / static_cast<float>(steps_ - 1);
+  }
+
+  virtual void GetJobType(std::string& type)
+  {
+    type = "DummyJob";
+  }
+
+  virtual void GetInternalContent(Json::Value& value)
+  {
+  }
+
+  virtual void GetPublicContent(Json::Value& value)
+  {
+    value["hello"] = "world";
+  }
+};
+
+
+static bool CheckState(Orthanc::JobsRegistry& registry,
+                       const std::string& id,
+                       Orthanc::JobState state)
+{
+  Orthanc::JobState s;
+  if (registry.GetState(s, id))
+  {
+    return state == s;
+  }
+  else
+  {
+    return false;
+  }
 }
 
 
-TEST(MultiThreading, ReaderWriterLock)
+static bool CheckErrorCode(Orthanc::JobsRegistry& registry,
+                           const std::string& id,
+                           Orthanc::ErrorCode code)
 {
-  ReaderWriterLock lock;
-
+  Orthanc::JobInfo s;
+  if (registry.GetJobInfo(s, id))
   {
-    Locker locker1(lock.ForReader());
-    Locker locker2(lock.ForReader());
+    return code == s.GetStatus().GetErrorCode();
   }
-
+  else
   {
-    Locker locker3(lock.ForWriter());
+    return false;
   }
 }
 
 
+TEST(JobsRegistry, Priority)
+{
+  JobsRegistry registry;
 
-#include "../Core/DicomNetworking/ReusableDicomUserConnection.h"
+  std::string i1, i2, i3, i4;
+  registry.Submit(i1, new DummyJob(), 10);
+  registry.Submit(i2, new DummyJob(), 30);
+  registry.Submit(i3, new DummyJob(), 20);
+  registry.Submit(i4, new DummyJob(), 5);  
+
+  registry.SetMaxCompletedJobs(2);
+
+  std::set<std::string> id;
+  registry.ListJobs(id);
+
+  ASSERT_EQ(4u, id.size());
+  ASSERT_TRUE(id.find(i1) != id.end());
+  ASSERT_TRUE(id.find(i2) != id.end());
+  ASSERT_TRUE(id.find(i3) != id.end());
+  ASSERT_TRUE(id.find(i4) != id.end());
+
+  ASSERT_TRUE(CheckState(registry, i2, Orthanc::JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(30, job.GetPriority());
+    ASSERT_EQ(i2, job.GetId());
+
+    ASSERT_TRUE(CheckState(registry, i2, Orthanc::JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, i2, Orthanc::JobState_Failure));
+  ASSERT_TRUE(CheckState(registry, i3, Orthanc::JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(20, job.GetPriority());
+    ASSERT_EQ(i3, job.GetId());
+
+    job.MarkSuccess();
+
+    ASSERT_TRUE(CheckState(registry, i3, Orthanc::JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, i3, Orthanc::JobState_Success));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(10, job.GetPriority());
+    ASSERT_EQ(i1, job.GetId());
+  }
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(5, job.GetPriority());
+    ASSERT_EQ(i4, job.GetId());
+  }
+
+  {
+    JobsRegistry::RunningJob job(registry, 1);
+    ASSERT_FALSE(job.IsValid());
+  }
+
+  Orthanc::JobState s;
+  ASSERT_TRUE(registry.GetState(s, i1));
+  ASSERT_FALSE(registry.GetState(s, i2));  // Removed because oldest
+  ASSERT_FALSE(registry.GetState(s, i3));  // Removed because second oldest
+  ASSERT_TRUE(registry.GetState(s, i4));
+
+  registry.SetMaxCompletedJobs(1);  // (*)
+  ASSERT_FALSE(registry.GetState(s, i1));  // Just discarded by (*)
+  ASSERT_TRUE(registry.GetState(s, i4));
+}
+
+
+TEST(JobsRegistry, Simultaneous)
+{
+  JobsRegistry registry;
+
+  std::string i1, i2;
+  registry.Submit(i1, new DummyJob(), 20);
+  registry.Submit(i2, new DummyJob(), 10);
 
-TEST(ReusableDicomUserConnection, DISABLED_Basic)
+  ASSERT_TRUE(CheckState(registry, i1, Orthanc::JobState_Pending));
+  ASSERT_TRUE(CheckState(registry, i2, Orthanc::JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job1(registry, 0);
+    JobsRegistry::RunningJob job2(registry, 0);
+
+    ASSERT_TRUE(job1.IsValid());
+    ASSERT_TRUE(job2.IsValid());
+
+    job1.MarkFailure();
+    job2.MarkSuccess();
+
+    ASSERT_TRUE(CheckState(registry, i1, Orthanc::JobState_Running));
+    ASSERT_TRUE(CheckState(registry, i2, Orthanc::JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, i1, Orthanc::JobState_Failure));
+  ASSERT_TRUE(CheckState(registry, i2, Orthanc::JobState_Success));
+}
+
+
+TEST(JobsRegistry, Resubmit)
 {
-  ReusableDicomUserConnection c;
-  c.SetMillisecondsBeforeClose(200);
-  printf("START\n"); fflush(stdout);
+  JobsRegistry registry;
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Pending));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    job.MarkFailure();
+
+    ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Running));
+
+    registry.Resubmit(id);
+    ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Failure));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(id, job.GetId());
+
+    job.MarkSuccess();
+    ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Success));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Success));
+}
+
+
+TEST(JobsRegistry, Retry)
+{
+  JobsRegistry registry;
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    job.MarkRetry(0);
+
+    ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Retry));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Retry));
+  
+  registry.ScheduleRetries();
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Pending));
 
   {
-    RemoteModalityParameters remote("STORESCP", "localhost", 2000, ModalityManufacturer_Generic);
-    ReusableDicomUserConnection::Locker lock(c, "ORTHANC", remote);
-    lock.GetConnection().StoreFile("/home/jodogne/DICOM/Cardiac/MR.X.1.2.276.0.7230010.3.1.4.2831157719.2256.1336386844.676281");
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    job.MarkSuccess();
+
+    ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Success));
+}
+
+
+TEST(JobsRegistry, PausePending)
+{
+  JobsRegistry registry;
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Pending));
+
+  registry.Pause(id);
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Paused));
+
+  registry.Pause(id);
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Paused));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Paused));
+
+  registry.Resume(id);
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Pending));
+}
+
+
+TEST(JobsRegistry, PauseRunning)
+{
+  JobsRegistry registry;
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+
+    registry.Resubmit(id);
+    job.MarkPause();
+    ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Paused));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Paused));
+
+  registry.Resume(id);
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+
+    job.MarkSuccess();
+    ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Success));
+}
+
+
+TEST(JobsRegistry, PauseRetry)
+{
+  JobsRegistry registry;
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+
+    job.MarkRetry(0);
+    ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Running));
   }
 
-  printf("**\n"); fflush(stdout);
-  SystemToolbox::USleep(1000000);
-  printf("**\n"); fflush(stdout);
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Retry));
+
+  registry.Pause(id);
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Paused));
+
+  registry.Resume(id);
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Pending));
 
   {
-    RemoteModalityParameters remote("STORESCP", "localhost", 2000, ModalityManufacturer_Generic);
-    ReusableDicomUserConnection::Locker lock(c, "ORTHANC", remote);
-    lock.GetConnection().StoreFile("/home/jodogne/DICOM/Cardiac/MR.X.1.2.276.0.7230010.3.1.4.2831157719.2256.1336386844.676277");
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+
+    job.MarkSuccess();
+    ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Success));
+}
+
+
+TEST(JobsRegistry, Cancel)
+{
+  JobsRegistry registry;
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_FALSE(registry.Cancel("nope"));
+
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Pending));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+            
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+  
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+  
+  ASSERT_TRUE(registry.Resubmit(id));
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Pending));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+  
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+
+    ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+
+    job.MarkSuccess();
+    ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Running));
   }
 
-  SystemToolbox::ServerBarrier();
-  printf("DONE\n"); fflush(stdout);
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Success));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Success));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+
+  registry.Submit(id, new DummyJob(), 10);
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(id, job.GetId());
+
+    ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+    ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Running));
+
+    job.MarkCanceled();
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+
+  ASSERT_TRUE(registry.Resubmit(id));
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Pending));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+
+  ASSERT_TRUE(registry.Pause(id));
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Paused));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+
+  ASSERT_TRUE(registry.Resubmit(id));
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Pending));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(id, job.GetId());
+
+    ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+    ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Running));
+
+    job.MarkRetry(500);
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Retry));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, Orthanc::JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
 }
 
 
 
-class Tutu : public IServerCommand
+TEST(JobsEngine, SubmitAndWait)
 {
-private:
-  int factor_;
-
-public:
-  Tutu(int f) : factor_(f)
-  {
-  }
-
-  virtual bool Apply(ListOfStrings& outputs,
-                     const ListOfStrings& inputs)
-  {
-    for (ListOfStrings::const_iterator 
-           it = inputs.begin(); it != inputs.end(); ++it)
-    {
-      int a = boost::lexical_cast<int>(*it);
-      int b = factor_ * a;
-
-      printf("%d * %d = %d\n", a, factor_, b);
-
-      //if (a == 84) { printf("BREAK\n"); return false; }
+  JobsEngine engine;
+  engine.SetWorkersCount(3);
+  engine.Start();
 
-      outputs.push_back(boost::lexical_cast<std::string>(b));
-    }
-
-    SystemToolbox::USleep(30000);
-
-    return true;
-  }
-};
-
+  ASSERT_TRUE(engine.GetRegistry().SubmitAndWait(new DummyJob(), rand() % 10));
+  ASSERT_FALSE(engine.GetRegistry().SubmitAndWait(new DummyJob(true), rand() % 10));
 
-static void Tata(ServerScheduler* s, ServerJob* j, bool* done)
-{
-  typedef IServerCommand::ListOfStrings  ListOfStrings;
-
-  while (!(*done))
-  {
-    ListOfStrings l;
-    s->GetListOfJobs(l);
-    for (ListOfStrings::iterator it = l.begin(); it != l.end(); ++it)
-    {
-      printf(">> %s: %0.1f\n", it->c_str(), 100.0f * s->GetProgress(*it));
-    }
-    SystemToolbox::USleep(3000);
-  }
+  engine.Stop();
 }
 
 
-TEST(MultiThreading, ServerScheduler)
-{
-  ServerScheduler scheduler(10);
+
+
 
-  ServerJob job;
-  ServerCommandInstance& f2 = job.AddCommand(new Tutu(2));
-  ServerCommandInstance& f3 = job.AddCommand(new Tutu(3));
-  ServerCommandInstance& f4 = job.AddCommand(new Tutu(4));
-  ServerCommandInstance& f5 = job.AddCommand(new Tutu(5));
-  f2.AddInput(boost::lexical_cast<std::string>(42));
-  //f3.AddInput(boost::lexical_cast<std::string>(42));
-  //f4.AddInput(boost::lexical_cast<std::string>(42));
-  f2.ConnectOutput(f3);
-  f3.ConnectOutput(f4);
-  f4.ConnectOutput(f5);
-
-  f3.SetConnectedToSink(true);
-  f5.SetConnectedToSink(true);
-
-  job.SetDescription("tutu");
-
-  bool done = false;
-  boost::thread t(Tata, &scheduler, &job, &done);
+#include "../OrthancServer/ServerJobs/LuaJobManager.h"
+#include "../Core/JobsEngine/Operations/StringOperationValue.h"
+#include "../Core/JobsEngine/Operations/LogJobOperation.h"
 
 
-  //scheduler.Submit(job);
+TEST(JobsEngine, DISABLED_SequenceOfOperationsJob)
+{
+  JobsEngine engine;
+  engine.SetWorkersCount(3);
+  engine.Start();
 
-  IServerCommand::ListOfStrings l;
-  scheduler.SubmitAndWait(l, job);
+  std::string id;
+  SequenceOfOperationsJob* job = NULL;
 
-  ASSERT_EQ(2u, l.size());
-  ASSERT_EQ(42 * 2 * 3, boost::lexical_cast<int>(l.front()));
-  ASSERT_EQ(42 * 2 * 3 * 4 * 5, boost::lexical_cast<int>(l.back()));
+  {
+    std::auto_ptr<SequenceOfOperationsJob> a(new SequenceOfOperationsJob);
+    job = a.get();
+    engine.GetRegistry().Submit(id, a.release(), 0);
+  }
+
+  boost::this_thread::sleep(boost::posix_time::milliseconds(500));
 
-  for (IServerCommand::ListOfStrings::iterator i = l.begin(); i != l.end(); i++)
   {
-    printf("** %s\n", i->c_str());
+    SequenceOfOperationsJob::Lock lock(*job);
+    size_t i = lock.AddOperation(new LogJobOperation);
+    size_t j = lock.AddOperation(new LogJobOperation);
+    size_t k = lock.AddOperation(new LogJobOperation);
+    lock.AddInput(i, StringOperationValue("Hello"));
+    lock.AddInput(i, StringOperationValue("World"));
+    lock.Connect(i, j);
+    lock.Connect(j, k);
   }
 
-  //SystemToolbox::ServerBarrier();
-  //SystemToolbox::USleep(3000000);
+  boost::this_thread::sleep(boost::posix_time::milliseconds(2000));
+
+  engine.Stop();
+
+}
+
 
-  scheduler.Stop();
+TEST(JobsEngine, DISABLED_Lua)
+{
+  JobsEngine engine;
+  engine.SetWorkersCount(2);
+  engine.Start();
+
+  LuaJobManager lua;
+  lua.SetMaxOperationsPerJob(5);
+  lua.SetTrailingOperationTimeout(200);
 
-  done = true;
-  if (t.joinable())
+  for (size_t i = 0; i < 30; i++)
   {
-    t.join();
+    boost::this_thread::sleep(boost::posix_time::milliseconds(150));
+
+    LuaJobManager::Lock lock(lua, engine);
+    size_t a = lock.AddLogOperation();
+    size_t b = lock.AddLogOperation();
+    size_t c = lock.AddSystemCallOperation("echo");
+    lock.AddStringInput(a, boost::lexical_cast<std::string>(i));
+    lock.AddNullInput(a);
+    lock.Connect(a, b);
+    lock.Connect(a, c);
   }
+
+  boost::this_thread::sleep(boost::posix_time::milliseconds(2000));
+
+  engine.Stop();
+
 }