changeset 2680:2c4fe62550e9 jobs

integration mainline->jobs
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 20 Jun 2018 18:03:20 +0200
parents e0476e8cb73d (diff) ee9a6cd63891 (current diff)
children 1bc4054ad96c
files NEWS
diffstat 179 files changed, 15868 insertions(+), 4801 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Wed Jun 20 18:02:36 2018 +0200
+++ b/CMakeLists.txt	Wed Jun 20 18:03:20 2018 +0200
@@ -55,6 +55,7 @@
 set(ORTHANC_SERVER_SOURCES
   OrthancServer/DatabaseWrapper.cpp
   OrthancServer/DatabaseWrapperBase.cpp
+  OrthancServer/DicomInstanceOrigin.cpp
   OrthancServer/DicomInstanceToStore.cpp
   OrthancServer/ExportedResource.cpp
   OrthancServer/LuaScripting.cpp
@@ -70,14 +71,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 +83,17 @@
   OrthancServer/ServerContext.cpp
   OrthancServer/ServerEnumerations.cpp
   OrthancServer/ServerIndex.cpp
+  OrthancServer/ServerJobs/ArchiveJob.cpp
+  OrthancServer/ServerJobs/DicomModalityStoreJob.cpp
+  OrthancServer/ServerJobs/LuaJobManager.cpp
+  OrthancServer/ServerJobs/Operations/DeleteResourceOperation.cpp
+  OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp
+  OrthancServer/ServerJobs/Operations/StorePeerOperation.cpp
+  OrthancServer/ServerJobs/Operations/StoreScuOperation.cpp
+  OrthancServer/ServerJobs/Operations/SystemCallOperation.cpp
+  OrthancServer/ServerJobs/OrthancJobUnserializer.cpp
+  OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp
+  OrthancServer/ServerJobs/ResourceModificationJob.cpp
   OrthancServer/ServerToolbox.cpp
   OrthancServer/SliceOrdering.cpp
   )
--- a/Core/DicomFormat/DicomTag.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/Core/DicomFormat/DicomTag.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -39,9 +39,32 @@
 #include <iostream>
 #include <iomanip>
 #include <stdio.h>
+#include <string.h>
 
 namespace Orthanc
-{
+{ 
+  static inline uint16_t GetCharValue(char c)
+  {
+    if (c >= '0' && c <= '9')
+      return c - '0';
+    else if (c >= 'a' && c <= 'f')
+      return c - 'a' + 10;
+    else if (c >= 'A' && c <= 'F')
+      return c - 'A' + 10;
+    else
+      return 0;
+  }
+
+
+  static inline uint16_t GetTagValue(const char* c)
+  {
+    return ((GetCharValue(c[0]) << 12) + 
+            (GetCharValue(c[1]) << 8) + 
+            (GetCharValue(c[2]) << 4) + 
+            GetCharValue(c[3]));
+  }
+
+
   bool DicomTag::operator< (const DicomTag& other) const
   {
     if (group_ < other.group_)
@@ -74,6 +97,49 @@
   }
 
 
+  bool DicomTag::ParseHexadecimal(DicomTag& tag,
+                                  const char* value)
+  {
+    size_t length = strlen(value);
+
+    if (length == 9 &&
+        isxdigit(value[0]) &&
+        isxdigit(value[1]) &&
+        isxdigit(value[2]) &&
+        isxdigit(value[3]) &&
+        (value[4] == '-' || value[4] == ',') &&
+        isxdigit(value[5]) &&
+        isxdigit(value[6]) &&
+        isxdigit(value[7]) &&
+        isxdigit(value[8]))        
+    {
+      uint16_t group = GetTagValue(value);
+      uint16_t element = GetTagValue(value + 5);
+      tag = DicomTag(group, element);
+      return true;
+    }
+    else if (length == 8 &&
+             isxdigit(value[0]) &&
+             isxdigit(value[1]) &&
+             isxdigit(value[2]) &&
+             isxdigit(value[3]) &&
+             isxdigit(value[4]) &&
+             isxdigit(value[5]) &&
+             isxdigit(value[6]) &&
+             isxdigit(value[7])) 
+    {
+      uint16_t group = GetTagValue(value);
+      uint16_t element = GetTagValue(value + 4);
+      tag = DicomTag(group, element);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
   const char* DicomTag::GetMainTagsName() const
   {
     if (*this == DICOM_TAG_ACCESSION_NUMBER)
--- a/Core/DicomFormat/DicomTag.h	Wed Jun 20 18:02:36 2018 +0200
+++ b/Core/DicomFormat/DicomTag.h	Wed Jun 20 18:03:20 2018 +0200
@@ -88,6 +88,9 @@
 
     std::string Format() const;
 
+    static bool ParseHexadecimal(DicomTag& tag,
+                                 const char* value);
+
     friend std::ostream& operator<< (std::ostream& o, const DicomTag& tag);
 
     static void AddTagsForModule(std::set<DicomTag>& target,
--- a/Core/DicomNetworking/DicomUserConnection.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/Core/DicomNetworking/DicomUserConnection.cpp	Wed Jun 20 18:03:20 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 Jun 20 18:02:36 2018 +0200
+++ b/Core/DicomNetworking/DicomUserConnection.h	Wed Jun 20 18:03:20 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 Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,82 @@
+/**
+ * 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_ENABLE_DCMTK_NETWORKING)
+#  error The macro ORTHANC_ENABLE_DCMTK_NETWORKING must be defined
+#endif
+
+#if ORTHANC_ENABLE_DCMTK_NETWORKING == 0
+
+namespace Orthanc
+{
+  // DICOM networking is disabled, this is just a void class
+  class IDicomConnectionManager : public boost::noncopyable
+  {
+  public:
+    virtual ~IDicomConnectionManager()
+    {
+    }
+  };
+}
+
+#else
+
+#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;
+  };
+}
+
+#endif
--- a/Core/DicomNetworking/RemoteModalityParameters.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/Core/DicomNetworking/RemoteModalityParameters.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -36,6 +36,7 @@
 
 #include "../Logging.h"
 #include "../OrthancException.h"
+#include "../SerializationToolbox.h"
 
 #include <boost/lexical_cast.hpp>
 #include <stdexcept>
@@ -125,4 +126,26 @@
     value.append(GetPort());
     value.append(EnumerationToString(GetManufacturer()));
   }
+
+
+  
+  void RemoteModalityParameters::Serialize(Json::Value& target) const
+  {
+    target = Json::objectValue;
+    target["AET"] = aet_;
+    target["Host"] = host_;
+    target["Port"] = port_;
+    target["Manufacturer"] = EnumerationToString(manufacturer_);
+  }
+
+  
+  RemoteModalityParameters::RemoteModalityParameters(const Json::Value& serialized)
+  {
+    aet_ = SerializationToolbox::ReadString(serialized, "AET");
+    host_ = SerializationToolbox::ReadString(serialized, "Host");
+    port_ = static_cast<uint16_t>
+      (SerializationToolbox::ReadUnsignedInteger(serialized, "Port"));
+    manufacturer_ = StringToModalityManufacturer
+      (SerializationToolbox::ReadString(serialized, "Manufacturer"));
+  }
 }
--- a/Core/DicomNetworking/RemoteModalityParameters.h	Wed Jun 20 18:02:36 2018 +0200
+++ b/Core/DicomNetworking/RemoteModalityParameters.h	Wed Jun 20 18:03:20 2018 +0200
@@ -52,6 +52,8 @@
   public:
     RemoteModalityParameters();
 
+    RemoteModalityParameters(const Json::Value& serialized);
+
     RemoteModalityParameters(const std::string& aet,
                              const std::string& host,
                              uint16_t port,
@@ -105,5 +107,7 @@
     void FromJson(const Json::Value& modality);
 
     void ToJson(Json::Value& value) const;
+
+    void Serialize(Json::Value& target) const;
   };
 }
--- a/Core/DicomNetworking/ReusableDicomUserConnection.cpp	Wed Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:03:20 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 Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,102 @@
+/**
+ * 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"
+
+#if ORTHANC_ENABLE_DCMTK_NETWORKING == 0
+
+namespace Orthanc
+{
+  class TimeoutDicomConnectionManager : public IDicomConnectionManager
+  {
+  public:
+    void SetTimeout(unsigned int timeout)
+    {
+    }
+
+    unsigned int GetTimeout()
+    {
+      return 0;
+    }
+
+    void Close()
+    {
+    }
+
+    void CheckTimeout()
+    {
+    }
+  };
+}
+
+#else
+
+#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);
+  };
+}
+
+#endif
--- a/Core/DicomParsing/DicomModification.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/Core/DicomParsing/DicomModification.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -36,6 +36,7 @@
 
 #include "../Logging.h"
 #include "../OrthancException.h"
+#include "../SerializationToolbox.h"
 #include "FromDcmtkBridge.h"
 #include "ITagVisitor.h"
 
@@ -1237,4 +1238,166 @@
     patientNameReplaced = (IsReplaced(DICOM_TAG_PATIENT_NAME) &&
                            GetReplacement(DICOM_TAG_PATIENT_NAME) == patientName);
   }
+
+
+
+
+  static const char* REMOVE_PRIVATE_TAGS = "RemovePrivateTags";
+  static const char* LEVEL = "Level";
+  static const char* ALLOW_MANUAL_IDENTIFIERS = "AllowManualIdentifiers";
+  static const char* KEEP_STUDY_INSTANCE_UID = "KeepStudyInstanceUID";
+  static const char* KEEP_SERIES_INSTANCE_UID = "KeepSeriesInstanceUID";
+  static const char* UPDATE_REFERENCED_RELATIONSHIPS = "UpdateReferencedRelationships";
+  static const char* REMOVALS = "Removals";
+  static const char* CLEARINGS = "Clearings";
+  static const char* PRIVATE_TAGS_TO_KEEP = "PrivateTagsToKeep";
+  static const char* REPLACEMENTS = "Replacements";
+  static const char* MAP_PATIENTS = "MapPatients";
+  static const char* MAP_STUDIES = "MapStudies";
+  static const char* MAP_SERIES = "MapSeries";
+  static const char* MAP_INSTANCES = "MapInstances";
+  
+  void DicomModification::Serialize(Json::Value& value) const
+  {
+    if (identifierGenerator_ != NULL)
+    {
+      LOG(ERROR) << "Cannot serialize a DicomModification with a custom identifier generator";
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    value = Json::objectValue;
+    value[REMOVE_PRIVATE_TAGS] = removePrivateTags_;
+    value[LEVEL] = EnumerationToString(level_);
+    value[ALLOW_MANUAL_IDENTIFIERS] = allowManualIdentifiers_;
+    value[KEEP_STUDY_INSTANCE_UID] = keepStudyInstanceUid_;
+    value[KEEP_SERIES_INSTANCE_UID] = keepSeriesInstanceUid_;
+    value[UPDATE_REFERENCED_RELATIONSHIPS] = updateReferencedRelationships_;
+
+    SerializationToolbox::WriteSetOfTags(value, removals_, REMOVALS);
+    SerializationToolbox::WriteSetOfTags(value, clearings_, CLEARINGS);
+    SerializationToolbox::WriteSetOfTags(value, privateTagsToKeep_, PRIVATE_TAGS_TO_KEEP);
+
+    Json::Value& tmp = value[REPLACEMENTS];
+
+    tmp = Json::objectValue;
+
+    for (Replacements::const_iterator it = replacements_.begin();
+         it != replacements_.end(); ++it)
+    {
+      assert(it->second != NULL);
+      tmp[it->first.Format()] = *it->second;
+    }
+
+    Json::Value& mapPatients = value[MAP_PATIENTS];
+    Json::Value& mapStudies = value[MAP_STUDIES];
+    Json::Value& mapSeries = value[MAP_SERIES];
+    Json::Value& mapInstances = value[MAP_INSTANCES];
+
+    mapPatients = Json::objectValue;
+    mapStudies = Json::objectValue;
+    mapSeries = Json::objectValue;
+    mapInstances = Json::objectValue;
+
+    for (UidMap::const_iterator it = uidMap_.begin(); it != uidMap_.end(); ++it)
+    {
+      Json::Value* tmp = NULL;
+
+      switch (it->first.first)
+      {
+        case ResourceType_Patient:
+          tmp = &mapPatients;
+          break;
+
+        case ResourceType_Study:
+          tmp = &mapStudies;
+          break;
+
+        case ResourceType_Series:
+          tmp = &mapSeries;
+          break;
+
+        case ResourceType_Instance:
+          tmp = &mapInstances;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      assert(tmp != NULL);
+      (*tmp) [it->first.second] = it->second;
+    }
+  }
+
+
+  void DicomModification::UnserializeUidMap(ResourceType level,
+                                            const Json::Value& serialized,
+                                            const char* field)
+  {
+    if (!serialized.isMember(field) ||
+        serialized[field].type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    Json::Value::Members names = serialized[field].getMemberNames();
+    
+    for (Json::Value::Members::const_iterator it = names.begin(); it != names.end(); ++it)
+    {
+      const Json::Value& value = serialized[field][*it];
+
+      if (value.type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        uidMap_[std::make_pair(level, *it)] = value.asString();
+      }
+    }
+  }
+
+  
+  DicomModification::DicomModification(const Json::Value& serialized) :
+    identifierGenerator_(NULL)
+  {
+    removePrivateTags_ = SerializationToolbox::ReadBoolean(serialized, REMOVE_PRIVATE_TAGS);
+    level_ = StringToResourceType(SerializationToolbox::ReadString(serialized, LEVEL).c_str());
+    allowManualIdentifiers_ = SerializationToolbox::ReadBoolean(serialized, ALLOW_MANUAL_IDENTIFIERS);
+    keepStudyInstanceUid_ = SerializationToolbox::ReadBoolean(serialized, KEEP_STUDY_INSTANCE_UID);
+    keepSeriesInstanceUid_ = SerializationToolbox::ReadBoolean(serialized, KEEP_SERIES_INSTANCE_UID);
+    updateReferencedRelationships_ = SerializationToolbox::ReadBoolean
+      (serialized, UPDATE_REFERENCED_RELATIONSHIPS);
+
+    SerializationToolbox::ReadSetOfTags(removals_, serialized, REMOVALS);
+    SerializationToolbox::ReadSetOfTags(clearings_, serialized, CLEARINGS);
+    SerializationToolbox::ReadSetOfTags(privateTagsToKeep_, serialized, PRIVATE_TAGS_TO_KEEP);
+
+    if (!serialized.isMember(REPLACEMENTS) ||
+        serialized[REPLACEMENTS].type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    Json::Value::Members names = serialized[REPLACEMENTS].getMemberNames();
+
+    for (Json::Value::Members::const_iterator it = names.begin(); it != names.end(); ++it)
+    {
+      DicomTag tag(0, 0);
+      if (!DicomTag::ParseHexadecimal(tag, it->c_str()))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        const Json::Value& value = serialized[REPLACEMENTS][*it];
+        replacements_.insert(std::make_pair(tag, new Json::Value(value)));
+      }
+    }
+
+    UnserializeUidMap(ResourceType_Patient, serialized, MAP_PATIENTS);
+    UnserializeUidMap(ResourceType_Study, serialized, MAP_STUDIES);
+    UnserializeUidMap(ResourceType_Series, serialized, MAP_SERIES);
+    UnserializeUidMap(ResourceType_Instance, serialized, MAP_INSTANCES);
+  }
 }
--- a/Core/DicomParsing/DicomModification.h	Wed Jun 20 18:02:36 2018 +0200
+++ b/Core/DicomParsing/DicomModification.h	Wed Jun 20 18:03:20 2018 +0200
@@ -107,9 +107,15 @@
 
     void SetupAnonymization2017c();
 
+    void UnserializeUidMap(ResourceType level,
+                           const Json::Value& serialized,
+                           const char* field);
+
   public:
     DicomModification();
 
+    DicomModification(const Json::Value& serialized);
+
     ~DicomModification();
 
     void Keep(const DicomTag& tag);
@@ -172,5 +178,7 @@
     {
       identifierGenerator_ = &generator;
     }
+
+    void Serialize(Json::Value& value) const;
   };
 }
--- a/Core/DicomParsing/FromDcmtkBridge.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/Core/DicomParsing/FromDcmtkBridge.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -106,27 +106,6 @@
 
 namespace Orthanc
 {
-  static inline uint16_t GetCharValue(char c)
-  {
-    if (c >= '0' && c <= '9')
-      return c - '0';
-    else if (c >= 'a' && c <= 'f')
-      return c - 'a' + 10;
-    else if (c >= 'A' && c <= 'F')
-      return c - 'A' + 10;
-    else
-      return 0;
-  }
-
-  static inline uint16_t GetTagValue(const char* c)
-  {
-    return ((GetCharValue(c[0]) << 12) + 
-            (GetCharValue(c[1]) << 8) + 
-            (GetCharValue(c[2]) << 4) + 
-            GetCharValue(c[3]));
-  }
-
-
 #if DCMTK_USE_EMBEDDED_DICTIONARIES == 1
   static void LoadEmbeddedDictionary(DcmDataDictionary& dictionary,
                                      EmbeddedResources::FileResourceId resource)
@@ -1061,35 +1040,10 @@
 
   DicomTag FromDcmtkBridge::ParseTag(const char* name)
   {
-    if (strlen(name) == 9 &&
-        isxdigit(name[0]) &&
-        isxdigit(name[1]) &&
-        isxdigit(name[2]) &&
-        isxdigit(name[3]) &&
-        (name[4] == '-' || name[4] == ',') &&
-        isxdigit(name[5]) &&
-        isxdigit(name[6]) &&
-        isxdigit(name[7]) &&
-        isxdigit(name[8]))        
+    DicomTag parsed(0, 0);
+    if (DicomTag::ParseHexadecimal(parsed, name))
     {
-      uint16_t group = GetTagValue(name);
-      uint16_t element = GetTagValue(name + 5);
-      return DicomTag(group, element);
-    }
-
-    if (strlen(name) == 8 &&
-        isxdigit(name[0]) &&
-        isxdigit(name[1]) &&
-        isxdigit(name[2]) &&
-        isxdigit(name[3]) &&
-        isxdigit(name[4]) &&
-        isxdigit(name[5]) &&
-        isxdigit(name[6]) &&
-        isxdigit(name[7]))        
-    {
-      uint16_t group = GetTagValue(name);
-      uint16_t element = GetTagValue(name + 4);
-      return DicomTag(group, element);
+      return parsed;
     }
 
 #if 0
--- a/Core/Enumerations.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/Core/Enumerations.cpp	Wed Jun 20 18:03:20 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";
 
@@ -987,6 +990,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);
@@ -1436,6 +1467,68 @@
   }
 
 
+  JobState StringToJobState(const std::string& state)
+  {
+    if (state == "Pending")
+    {
+      return JobState_Pending;
+    }
+    else if (state == "Running")
+    {
+      return JobState_Running;
+    }
+    else if (state == "Success")
+    {
+      return JobState_Success;
+    }
+    else if (state == "Failure")
+    {
+      return JobState_Failure;
+    }
+    else if (state == "Paused")
+    {
+      return JobState_Paused;
+    }
+    else if (state == "Retry")
+    {
+      return JobState_Retry;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  RequestOrigin StringToRequestOrigin(const std::string& origin)
+  {
+    if (origin == "Unknown")
+    {
+      return RequestOrigin_Unknown;
+    }
+    else if (origin == "DicomProtocol")
+    {
+      return RequestOrigin_DicomProtocol;
+    }
+    else if (origin == "RestApi")
+    {
+      return RequestOrigin_RestApi;
+    }
+    else if (origin == "Plugins")
+    {
+      return RequestOrigin_Plugins;
+    }
+    else if (origin == "Lua")
+    {
+      return RequestOrigin_Lua;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+  
+
   unsigned int GetBytesPerPixel(PixelFormat format)
   {
     switch (format)
--- a/Core/Enumerations.h	Wed Jun 20 18:02:36 2018 +0200
+++ b/Core/Enumerations.h	Wed Jun 20 18:03:20 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 */,
@@ -548,6 +549,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
@@ -628,6 +647,8 @@
 
   const char* EnumerationToString(ValueRepresentation vr);
 
+  const char* EnumerationToString(JobState state);
+
   Encoding StringToEncoding(const char* encoding);
 
   ResourceType StringToResourceType(const char* type);
@@ -644,6 +665,10 @@
   ModalityManufacturer StringToModalityManufacturer(const std::string& manufacturer);
 
   DicomVersion StringToDicomVersion(const std::string& version);
+
+  JobState StringToJobState(const std::string& state);
+  
+  RequestOrigin StringToRequestOrigin(const std::string& origin);
   
   unsigned int GetBytesPerPixel(PixelFormat format);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/FileStorage/MemoryStorageArea.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,119 @@
+/**
+ * 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 "MemoryStorageArea.h"
+
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  MemoryStorageArea::~MemoryStorageArea()
+  {
+    for (Content::iterator it = content_.begin(); it != content_.end(); ++it)
+    {
+      if (it->second != NULL)
+      {
+        delete it->second;
+      }
+    }
+  }
+    
+  void MemoryStorageArea::Create(const std::string& uuid,
+                                 const void* content,
+                                 size_t size,
+                                 FileContentType type)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    if (size != 0 &&
+        content == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else if (content_.find(uuid) != content_.end())
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      content_[uuid] = new std::string(reinterpret_cast<const char*>(content), size);
+    }
+  }
+
+  
+  void MemoryStorageArea::Read(std::string& content,
+                               const std::string& uuid,
+                               FileContentType type)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Content::const_iterator found = content_.find(uuid);
+
+    if (found == content_.end())
+    {
+      throw OrthancException(ErrorCode_InexistentFile);
+    }
+    else if (found->second == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      content.assign(*found->second);
+    }
+  }
+      
+
+  void MemoryStorageArea::Remove(const std::string& uuid,
+                                 FileContentType type)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Content::iterator found = content_.find(uuid);
+    
+    if (found == content_.end())
+    {
+      // Ignore second removal
+    }
+    else if (found->second == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+    else
+    {
+      delete found->second;
+      content_.erase(found);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/FileStorage/MemoryStorageArea.h	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,66 @@
+/**
+ * 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 "IStorageArea.h"
+
+#include <boost/thread/mutex.hpp>
+#include <map>
+
+namespace Orthanc
+{
+  class MemoryStorageArea : public IStorageArea
+  {
+  private:
+    typedef std::map<std::string, std::string*>  Content;
+    
+    boost::mutex  mutex_;
+    Content       content_;
+    
+  public:
+    virtual ~MemoryStorageArea();
+    
+    virtual void Create(const std::string& uuid,
+                        const void* content,
+                        size_t size,
+                        FileContentType type);
+
+    virtual void Read(std::string& content,
+                      const std::string& uuid,
+                      FileContentType type);
+
+    virtual void Remove(const std::string& uuid,
+                        FileContentType type);
+  };
+}
--- a/Core/HttpClient.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/Core/HttpClient.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -46,16 +46,6 @@
 #include <boost/thread/mutex.hpp>
 
 
-#if ORTHANC_ENABLE_SSL == 1
-// For OpenSSL initialization and finalization
-#  include <openssl/conf.h>
-#  include <openssl/engine.h>
-#  include <openssl/err.h>
-#  include <openssl/evp.h>
-#  include <openssl/ssl.h>
-#endif
-
-
 #if ORTHANC_ENABLE_PKCS11 == 1
 #  include "Pkcs11.h"
 #endif
@@ -812,34 +802,4 @@
     throw OrthancException(ErrorCode_InternalError);
 #endif
   }
-
-
-  void HttpClient::InitializeOpenSsl()
-  {
-#if ORTHANC_ENABLE_SSL == 1
-    // https://wiki.openssl.org/index.php/Library_Initialization
-    SSL_library_init();
-    SSL_load_error_strings();
-    OpenSSL_add_all_algorithms();
-    ERR_load_crypto_strings();
-#endif
-  }
-
-
-  void HttpClient::FinalizeOpenSsl()
-  {
-#if ORTHANC_ENABLE_SSL == 1
-    // Finalize OpenSSL
-    // https://wiki.openssl.org/index.php/Library_Initialization#Cleanup
-#ifdef FIPS_mode_set
-    FIPS_mode_set(0);
-#endif
-    ENGINE_cleanup();
-    CONF_modules_unload(1);
-    EVP_cleanup();
-    CRYPTO_cleanup_all_ex_data();
-    ERR_remove_state(0);
-    ERR_free_strings();
-#endif
-  }
 }
--- a/Core/HttpClient.h	Wed Jun 20 18:02:36 2018 +0200
+++ b/Core/HttpClient.h	Wed Jun 20 18:03:20 2018 +0200
@@ -268,10 +268,6 @@
   
     static void GlobalFinalize();
 
-    static void InitializeOpenSsl();
-
-    static void FinalizeOpenSsl();
-
     static void InitializePkcs11(const std::string& module,
                                  const std::string& pin,
                                  bool verbose);
--- a/Core/HttpServer/IIncomingHttpRequestFilter.h	Wed Jun 20 18:02:36 2018 +0200
+++ b/Core/HttpServer/IIncomingHttpRequestFilter.h	Wed Jun 20 18:03:20 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 Jun 20 18:02:36 2018 +0200
+++ b/Core/HttpServer/MongooseServer.cpp	Wed Jun 20 18:03:20 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 Jun 20 18:02:36 2018 +0200
+++ b/Core/HttpServer/MongooseServer.h	Wed Jun 20 18:03:20 2018 +0200
@@ -162,7 +162,7 @@
 
     void SetHttpCompressionEnabled(bool enabled);
 
-    const IIncomingHttpRequestFilter* GetIncomingHttpRequestFilter() const
+    IIncomingHttpRequestFilter* GetIncomingHttpRequestFilter() const
     {
       return filter_;
     }
--- a/Core/Images/ImageProcessing.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/Core/Images/ImageProcessing.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -550,7 +550,8 @@
       return;
     }
 
-    if (target.GetFormat() == PixelFormat_RGBA32 &&
+    if ((target.GetFormat() == PixelFormat_RGBA32 ||
+         target.GetFormat() == PixelFormat_BGRA32) &&
         source.GetFormat() == PixelFormat_Grayscale8)
     {
       for (unsigned int y = 0; y < source.GetHeight(); y++)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/GenericJobUnserializer.cpp	Wed Jun 20 18:03:20 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 "../PrecompiledHeaders.h"
+#include "GenericJobUnserializer.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../SerializationToolbox.h"
+
+#include "Operations/LogJobOperation.h"
+#include "Operations/NullOperationValue.h"
+#include "Operations/SequenceOfOperationsJob.h"
+#include "Operations/StringOperationValue.h"
+
+namespace Orthanc
+{
+  IJob* GenericJobUnserializer::UnserializeJob(const Json::Value& source)
+  {
+    const std::string type = SerializationToolbox::ReadString(source, "Type");
+
+    if (type == "SequenceOfOperations")
+    {
+      return new SequenceOfOperationsJob(*this, source);
+    }
+    else
+    {
+      LOG(ERROR) << "Cannot unserialize job of type: " << type;
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  IJobOperation* GenericJobUnserializer::UnserializeOperation(const Json::Value& source)
+  {
+    const std::string type = SerializationToolbox::ReadString(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 = SerializationToolbox::ReadString(source, "Type");
+
+    if (type == "String")
+    {
+      return new StringOperationValue(SerializationToolbox::ReadString(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 Jun 20 18:03:20 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& value);
+
+    virtual IJobOperation* UnserializeOperation(const Json::Value& value);
+
+    virtual JobOperationValue* UnserializeValue(const Json::Value& value);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/IJob.h	Wed Jun 20 18:03:20 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 bool Serialize(Json::Value& value) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/IJobUnserializer.h	Wed Jun 20 18:03:20 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 "IJob.h"
+#include "Operations/JobOperationValue.h"
+#include "Operations/IJobOperation.h"
+
+#include <vector>
+
+namespace Orthanc
+{
+  class IJobUnserializer : public boost::noncopyable
+  {
+  public:
+    virtual ~IJobUnserializer()
+    {
+    }
+
+    virtual IJob* UnserializeJob(const Json::Value& value) = 0;
+
+    virtual IJobOperation* UnserializeOperation(const Json::Value& value) = 0;
+
+    virtual JobOperationValue* UnserializeValue(const Json::Value& value) = 0;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobInfo.cpp	Wed Jun 20 18:03:20 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/>.
+ **/
+
+
+#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::Format(Json::Value& target) 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["Content"] = status_.GetPublicContent();
+
+    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 Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,120 @@
+/**
+ * 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 Format(Json::Value& target) const;
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobStatus.cpp	Wed Jun 20 18:03:20 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 "../PrecompiledHeaders.h"
+#include "JobStatus.h"
+
+#include "../OrthancException.h"
+
+namespace Orthanc
+{
+  JobStatus::JobStatus() :
+    errorCode_(ErrorCode_InternalError),
+    progress_(0),
+    jobType_("Invalid"),
+    publicContent_(Json::objectValue),
+    hasSerialized_(false)
+  {
+  }
+
+  
+  JobStatus::JobStatus(ErrorCode code,
+                       IJob& job) :
+    errorCode_(code),
+    progress_(job.GetProgress()),
+    publicContent_(Json::objectValue)
+  {
+    if (progress_ < 0)
+    {
+      progress_ = 0;
+    }
+      
+    if (progress_ > 1)
+    {
+      progress_ = 1;
+    }
+
+    job.GetJobType(jobType_);
+    job.GetPublicContent(publicContent_);
+
+    hasSerialized_ = job.Serialize(serialized_);
+  }
+
+
+  const Json::Value& JobStatus::GetSerialized() const
+  {
+    if (!hasSerialized_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return serialized_;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobStatus.h	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,88 @@
+/**
+ * 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    serialized_;
+    bool           hasSerialized_;
+
+  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& GetSerialized() const;
+
+    bool HasSerialized() const
+    {
+      return hasSerialized_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobStepResult.cpp	Wed Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,324 @@
+/**
+ * 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"
+
+#include <json/reader.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(engine->threadSleep_));
+      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(), engine->threadSleep_);
+
+      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),
+    registry_(new JobsRegistry),
+    threadSleep_(200),
+    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();
+    }
+  }
+
+ 
+  JobsRegistry& JobsEngine::GetRegistry()
+  {
+    if (registry_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    return *registry_;
+  }
+  
+   
+  void JobsEngine::LoadRegistryFromJson(IJobUnserializer& unserializer,
+                                        const Json::Value& serialized)
+  {
+    boost::mutex::scoped_lock lock(stateMutex_);
+      
+    if (state_ != State_Setup)
+    {
+      // Can only be invoked before calling "Start()"
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    registry_.reset(new JobsRegistry(unserializer, serialized));
+  }
+
+
+  void JobsEngine::LoadRegistryFromString(IJobUnserializer& unserializer,
+                                          const std::string& serialized)
+  {
+    Json::Value value;
+    Json::Reader reader;
+    if (reader.parse(serialized, value))
+    {
+      LoadRegistryFromJson(unserializer, value);
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+
+
+  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::SetThreadSleep(unsigned int sleep)
+  {
+    boost::mutex::scoped_lock lock(stateMutex_);
+      
+    if (state_ != State_Setup)
+    {
+      // Can only be invoked before calling "Start()"
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    threadSleep_ = sleep;
+  }
+
+
+  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 Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,91 @@
+/**
+ * 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_;
+    std::auto_ptr<JobsRegistry>  registry_;
+    boost::thread                retryHandler_;
+    unsigned int                 threadSleep_;
+    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();
+
+    JobsRegistry& GetRegistry();
+
+    void LoadRegistryFromJson(IJobUnserializer& unserializer,
+                              const Json::Value& serialized);
+
+    void LoadRegistryFromString(IJobUnserializer& unserializer,
+                                const std::string& serialized);
+
+    void SetWorkersCount(size_t count);
+
+    void SetThreadSleep(unsigned int sleep);
+
+    void Start();
+
+    void Stop();
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobsRegistry.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,1304 @@
+/**
+ * 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"
+#include "../SerializationToolbox.h"
+
+namespace Orthanc
+{
+  static const char* STATE = "State";
+  static const char* TYPE = "Type";
+  static const char* PRIORITY = "Priority";
+  static const char* JOB = "Job";
+  static const char* JOBS = "Jobs";
+  static const char* JOBS_REGISTRY = "JobsRegistry";
+  static const char* MAX_COMPLETED_JOBS = "MaxCompletedJobs";
+  static const char* CREATION_TIME = "CreationTime";
+  static const char* LAST_CHANGE_TIME = "LastChangeTime";
+  static const char* RUNTIME = "Runtime";
+  
+
+  class JobsRegistry::JobHandler : public boost::noncopyable
+  {   
+  private:
+    std::string                       id_;
+    JobState                          state_;
+    std::string                       jobType_;
+    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);
+      }
+
+      job->GetJobType(jobType_);
+      job->Start();
+
+      lastStatus_ = JobStatus(ErrorCode_Success, *job_);
+    }
+
+    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_;
+    }
+
+    void SetLastStateChangeTime(const boost::posix_time::ptime& time)
+    {
+      lastStateChangeTime_ = time;
+    }
+
+    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 Serialize(Json::Value& target) const
+    {
+      target = Json::objectValue;
+
+      bool ok;
+
+      if (state_ == JobState_Running)
+      {
+        // WARNING: Cannot directly access the "job_" member, as long
+        // as a "RunningJob" instance is running. We do not use a
+        // mutex at the "JobHandler" level, as serialization would be
+        // blocked while a step in the job is running. Instead, we
+        // save a snapshot of the serialized job.
+
+        if (lastStatus_.HasSerialized())
+        {
+          target[JOB] = lastStatus_.GetSerialized();
+          ok = true;
+        }
+        else
+        {
+          ok = false;
+        }
+      }
+      else 
+      {
+        ok = job_->Serialize(target[JOB]);
+      }
+
+      if (ok)
+      {
+        target[STATE] = EnumerationToString(state_);
+        target[PRIORITY] = priority_;
+        target[CREATION_TIME] = boost::posix_time::to_iso_string(creationTime_);
+        target[LAST_CHANGE_TIME] = boost::posix_time::to_iso_string(lastStateChangeTime_);
+        target[RUNTIME] = static_cast<unsigned int>(runtime_.total_milliseconds());
+        return true;
+      }
+      else
+      {
+        LOG(INFO) << "Job backup is not supported for job of type: " << jobType_;
+        return false;
+      }
+    }
+
+    JobHandler(IJobUnserializer& unserializer,
+               const Json::Value& serialized,
+               const std::string& id) :
+      id_(id),
+      pauseScheduled_(false),
+      cancelScheduled_(false)
+    {
+      state_ = StringToJobState(SerializationToolbox::ReadString(serialized, STATE));
+      priority_ = SerializationToolbox::ReadInteger(serialized, PRIORITY);
+      creationTime_ = boost::posix_time::from_iso_string
+        (SerializationToolbox::ReadString(serialized, CREATION_TIME));
+      lastStateChangeTime_ = boost::posix_time::from_iso_string
+        (SerializationToolbox::ReadString(serialized, LAST_CHANGE_TIME));
+      runtime_ = boost::posix_time::milliseconds
+        (SerializationToolbox::ReadInteger(serialized, RUNTIME));
+
+      retryTime_ = creationTime_;
+
+      job_.reset(unserializer.UnserializeJob(serialized[JOB]));
+      job_->GetJobType(jobType_);
+      job_->Start();
+
+      lastStatus_ = JobStatus(ErrorCode_Success, *job_);
+    }
+  };
+
+
+  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);
+
+    if (observer_ != NULL)
+    {
+      if (success)
+      {
+        observer_->SignalJobSuccess(job.GetId());
+      }
+      else
+      {
+        observer_->SignalJobFailure(job.GetId());
+      }
+    }
+
+    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::SubmitInternal(std::string& id,
+                                    JobHandler* handlerRaw,
+                                    bool keepLastChangeTime)
+  {
+    if (handlerRaw == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    
+    std::auto_ptr<JobHandler>  handler(handlerRaw);
+
+    boost::posix_time::ptime lastChangeTime = handler->GetLastStateChangeTime();
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      CheckInvariants();
+      
+      id = handler->GetId();
+      int priority = handler->GetPriority();
+
+      switch (handler->GetState())
+      {
+        case JobState_Pending:
+        case JobState_Retry:
+        case JobState_Running:
+          handler->SetState(JobState_Pending);
+          pendingJobs_.push(handler.get());
+          pendingJobAvailable_.notify_one();
+          break;
+ 
+        case JobState_Success:
+          SetCompletedJob(*handler, true);
+          break;
+        
+        case JobState_Failure:
+          SetCompletedJob(*handler, false);
+          break;
+
+        case JobState_Paused:
+          break;
+        
+        default:
+          LOG(ERROR) << "A job should not be loaded from state: "
+                     << EnumerationToString(handler->GetState());
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (keepLastChangeTime)
+      {
+        handler->SetLastStateChangeTime(lastChangeTime);
+      }
+    
+      jobsIndex_.insert(std::make_pair(id, handler.release()));
+
+      LOG(INFO) << "New job submitted with priority " << priority << ": " << id;
+
+      if (observer_ != NULL)
+      {
+        observer_->SignalJobSubmitted(id);
+      }
+
+      CheckInvariants();
+    }
+  }
+
+
+  void JobsRegistry::Submit(std::string& id,
+                            IJob* job,        // Takes ownership
+                            int priority)
+  {
+    SubmitInternal(id, new JobHandler(job, priority), false);
+  }
+
+
+  void JobsRegistry::Submit(IJob* job,        // Takes ownership
+                            int priority)
+  {
+    std::string id;
+    SubmitInternal(id, new JobHandler(job, priority), false);
+  }
+
+
+  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)
+    {
+      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);
+  }
+
+
+  void JobsRegistry::SetObserver(JobsRegistry::IObserver& observer)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    observer_ = &observer;
+  }
+
+  
+  void JobsRegistry::ResetObserver()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    observer_ = NULL;
+  }
+
+  
+  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);
+    }
+  }
+
+
+
+  void JobsRegistry::Serialize(Json::Value& target)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    CheckInvariants();
+
+    target = Json::objectValue;
+    target[TYPE] = JOBS_REGISTRY;
+    target[MAX_COMPLETED_JOBS] = static_cast<unsigned int>(maxCompletedJobs_);
+    target[JOBS] = Json::objectValue;
+    
+    for (JobsIndex::const_iterator it = jobsIndex_.begin(); 
+         it != jobsIndex_.end(); ++it)
+    {
+      Json::Value v;
+      if (it->second->Serialize(v))
+      {
+        target[JOBS][it->first] = v;
+      }
+    }
+  }
+
+
+  JobsRegistry::JobsRegistry(IJobUnserializer& unserializer,
+                             const Json::Value& s) :
+    observer_(NULL)
+  {
+    if (SerializationToolbox::ReadString(s, TYPE) != JOBS_REGISTRY ||
+        !s.isMember(JOBS) ||
+        s[JOBS].type() != Json::objectValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    maxCompletedJobs_ = SerializationToolbox::ReadUnsignedInteger(s, MAX_COMPLETED_JOBS);
+
+    Json::Value::Members members = s[JOBS].getMemberNames();
+
+    for (Json::Value::Members::const_iterator it = members.begin();
+         it != members.end(); ++it)
+    {
+      std::auto_ptr<JobHandler> job(new JobHandler(unserializer, s[JOBS][*it], *it));
+      
+      std::string id;
+      SubmitInternal(id, job.release(), true);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/JobsRegistry.h	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,234 @@
+/**
+ * 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 "IJobUnserializer.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
+  {
+  public:
+    class IObserver : public boost::noncopyable
+    {
+    public:
+      virtual ~IObserver()
+      {
+      }
+
+      virtual void SignalJobSubmitted(const std::string& jobId) = 0;
+
+      virtual void SignalJobSuccess(const std::string& jobId) = 0;
+
+      virtual void SignalJobFailure(const std::string& jobId) = 0;
+    };
+    
+  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_;
+
+    IObserver*                 observer_;
+
+
+#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);
+      
+    void SubmitInternal(std::string& id,
+                        JobHandler* handler,
+                        bool keepLastChangeTime);
+    
+  public:
+    JobsRegistry() :
+      maxCompletedJobs_(10),
+      observer_(NULL)
+    {
+    }
+
+    JobsRegistry(IJobUnserializer& unserializer,
+                 const Json::Value& s);
+
+    ~JobsRegistry();
+
+    void SetMaxCompletedJobs(size_t i);
+    
+    void ListJobs(std::set<std::string>& target);
+
+    bool GetJobInfo(JobInfo& target,
+                    const std::string& id);
+
+    void Serialize(Json::Value& target);
+    
+    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);
+
+    void SetObserver(IObserver& observer);
+
+    void ResetObserver();
+
+    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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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* 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,53 @@
+/**
+ * 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 = Json::objectValue;
+      result["Type"] = "Log";
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/NullOperationValue.h	Wed Jun 20 18:03:20 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 "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 = Json::objectValue;
+      target["Type"] = "Null";
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,495 @@
+/**
+ * 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"
+#include "../../SerializationToolbox.h"
+#include "../IJobUnserializer.h"
+
+namespace Orthanc
+{
+  static const char* CURRENT = "Current";
+  static const char* DESCRIPTION = "Description";
+  static const char* DICOM_TIMEOUT = "DicomTimeout";
+  static const char* NEXT_OPERATIONS = "Next";
+  static const char* OPERATION = "Operation";
+  static const char* OPERATIONS = "Operations";
+  static const char* ORIGINAL_INPUTS = "OriginalInputs";
+  static const char* TRAILING_TIMEOUT = "TrailingTimeout";
+  static const char* TYPE = "Type";
+  static const char* WORK_INPUTS = "WorkInputs";
+
+  
+  class SequenceOfOperationsJob::Operation : public boost::noncopyable
+  {
+  private:
+    size_t                             index_;
+    std::auto_ptr<IJobOperation>       operation_;
+    std::auto_ptr<JobOperationValues>  originalInputs_;
+    std::auto_ptr<JobOperationValues>  workInputs_;
+    std::list<Operation*>              nextOperations_;
+    size_t                             currentInput_;
+
+  public:
+    Operation(size_t index,
+              IJobOperation* operation) :
+      index_(index),
+      operation_(operation),
+      originalInputs_(new JobOperationValues),
+      workInputs_(new JobOperationValues),
+      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,
+                          bool unserializing)
+    {
+      if (other.index_ <= index_)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      if (!unserializing &&
+          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;
+      target[CURRENT] = static_cast<unsigned int>(currentInput_);
+      operation_->Serialize(target[OPERATION]);
+      originalInputs_->Serialize(target[ORIGINAL_INPUTS]);
+      workInputs_->Serialize(target[WORK_INPUTS]);      
+
+      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[NEXT_OPERATIONS] = tmp;
+    }
+
+    Operation(IJobUnserializer& unserializer,
+              Json::Value::ArrayIndex index,
+              const Json::Value& serialized) :
+      index_(index)
+    {
+      if (serialized.type() != Json::objectValue ||
+          !serialized.isMember(OPERATION) ||
+          !serialized.isMember(ORIGINAL_INPUTS) ||
+          !serialized.isMember(WORK_INPUTS))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      currentInput_ = SerializationToolbox::ReadUnsignedInteger(serialized, CURRENT);
+      operation_.reset(unserializer.UnserializeOperation(serialized[OPERATION]));
+      originalInputs_.reset(JobOperationValues::Unserialize
+                            (unserializer, serialized[ORIGINAL_INPUTS]));
+      workInputs_.reset(JobOperationValues::Unserialize
+                        (unserializer, serialized[WORK_INPUTS]));
+    }
+  };
+
+
+  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::GetDescription(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, false /* not unserializing */);
+    }
+  }
+
+
+  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 were 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_;
+  }
+
+
+  bool SequenceOfOperationsJob::Serialize(Json::Value& value)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    value = Json::objectValue;
+
+    std::string jobType;
+    GetJobType(jobType);
+    value[TYPE] = jobType;
+    
+    value[DESCRIPTION] = description_;
+    value[TRAILING_TIMEOUT] = static_cast<unsigned int>(trailingTimeout_.total_milliseconds());
+    value[DICOM_TIMEOUT] = connectionManager_.GetTimeout();
+    value[CURRENT] = static_cast<unsigned int>(current_);
+    
+    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;
+
+    return true;
+  }
+
+
+  SequenceOfOperationsJob::SequenceOfOperationsJob(IJobUnserializer& unserializer,
+                                                   const Json::Value& serialized) :
+    done_(false)
+  {
+    std::string jobType;
+    GetJobType(jobType);
+    
+    if (SerializationToolbox::ReadString(serialized, TYPE) != jobType ||
+        !serialized.isMember(OPERATIONS) ||
+        serialized[OPERATIONS].type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    description_ = SerializationToolbox::ReadString(serialized, DESCRIPTION);
+    trailingTimeout_ = boost::posix_time::milliseconds
+      (SerializationToolbox::ReadUnsignedInteger(serialized, TRAILING_TIMEOUT));
+    connectionManager_.SetTimeout
+      (SerializationToolbox::ReadUnsignedInteger(serialized, DICOM_TIMEOUT));
+    current_ = SerializationToolbox::ReadUnsignedInteger(serialized, CURRENT);
+
+    const Json::Value& ops = serialized[OPERATIONS];
+
+    // Unserialize the individual operations
+    operations_.reserve(ops.size());
+    for (Json::Value::ArrayIndex i = 0; i < ops.size(); i++)
+    {
+      operations_.push_back(new Operation(unserializer, i, ops[i]));
+    }
+
+    // Connect the next operations
+    for (Json::Value::ArrayIndex i = 0; i < ops.size(); i++)
+    {
+      if (!ops[i].isMember(NEXT_OPERATIONS) ||
+          ops[i][NEXT_OPERATIONS].type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      const Json::Value& next = ops[i][NEXT_OPERATIONS];
+      for (Json::Value::ArrayIndex j = 0; j < next.size(); j++)
+      {
+        if (next[j].type() != Json::intValue ||
+            next[j].asInt() < 0 ||
+            next[j].asUInt() >= operations_.size())
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }
+        else
+        {
+          operations_[i]->AddNextOperation(*operations_[next[j].asUInt()], true);
+        }
+      }
+    }  
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/Operations/SequenceOfOperationsJob.h	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,153 @@
+/**
+ * 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();
+
+    SequenceOfOperationsJob(IJobUnserializer& unserializer,
+                            const Json::Value& serialized);
+
+    virtual ~SequenceOfOperationsJob();
+
+    void SetDescription(const std::string& description);
+
+    void GetDescription(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 bool Serialize(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 Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,71 @@
+/**
+ * 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 = Json::objectValue;
+      target["Type"] = "String";
+      target["Content"] = content_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/SetOfInstancesJob.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,236 @@
+/**
+ * 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"
+#include "../SerializationToolbox.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()));
+    }
+  }
+
+
+  const std::string& SetOfInstancesJob::GetInstance(size_t index) const
+  {
+    if (index > instances_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return instances_[index];
+    }
+  }
+      
+
+  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::GetPublicContent(Json::Value& value)
+  {
+    value["Description"] = GetDescription();
+    value["InstancesCount"] = static_cast<uint32_t>(instances_.size());
+    value["FailedInstancesCount"] = static_cast<uint32_t>(failedInstances_.size());
+  }    
+
+
+  bool SetOfInstancesJob::Serialize(Json::Value& value)
+  {
+    value = Json::objectValue;
+
+    std::string type;
+    GetJobType(type);
+    value["Type"] = type;
+
+    value["Permissive"] = permissive_;
+    value["Position"] = static_cast<unsigned int>(position_);
+    value["Description"] = description_;
+
+    SerializationToolbox::WriteArrayOfStrings(value, instances_, "Instances");
+    SerializationToolbox::WriteSetOfStrings(value, failedInstances_, "FailedInstances");
+
+    return true;
+  }
+
+
+  SetOfInstancesJob::SetOfInstancesJob(const Json::Value& value) :
+    started_(false),
+    permissive_(SerializationToolbox::ReadBoolean(value, "Permissive")),
+    position_(SerializationToolbox::ReadUnsignedInteger(value, "Position")),
+    description_(SerializationToolbox::ReadString(value, "Description"))
+  {
+    SerializationToolbox::ReadArrayOfStrings(instances_, value, "Instances");
+    SerializationToolbox::ReadSetOfStrings(failedInstances_, value, "FailedInstances");
+
+    if (position_ > instances_.size())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/SetOfInstancesJob.h	Wed Jun 20 18:03:20 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 "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_;
+    std::string               description_;
+
+  protected:
+    virtual bool HandleInstance(const std::string& instance) = 0;
+
+  public:
+    SetOfInstancesJob();
+
+    SetOfInstancesJob(const Json::Value& s);  // Unserialization
+
+    size_t GetPosition() const
+    {
+      return position_;
+    }
+
+    void SetDescription(const std::string& description)
+    {
+      description_ = description;
+    }
+
+    const std::string& GetDescription() const
+    {
+      return description_;
+    }
+
+    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::string& GetInstance(size_t index) const;
+      
+    const std::set<std::string>& GetFailedInstances() const
+    {
+      return failedInstances_;
+    }
+
+    bool IsFailedInstance(const std::string& instance) const
+    {
+      return failedInstances_.find(instance) != failedInstances_.end();
+    }
+    
+    virtual JobStepResult ExecuteStep();
+    
+    virtual void GetPublicContent(Json::Value& value);
+    
+    virtual bool Serialize(Json::Value& value);
+  };
+}
--- a/Core/MultiThreading/BagOfTasks.h	Wed Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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();
-      }
-    };
-  };
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/SerializationToolbox.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,262 @@
+/**
+ * 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 "SerializationToolbox.h"
+
+#include "OrthancException.h"
+
+namespace Orthanc
+{
+  namespace SerializationToolbox
+  {
+    std::string ReadString(const Json::Value& value,
+                           const std::string& field)
+    {
+      if (value.type() != Json::objectValue ||
+          !value.isMember(field.c_str()) ||
+          value[field.c_str()].type() != Json::stringValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        return value[field.c_str()].asString();
+      }
+    }
+
+
+    int ReadInteger(const Json::Value& value,
+                    const std::string& field)
+    {
+      if (value.type() != Json::objectValue ||
+          !value.isMember(field.c_str()) ||
+          (value[field.c_str()].type() != Json::intValue &&
+           value[field.c_str()].type() != Json::uintValue))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        return value[field.c_str()].asInt();
+      }    
+    }
+
+
+    unsigned int ReadUnsignedInteger(const Json::Value& value,
+                                     const std::string& field)
+    {
+      int tmp = ReadInteger(value, field);
+
+      if (tmp < 0)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        return static_cast<unsigned int>(tmp);
+      }
+    }
+
+
+    bool ReadBoolean(const Json::Value& value,
+                     const std::string& field)
+    {
+      if (value.type() != Json::objectValue ||
+          !value.isMember(field.c_str()) ||
+          value[field.c_str()].type() != Json::booleanValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+      else
+      {
+        return value[field.c_str()].asBool();
+      }   
+    }
+
+  
+    void ReadArrayOfStrings(std::vector<std::string>& target,
+                            const Json::Value& value,
+                            const std::string& field)
+    {
+      if (value.type() != Json::objectValue ||
+          !value.isMember(field.c_str()) ||
+          value[field.c_str()].type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      const Json::Value& arr = value[field.c_str()];
+
+      target.resize(arr.size());
+
+      for (Json::Value::ArrayIndex i = 0; i < arr.size(); i++)
+      {
+        if (arr[i].type() != Json::stringValue)
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);        
+        }
+        else
+        {
+          target[i] = arr[i].asString();
+        }
+      }
+    }
+
+
+    void ReadListOfStrings(std::list<std::string>& target,
+                           const Json::Value& value,
+                           const std::string& field)
+    {
+      std::vector<std::string> tmp;
+      ReadArrayOfStrings(tmp, value, field);
+
+      target.clear();
+      for (size_t i = 0; i < tmp.size(); i++)
+      {
+        target.push_back(tmp[i]);
+      }
+    }
+  
+
+    void ReadSetOfStrings(std::set<std::string>& target,
+                          const Json::Value& value,
+                          const std::string& field)
+    {
+      std::vector<std::string> tmp;
+      ReadArrayOfStrings(tmp, value, field);
+
+      target.clear();
+      for (size_t i = 0; i < tmp.size(); i++)
+      {
+        target.insert(tmp[i]);
+      }
+    }
+
+
+    void ReadSetOfTags(std::set<DicomTag>& target,
+                       const Json::Value& value,
+                       const std::string& field)
+    {
+      if (value.type() != Json::objectValue ||
+          !value.isMember(field.c_str()) ||
+          value[field.c_str()].type() != Json::arrayValue)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      const Json::Value& arr = value[field.c_str()];
+
+      target.clear();
+
+      for (Json::Value::ArrayIndex i = 0; i < arr.size(); i++)
+      {
+        DicomTag tag(0, 0);
+
+        if (arr[i].type() != Json::stringValue ||
+            !DicomTag::ParseHexadecimal(tag, arr[i].asCString()))
+        {
+          throw OrthancException(ErrorCode_BadFileFormat);        
+        }
+        else
+        {
+          target.insert(tag);
+        }
+      }
+    }
+
+
+    void WriteArrayOfStrings(Json::Value& target,
+                             const std::vector<std::string>& values,
+                             const std::string& field)
+    {
+      if (target.type() != Json::objectValue ||
+          target.isMember(field.c_str()))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      Json::Value& value = target[field];
+
+      value = Json::arrayValue;
+      for (size_t i = 0; i < values.size(); i++)
+      {
+        value.append(values[i]);
+      }
+    }
+
+
+    void WriteSetOfStrings(Json::Value& target,
+                           const std::set<std::string>& values,
+                           const std::string& field)
+    {
+      if (target.type() != Json::objectValue ||
+          target.isMember(field.c_str()))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      Json::Value& value = target[field];
+
+      value = Json::arrayValue;
+
+      for (std::set<std::string>::const_iterator it = values.begin();
+           it != values.end(); ++it)
+      {
+        value.append(*it);
+      }
+    }
+
+
+    void WriteSetOfTags(Json::Value& target,
+                        const std::set<DicomTag>& tags,
+                        const std::string& field)
+    {
+      if (target.type() != Json::objectValue ||
+          target.isMember(field.c_str()))
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+
+      Json::Value& value = target[field];
+
+      value = Json::arrayValue;
+
+      for (std::set<DicomTag>::const_iterator it = tags.begin();
+           it != tags.end(); ++it)
+      {
+        value.append(it->Format());
+      }
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/SerializationToolbox.h	Wed Jun 20 18:03:20 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 "DicomFormat/DicomTag.h"
+
+#include <json/value.h>
+#include <list>
+
+namespace Orthanc
+{
+  namespace SerializationToolbox
+  {
+    std::string ReadString(const Json::Value& value,
+                           const std::string& field);
+
+    int ReadInteger(const Json::Value& value,
+                    const std::string& field);
+
+    unsigned int ReadUnsignedInteger(const Json::Value& value,
+                                     const std::string& field);
+
+    bool ReadBoolean(const Json::Value& value,
+                     const std::string& field);
+
+    void ReadArrayOfStrings(std::vector<std::string>& target,
+                            const Json::Value& value,
+                            const std::string& field);
+
+    void ReadListOfStrings(std::list<std::string>& target,
+                           const Json::Value& value,
+                           const std::string& field);
+
+    void ReadSetOfStrings(std::set<std::string>& target,
+                          const Json::Value& value,
+                          const std::string& field);
+
+    void ReadSetOfTags(std::set<DicomTag>& target,
+                       const Json::Value& value,
+                       const std::string& field);
+
+    void WriteArrayOfStrings(Json::Value& target,
+                             const std::vector<std::string>& values,
+                             const std::string& field);
+
+    void WriteSetOfStrings(Json::Value& target,
+                           const std::set<std::string>& values,
+                           const std::string& field);
+
+    void WriteSetOfTags(Json::Value& target,
+                        const std::set<DicomTag>& tags,
+                        const std::string& field);
+  }
+}
--- a/Core/Toolbox.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/Core/Toolbox.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -62,6 +62,15 @@
 #  include <boost/locale.hpp>
 #endif
 
+#if ORTHANC_ENABLE_SSL == 1
+// For OpenSSL initialization and finalization
+#  include <openssl/conf.h>
+#  include <openssl/engine.h>
+#  include <openssl/err.h>
+#  include <openssl/evp.h>
+#  include <openssl/ssl.h>
+#endif
+
 
 #if defined(_MSC_VER) && (_MSC_VER < 1800)
 // Patch for the missing "_strtoll" symbol when compiling with Visual Studio < 2013
@@ -1421,7 +1430,7 @@
     globalLocale_.reset();
   }
 
-  
+
   std::string Toolbox::ToUpperCaseWithAccents(const std::string& source)
   {
     if (globalLocale_.get() == NULL)
@@ -1464,6 +1473,36 @@
 #endif
 
 
+  void Toolbox::InitializeOpenSsl()
+  {
+#if ORTHANC_ENABLE_SSL == 1
+    // https://wiki.openssl.org/index.php/Library_Initialization
+    SSL_library_init();
+    SSL_load_error_strings();
+    OpenSSL_add_all_algorithms();
+    ERR_load_crypto_strings();
+#endif
+  }
+
+
+  void Toolbox::FinalizeOpenSsl()
+  {
+#if ORTHANC_ENABLE_SSL == 1
+    // Finalize OpenSSL
+    // https://wiki.openssl.org/index.php/Library_Initialization#Cleanup
+#ifdef FIPS_mode_set
+    FIPS_mode_set(0);
+#endif
+    ENGINE_cleanup();
+    CONF_modules_unload(1);
+    EVP_cleanup();
+    CRYPTO_cleanup_all_ex_data();
+    ERR_remove_state(0);
+    ERR_free_strings();
+#endif
+  }
+
+
   std::string Toolbox::GenerateUuid()
   {
 #ifdef WIN32
--- a/Core/Toolbox.h	Wed Jun 20 18:02:36 2018 +0200
+++ b/Core/Toolbox.h	Wed Jun 20 18:03:20 2018 +0200
@@ -233,6 +233,10 @@
     std::string ToUpperCaseWithAccents(const std::string& source);
 #endif
 
+    void InitializeOpenSsl();
+    
+    void FinalizeOpenSsl();
+
     std::string GenerateUuid();
   }
 }
--- a/Core/WebServiceParameters.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/Core/WebServiceParameters.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -34,8 +34,9 @@
 #include "PrecompiledHeaders.h"
 #include "WebServiceParameters.h"
 
-#include "../Core/Logging.h"
-#include "../Core/OrthancException.h"
+#include "Logging.h"
+#include "OrthancException.h"
+#include "SerializationToolbox.h"
 
 #if ORTHANC_SANDBOXED == 0
 #  include "../Core/SystemToolbox.h"
@@ -129,6 +130,11 @@
       SetUsername("");
       SetPassword("");
     }
+    else if (peer.size() == 2)
+    {
+      LOG(ERROR) << "The HTTP password is not provided";
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
     else if (peer.size() == 3)
     {
       SetUsername(peer.get(1u, "").asString());
@@ -177,12 +183,25 @@
     SetUsername(GetStringMember(peer, "Username", ""));
     SetPassword(GetStringMember(peer, "Password", ""));
 
+    if (!username_.empty() &&
+        !peer.isMember("Password"))
+    {
+      LOG(ERROR) << "The HTTP password is not provided";
+      throw OrthancException(ErrorCode_BadFileFormat);      
+    }
+
 #if ORTHANC_SANDBOXED == 0
     if (peer.isMember("CertificateFile"))
     {
       SetClientCertificate(GetStringMember(peer, "CertificateFile", ""),
                            GetStringMember(peer, "CertificateKeyFile", ""),
                            GetStringMember(peer, "CertificateKeyPassword", ""));
+
+      if (!peer.isMember("CertificateKeyPassword"))
+      {
+        LOG(ERROR) << "The password for the HTTPS certificate is not provided";
+        throw OrthancException(ErrorCode_BadFileFormat);      
+      }
     }
 #endif
 
@@ -228,7 +247,8 @@
   }
 
 
-  void WebServiceParameters::ToJson(Json::Value& value) const
+  void WebServiceParameters::ToJson(Json::Value& value,
+                                    bool includePasswords) const
   {
     if (advancedFormat_)
     {
@@ -239,7 +259,11 @@
           !password_.empty())
       {
         value["Username"] = username_;
-        value["Password"] = password_;
+
+        if (includePasswords)
+        {
+          value["Password"] = password_;
+        }
       }
 
       if (!certificateFile_.empty())
@@ -252,7 +276,8 @@
         value["CertificateKeyFile"] = certificateKeyFile_;
       }
 
-      if (!certificateKeyPassword_.empty())
+      if (!certificateKeyPassword_.empty() &&
+          includePasswords)
       {
         value["CertificateKeyPassword"] = certificateKeyPassword_;
       }
@@ -266,8 +291,48 @@
           !password_.empty())
       {
         value.append(username_);
-        value.append(password_);
+
+        if (includePasswords)
+        {
+          value.append(password_);
+        }
       }
     }
   }
+
+  
+  void WebServiceParameters::Serialize(Json::Value& target) const
+  {
+    target = Json::objectValue;
+    target["URL"] = url_;
+    target["Username"] = username_;
+    target["Password"] = password_;
+    target["CertificateFile"] = certificateFile_;
+    target["CertificateKeyFile"] = certificateKeyFile_;
+    target["CertificateKeyPassword"] = certificateKeyPassword_;
+    target["PKCS11"] = pkcs11Enabled_;
+    target["AdvancedFormat"] = advancedFormat_;
+  }
+
+  
+  WebServiceParameters::WebServiceParameters(const Json::Value& serialized) :
+    advancedFormat_(true)
+  {
+    url_ = SerializationToolbox::ReadString(serialized, "URL");
+    username_ = SerializationToolbox::ReadString(serialized, "Username");
+    password_ = SerializationToolbox::ReadString(serialized, "Password");
+
+    std::string a, b, c;
+    a = SerializationToolbox::ReadString(serialized, "CertificateFile");
+    b = SerializationToolbox::ReadString(serialized, "CertificateKeyFile");
+    c = SerializationToolbox::ReadString(serialized, "CertificateKeyPassword");
+
+    if (!a.empty())
+    {
+      SetClientCertificate(a, b, c);
+    }
+    
+    pkcs11Enabled_ = SerializationToolbox::ReadBoolean(serialized, "PKCS11");
+    advancedFormat_ = SerializationToolbox::ReadBoolean(serialized, "AdvancedFormat");
+  }
 }
--- a/Core/WebServiceParameters.h	Wed Jun 20 18:02:36 2018 +0200
+++ b/Core/WebServiceParameters.h	Wed Jun 20 18:03:20 2018 +0200
@@ -61,6 +61,8 @@
   public:
     WebServiceParameters();
 
+    WebServiceParameters(const Json::Value& serialized);
+
     const std::string& GetUrl() const
     {
       return url_;
@@ -126,6 +128,9 @@
 
     void FromJson(const Json::Value& peer);
 
-    void ToJson(Json::Value& value) const;
+    void ToJson(Json::Value& value,
+                bool includePasswords) const;
+
+    void Serialize(Json::Value& target) const;
   };
 }
--- a/NEWS	Wed Jun 20 18:02:36 2018 +0200
+++ b/NEWS	Wed Jun 20 18:03:20 2018 +0200
@@ -1,6 +1,15 @@
 Pending changes in the mainline
 ===============================
 
+General
+-------
+
+* New advanced job engine
+* New configuration options:
+  - "ConcurrentJobs": Max number of jobs that are simultanously running
+  - "SynchronousCMove": Whether to run DICOM C-Move operations synchronously
+  - "JobsHistorySize": Max number of completed jobs that are kept in memory
+
 Orthanc Explorer
 ----------------
 
@@ -9,6 +18,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 +37,7 @@
 -----------
 
 * Fix generation of DICOMDIR if PatientID is empty
+* Fix issue 25 (Deadlock with Lua scripts): The event queue is now implemented for Lua
 * Upgraded dependencies for static and Windows builds:
   - boost 1.67.0
 
--- a/OrthancExplorer/explorer.html	Wed Jun 20 18:02:36 2018 +0200
+++ b/OrthancExplorer/explorer.html	Wed Jun 20 18:03:20 2018 +0200
@@ -38,12 +38,13 @@
       <div data-role="header" >
 	<h1><span class="orthanc-name"></span>Find a patient</h1>
         <div data-type="horizontal" data-role="controlgroup" class="ui-btn-left">
+          <a href="#find-studies" data-icon="arrow-r" data-role="button" data-direction="reverse">Studies</a>
           <a href="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</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="#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">
@@ -56,12 +57,13 @@
       <div data-role="header" >
 	<h1><span class="orthanc-name"></span>Find a study</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="#plugins" data-icon="grid" data-role="button" data-direction="reverse">Plugins</a>
-          <a href="#find-patients" data-icon="home" data-role="button" data-direction="reverse">Patients</a>
         </div>
         <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 Jun 20 18:02:36 2018 +0200
+++ b/OrthancExplorer/explorer.js	Wed Jun 20 18:03:20 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.Content.LocalAet);
+        AddJobField(item, 'Remote AET: ', job.Content.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 != 'Content' &&
+                     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.Content) {
+          var value = job.Content[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');
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/DicomInstanceOrigin.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,195 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PrecompiledHeadersServer.h"
+#include "DicomInstanceOrigin.h"
+
+#include "../Core/OrthancException.h"
+#include "../Core/SerializationToolbox.h"
+
+
+namespace Orthanc
+{
+  void DicomInstanceOrigin::Format(Json::Value& result) const
+  {
+    result = Json::objectValue;
+    result["RequestOrigin"] = EnumerationToString(origin_);
+
+    switch (origin_)
+    {
+      case RequestOrigin_Unknown:
+      {
+        // None of the methods "SetDicomProtocolOrigin()", "SetHttpOrigin()",
+        // "SetLuaOrigin()" or "SetPluginsOrigin()" was called!
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+
+      case RequestOrigin_DicomProtocol:
+      {
+        result["RemoteIp"] = remoteIp_;
+        result["RemoteAet"] = dicomRemoteAet_;
+        result["CalledAet"] = dicomCalledAet_;
+        break;
+      }
+
+      case RequestOrigin_RestApi:
+      {
+        result["RemoteIp"] = remoteIp_;
+        result["Username"] = httpUsername_;
+        break;
+      }
+
+      case RequestOrigin_Lua:
+      case RequestOrigin_Plugins:
+      {
+        // No additional information available for these kinds of requests
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+  }
+
+
+  DicomInstanceOrigin DicomInstanceOrigin::FromDicomProtocol(const char* remoteIp,
+                                                             const char* remoteAet,
+                                                             const char* calledAet)
+  {
+    DicomInstanceOrigin result(RequestOrigin_DicomProtocol);
+    result.remoteIp_ = remoteIp;
+    result.dicomRemoteAet_ = remoteAet;
+    result.dicomCalledAet_ = calledAet;
+    return result;
+  }
+
+  DicomInstanceOrigin DicomInstanceOrigin::FromRest(const RestApiCall& call)
+  {
+    DicomInstanceOrigin result(call.GetRequestOrigin());
+
+    if (result.origin_ == RequestOrigin_RestApi)
+    {
+      result.remoteIp_ = call.GetRemoteIp();
+      result.httpUsername_ = call.GetUsername();
+    }
+
+    return result;
+  }
+
+  DicomInstanceOrigin DicomInstanceOrigin::FromHttp(const char* remoteIp,
+                                                    const char* username)
+  {
+    DicomInstanceOrigin result(RequestOrigin_RestApi);
+    result.remoteIp_ = remoteIp;
+    result.httpUsername_ = username;
+    return result;
+  }
+
+  const char* DicomInstanceOrigin::GetRemoteAetC() const
+  {
+    if (origin_ == RequestOrigin_DicomProtocol)
+    {
+      return dicomRemoteAet_.c_str();
+    }
+    else
+    {
+      return "";
+    }
+  }
+
+  const std::string& DicomInstanceOrigin::GetRemoteIp() const
+  {
+    if (origin_ == RequestOrigin_DicomProtocol ||
+        origin_ == RequestOrigin_RestApi)
+    {
+      return remoteIp_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  const std::string& DicomInstanceOrigin::GetCalledAet() const
+  {
+    if (origin_ == RequestOrigin_DicomProtocol)
+    {
+      return dicomCalledAet_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+  const std::string& DicomInstanceOrigin::GetHttpUsername() const
+  {
+    if (origin_ == RequestOrigin_RestApi)
+    {
+      return httpUsername_;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+
+  static const char* ORIGIN = "Origin";
+  static const char* REMOTE_IP = "RemoteIP";
+  static const char* DICOM_REMOTE_AET = "RemoteAET";
+  static const char* DICOM_CALLED_AET = "CalledAET";
+  static const char* HTTP_USERNAME = "Username";
+  
+
+  DicomInstanceOrigin::DicomInstanceOrigin(const Json::Value& serialized)
+  {
+    origin_ = StringToRequestOrigin(SerializationToolbox::ReadString(serialized, ORIGIN));
+    remoteIp_ = SerializationToolbox::ReadString(serialized, REMOTE_IP);
+    dicomRemoteAet_ = SerializationToolbox::ReadString(serialized, DICOM_REMOTE_AET);
+    dicomCalledAet_ = SerializationToolbox::ReadString(serialized, DICOM_CALLED_AET);
+    httpUsername_ = SerializationToolbox::ReadString(serialized, HTTP_USERNAME);
+  }
+  
+  
+  void DicomInstanceOrigin::Serialize(Json::Value& result) const
+  {
+    result = Json::objectValue;
+    result[ORIGIN] = EnumerationToString(origin_);
+    result[REMOTE_IP] = remoteIp_;
+    result[DICOM_REMOTE_AET] = dicomRemoteAet_;
+    result[DICOM_CALLED_AET] = dicomCalledAet_;
+    result[HTTP_USERNAME] = httpUsername_;
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/DicomInstanceOrigin.h	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,98 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "../Core/RestApi/RestApiCall.h"
+
+namespace Orthanc
+{
+  class DicomInstanceOrigin
+  {
+  private:
+    RequestOrigin origin_;
+    std::string   remoteIp_;
+    std::string   dicomRemoteAet_;
+    std::string   dicomCalledAet_;
+    std::string   httpUsername_;
+
+    DicomInstanceOrigin(RequestOrigin origin) :
+      origin_(origin)
+    {
+    }
+
+  public:
+    DicomInstanceOrigin() :
+      origin_(RequestOrigin_Unknown)
+    {
+    }
+
+    DicomInstanceOrigin(const Json::Value& serialized);
+
+    static DicomInstanceOrigin FromDicomProtocol(const char* remoteIp,
+                                                 const char* remoteAet,
+                                                 const char* calledAet);
+
+    static DicomInstanceOrigin FromRest(const RestApiCall& call);
+
+    static DicomInstanceOrigin FromHttp(const char* remoteIp,
+                                        const char* username);
+
+    static DicomInstanceOrigin FromLua()
+    {
+      return DicomInstanceOrigin(RequestOrigin_Lua);
+    }
+
+    static DicomInstanceOrigin FromPlugins()
+    {
+      return DicomInstanceOrigin(RequestOrigin_Plugins);
+    }
+
+    RequestOrigin GetRequestOrigin() const
+    {
+      return origin_;
+    }
+
+    const char* GetRemoteAetC() const; 
+
+    const std::string& GetRemoteIp() const;
+    
+    const std::string& GetCalledAet() const; 
+
+    const std::string& GetHttpUsername() const; 
+
+    void Format(Json::Value& result) const;
+
+    void Serialize(Json::Value& result) const;
+  };
+}
--- a/OrthancServer/DicomInstanceToStore.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/DicomInstanceToStore.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -181,101 +181,6 @@
   }
 
 
-
-  void DicomInstanceToStore::GetOriginInformation(Json::Value& result) const
-  {
-    result = Json::objectValue;
-    result["RequestOrigin"] = EnumerationToString(origin_);
-
-    switch (origin_)
-    {
-      case RequestOrigin_Unknown:
-      {
-        // None of the methods "SetDicomProtocolOrigin()", "SetHttpOrigin()",
-        // "SetLuaOrigin()" or "SetPluginsOrigin()" was called!
-        throw OrthancException(ErrorCode_BadSequenceOfCalls);
-      }
-
-      case RequestOrigin_DicomProtocol:
-      {
-        result["RemoteIp"] = remoteIp_;
-        result["RemoteAet"] = dicomRemoteAet_;
-        result["CalledAet"] = dicomCalledAet_;
-        break;
-      }
-
-      case RequestOrigin_RestApi:
-      {
-        result["RemoteIp"] = remoteIp_;
-        result["Username"] = httpUsername_;
-        break;
-      }
-
-      case RequestOrigin_Lua:
-      case RequestOrigin_Plugins:
-      {
-        // No additional information available for these kinds of requests
-        break;
-      }
-
-      default:
-        throw OrthancException(ErrorCode_InternalError);
-    }
-  }
-
-
-  void DicomInstanceToStore::SetDicomProtocolOrigin(const char* remoteIp,
-                                                    const char* remoteAet,
-                                                    const char* calledAet)
-  {
-    origin_ = RequestOrigin_DicomProtocol;
-    remoteIp_ = remoteIp;
-    dicomRemoteAet_ = remoteAet;
-    dicomCalledAet_ = calledAet;
-  }
-
-  void DicomInstanceToStore::SetRestOrigin(const RestApiCall& call)
-  {
-    origin_ = call.GetRequestOrigin();
-
-    if (origin_ == RequestOrigin_RestApi)
-    {
-      remoteIp_ = call.GetRemoteIp();
-      httpUsername_ = call.GetUsername();
-    }
-  }
-
-  void DicomInstanceToStore::SetHttpOrigin(const char* remoteIp,
-                                           const char* username)
-  {
-    origin_ = RequestOrigin_RestApi;
-    remoteIp_ = remoteIp;
-    httpUsername_ = username;
-  }
-
-  void DicomInstanceToStore::SetLuaOrigin()
-  {
-    origin_ = RequestOrigin_Lua;
-  }
-
-  void DicomInstanceToStore::SetPluginsOrigin()
-  {
-    origin_ = RequestOrigin_Plugins;
-  }
-
-  const char* DicomInstanceToStore::GetRemoteAet() const
-  {
-    if (origin_ == RequestOrigin_DicomProtocol)
-    {
-      return dicomRemoteAet_.c_str();
-    }
-    else
-    {
-      return "";
-    }
-  }
-
-
   bool DicomInstanceToStore::LookupTransferSyntax(std::string& result)
   {
     ComputeMissingInformation();
--- a/OrthancServer/DicomInstanceToStore.h	Wed Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/DicomInstanceToStore.h	Wed Jun 20 18:03:20 2018 +0200
@@ -34,9 +34,9 @@
 #pragma once
 
 #include "../Core/DicomParsing/ParsedDicomFile.h"
+#include "../Core/OrthancException.h"
+#include "DicomInstanceOrigin.h"
 #include "ServerIndex.h"
-#include "../Core/OrthancException.h"
-#include "../Core/RestApi/RestApiCall.h"
 
 namespace Orthanc
 {
@@ -139,46 +139,26 @@
       }
     };
 
-
-    SmartContainer<std::string>  buffer_;
+    DicomInstanceOrigin              origin_;
+    SmartContainer<std::string>      buffer_;
     SmartContainer<ParsedDicomFile>  parsed_;
-    SmartContainer<DicomMap>  summary_;
-    SmartContainer<Json::Value>  json_;
-
-    RequestOrigin origin_;
-    std::string remoteIp_;
-    std::string dicomRemoteAet_;
-    std::string dicomCalledAet_;
-    std::string httpUsername_;
-    ServerIndex::MetadataMap metadata_;
+    SmartContainer<DicomMap>         summary_;
+    SmartContainer<Json::Value>      json_;
+    ServerIndex::MetadataMap         metadata_;
 
     void ComputeMissingInformation();
 
   public:
-    DicomInstanceToStore() : origin_(RequestOrigin_Unknown)
+    void SetOrigin(const DicomInstanceOrigin& origin)
     {
+      origin_ = origin;
     }
-
-    void SetDicomProtocolOrigin(const char* remoteIp,
-                                const char* remoteAet,
-                                const char* calledAet);
-
-    void SetRestOrigin(const RestApiCall& call);
-
-    void SetHttpOrigin(const char* remoteIp,
-                       const char* username);
-
-    void SetLuaOrigin();
-
-    void SetPluginsOrigin();
-
-    RequestOrigin GetRequestOrigin() const
+    
+    const DicomInstanceOrigin& GetOrigin() const
     {
       return origin_;
     }
-
-    const char* GetRemoteAet() const; 
-
+    
     void SetBuffer(const std::string& dicom)
     {
       buffer_.SetConstReference(dicom);
@@ -221,8 +201,6 @@
     
     const Json::Value& GetJson();
 
-    void GetOriginInformation(Json::Value& result) const;
-
     bool LookupTransferSyntax(std::string& result);
   };
 }
--- a/OrthancServer/IServerListener.h	Wed Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/IServerListener.h	Wed Jun 20 18:03:20 2018 +0200
@@ -40,7 +40,7 @@
 
 namespace Orthanc
 {
-  class IServerListener
+  class IServerListener : public boost::noncopyable
   {
   public:
     virtual ~IServerListener()
--- a/OrthancServer/LuaScripting.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/LuaScripting.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -34,24 +34,217 @@
 #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.GetOrigin().Format(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);
+      }
+
+      {
+        // Avoid unnecessary calls to the database if there's no Lua callback
+        LuaScripting::Lock lock(that);
+
+        if (!lock.GetLua().IsExistingFunction(name))
+        {
+          return;
+        }
+      }
+      
+      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();
+        }
+      }
+    }
+  };
+
+
+  class LuaScripting::JobEvent : public LuaScripting::IEvent
+  {
+  public:
+    enum Type
+    {
+      Type_Failure,
+      Type_Submitted,
+      Type_Success
+    };
+    
+  private:
+    Type         type_;
+    std::string  jobId_;
+
+  public:
+    JobEvent(Type type,
+             const std::string& jobId) :
+      type_(type),
+      jobId_(jobId)
+    {
+    }
+
+    virtual void Apply(LuaScripting& that)
+    {
+      std::string functionName;
+      
+      switch (type_)
+      {
+        case Type_Failure:
+          functionName = "OnJobFailure";
+          break;
+
+        case Type_Submitted:
+          functionName = "OnJobSubmitted";
+          break;
+
+        case Type_Success:
+          functionName = "OnJobSuccess";
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      {
+        LuaScripting::Lock lock(that);
+
+        if (lock.GetLua().IsExistingFunction(functionName.c_str()))
+        {
+          LuaFunctionCall call(lock.GetLua(), functionName.c_str());
+          call.PushString(jobId_);
+          call.Execute();
+        }
+      }
+    }
+  };
+
+
   ServerContext* LuaScripting::GetServerContext(lua_State *state)
   {
     const void* value = LuaContext::GetGlobalVariable(state, "_ServerContext");
@@ -211,7 +404,7 @@
     }
 
     LOG(ERROR) << "Lua: Error in RestApiDelete() for URI: " << uri;
-      lua_pushnil(state);
+    lua_pushnil(state);
 
     return 1;
   }
@@ -229,13 +422,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 +444,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 +507,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 +523,7 @@
   }
 
 
-  void LuaScripting::SubmitJob(const std::string& description)
+  void LuaScripting::SubmitJob()
   {
     Json::Value operations;
     LuaFunctionCall call2(lua_, "_AccessJob");
@@ -344,8 +534,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 +548,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),
+    state_(State_Setup)
   {
     lua_.SetGlobalVariable("_ServerContext", &context);
     lua_.RegisterFunction("RestApiGet", RestApiGet);
@@ -391,34 +582,88 @@
     lua_.RegisterFunction("RestApiPut", RestApiPut);
     lua_.RegisterFunction("RestApiDelete", RestApiDelete);
     lua_.RegisterFunction("GetOrthancConfiguration", GetOrthancConfiguration);
+  }
 
-    lua_.Execute(Orthanc::EmbeddedResources::LUA_TOOLBOX);
+
+  LuaScripting::~LuaScripting()
+  {
+    if (state_ == State_Running)
+    {
+      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->state_ != State_Running)
+        {
+          return;
+        }
+      }
+      else
+      {
+        try
+        {
+          dynamic_cast<IEvent&>(*event).Apply(*that);
+        }
+        catch (OrthancException& e)
+        {
+          LOG(ERROR) << "Error while processing Lua events: " << e.What();
+        }
+      }
+    }
+  }
+
+
+  void LuaScripting::Start()
+  {
+    boost::recursive_mutex::scoped_lock lock(mutex_);
 
-      LuaFunctionCall call(lua_, NAME);
-      call.PushString(instanceId);
-      call.PushJson(simplifiedTags);
-      call.PushJson(metadata);
+    if (state_ != State_Setup ||
+        eventThread_.joinable()  /* already started */)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      LOG(INFO) << "Starting the Lua engine";
+      eventThread_ = boost::thread(EventThread, this);
+      state_ = State_Running;
+    }
+  }
+
 
-      Json::Value origin;
-      instance.GetOriginInformation(origin);
-      call.PushJson(origin);
+  void LuaScripting::Stop()
+  {
+    {
+      boost::recursive_mutex::scoped_lock lock(mutex_);
+
+      if (state_ != State_Running)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
 
-      call.Execute();
+      state_ = State_Done;
+    }
+
+    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 +672,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,72 +684,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);
-    }
-
-    {
-      // Avoid unnecessary calls to the database if there's no Lua callback
-      boost::recursive_mutex::scoped_lock lock(mutex_);
-      if (!lua_.IsExistingFunction(name))
-      {
-        return;
-      }
-    }
-    
-    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));
     }
   }
 
@@ -524,7 +712,7 @@
       call.PushJson(simplified);
 
       Json::Value origin;
-      instance.GetOriginInformation(origin);
+      instance.GetOrigin().Format(origin);
       call.PushJson(origin);
 
       if (!call.ExecutePredicate())
@@ -539,12 +727,46 @@
 
   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);
     }
   }
+
+  
+  void LuaScripting::SignalJobSubmitted(const std::string& jobId)
+  {
+    pendingEvents_.Enqueue(new JobEvent(JobEvent::Type_Submitted, jobId));
+  }
+  
+
+  void LuaScripting::SignalJobSuccess(const std::string& jobId)
+  {
+    pendingEvents_.Enqueue(new JobEvent(JobEvent::Type_Success, jobId));
+  }
+  
+
+  void LuaScripting::SignalJobFailure(const std::string& jobId)
+  {
+    pendingEvents_.Enqueue(new JobEvent(JobEvent::Type_Failure, jobId));
+  }
 }
--- a/OrthancServer/LuaScripting.h	Wed Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/LuaScripting.h	Wed Jun 20 18:03:20 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,19 @@
   class LuaScripting : public IServerListener
   {
   private:
+    enum State
+    {
+      State_Setup,
+      State_Running,
+      State_Done
+    };
+    
+    class ExecuteEvent;
+    class IEvent;
+    class OnStoredInstanceEvent;
+    class StableResourceEvent;
+    class JobEvent;
+
     static ServerContext* GetServerContext(lua_State *state);
 
     static int RestApiPostOrPut(lua_State *state,
@@ -54,33 +70,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_;
+    State                    state_;
+    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 +109,12 @@
     };
 
     LuaScripting(ServerContext& context);
+
+    ~LuaScripting();
+
+    void Start();
+
+    void Stop();
     
     virtual void SignalStoredInstance(const std::string& publicId,
                                       DicomInstanceToStore& instance,
@@ -104,5 +126,13 @@
                                         const Json::Value& simplifiedTags);
 
     void Execute(const std::string& command);
+
+    void LoadGlobalConfiguration();
+
+    void SignalJobSubmitted(const std::string& jobId);
+
+    void SignalJobSuccess(const std::string& jobId);
+
+    void SignalJobFailure(const std::string& jobId);
   };
 }
--- a/OrthancServer/OrthancFindRequestHandler.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/OrthancFindRequestHandler.cpp	Wed Jun 20 18:03:20 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 Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/OrthancFindRequestHandler.h	Wed Jun 20 18:03:20 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/OrthancInitialization.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/OrthancInitialization.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -458,7 +458,7 @@
   {
     boost::recursive_mutex::scoped_lock lock(globalMutex_);
 
-    HttpClient::InitializeOpenSsl();
+    Toolbox::InitializeOpenSsl();
 
     InitializeServerEnumerations();
 
@@ -514,7 +514,7 @@
     boost::recursive_mutex::scoped_lock lock(globalMutex_);
     HttpClient::GlobalFinalize();
     FromDcmtkBridge::FinalizeCodecs();
-    HttpClient::FinalizeOpenSsl();
+    Toolbox::FinalizeOpenSsl();
     Toolbox::FinalizeGlobalLocale();
   }
 
@@ -960,7 +960,7 @@
     peers.removeMember(symbolicName);
 
     Json::Value v;
-    peer.ToJson(v);
+    peer.ToJson(v, true);
     peers[symbolicName] = v;
   }
   
--- a/OrthancServer/OrthancMoveRequestHandler.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/OrthancMoveRequestHandler.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -38,6 +38,7 @@
 #include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../Core/DicomFormat/DicomArray.h"
 #include "../Core/Logging.h"
+#include "ServerJobs/DicomModalityStoreJob.h"
 
 namespace Orthanc
 {
@@ -45,7 +46,7 @@
   {
     // Anonymous namespace to avoid clashes between compilation modules
 
-    class OrthancMoveRequestIterator : public IMoveRequestIterator
+    class SynchronousMove : public IMoveRequestIterator
     {
     private:
       ServerContext& context_;
@@ -55,20 +56,21 @@
       RemoteModalityParameters remote_;
       std::string originatorAet_;
       uint16_t originatorId_;
+      std::auto_ptr<DicomUserConnection> connection_;
 
     public:
-      OrthancMoveRequestIterator(ServerContext& context,
-                                 const std::string& aet,
-                                 const std::string& publicId,
-                                 const std::string& originatorAet,
-                                 uint16_t originatorId) :
+      SynchronousMove(ServerContext& context,
+                      const std::string& targetAet,
+                      const std::string& publicId,
+                      const std::string& originatorAet,
+                      uint16_t originatorId) :
         context_(context),
         localAet_(context.GetDefaultLocalApplicationEntityTitle()),
         position_(0),
         originatorAet_(originatorAet),
         originatorId_(originatorId)
       {
-        LOG(INFO) << "Sending resource " << publicId << " to modality \"" << aet << "\"";
+        LOG(INFO) << "Sending resource " << publicId << " to modality \"" << targetAet << "\"";
 
         std::list<std::string> tmp;
         context_.GetIndex().GetChildInstances(tmp, publicId);
@@ -79,7 +81,7 @@
           instances_.push_back(*it);
         }
 
-        remote_ = Configuration::GetModalityUsingAet(aet);
+        remote_ = Configuration::GetModalityUsingAet(targetAet);
       }
 
       virtual unsigned int GetSubOperationCount() const
@@ -99,15 +101,76 @@
         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;
       }
     };
+
+
+    class AsynchronousMove : public IMoveRequestIterator
+    {
+    private:
+      ServerContext&                        context_;
+      std::auto_ptr<DicomModalityStoreJob>  job_;
+      size_t                                position_;
+      
+    public:
+      AsynchronousMove(ServerContext& context,
+                       const std::string& targetAet,
+                       const std::string& publicId,
+                       const std::string& originatorAet,
+                       uint16_t originatorId) :
+        context_(context),
+        job_(new DicomModalityStoreJob(context)),
+        position_(0)
+      {
+        LOG(INFO) << "Sending resource " << publicId << " to modality \"" << targetAet << "\"";
+
+        job_->SetDescription("C-MOVE");
+        job_->SetPermissive(true);
+        job_->SetLocalAet(context.GetDefaultLocalApplicationEntityTitle());
+        job_->SetRemoteModality(Configuration::GetModalityUsingAet(targetAet));
+
+        if (originatorId != 0)
+        {
+          job_->SetMoveOriginator(originatorAet, originatorId);
+        }
+        
+        std::list<std::string> tmp;
+        context_.GetIndex().GetChildInstances(tmp, publicId);
+
+        job_->Reserve(tmp.size());
+
+        for (std::list<std::string>::iterator it = tmp.begin(); it != tmp.end(); ++it)
+        {
+          job_->AddInstance(*it);
+        }
+      }
+
+      virtual unsigned int GetSubOperationCount() const
+      {
+        return 1;
+      }
+
+      virtual Status DoNext()
+      {
+        if (position_ == 0)
+        {
+          context_.GetJobsEngine().GetRegistry().Submit(job_.release(), 0 /* priority */);
+          return Status_Success;
+        }
+        else
+        {
+          return Status_Failure;
+        }
+      }
+    };
   }
 
 
@@ -169,6 +232,25 @@
   }
 
 
+  static IMoveRequestIterator* CreateIterator(ServerContext& context,
+                                              const std::string& targetAet,
+                                              const std::string& publicId,
+                                              const std::string& originatorAet,
+                                              uint16_t originatorId)
+  {
+    bool synchronous = Configuration::GetGlobalBoolParameter("SynchronousCMove", false);
+
+    if (synchronous)
+    {
+      return new SynchronousMove(context, targetAet, publicId, originatorAet, originatorId);
+    }
+    else
+    {
+      return new AsynchronousMove(context, targetAet, publicId, originatorAet, originatorId);
+    }
+  }
+
+
   IMoveRequestIterator* OrthancMoveRequestHandler::Handle(const std::string& targetAet,
                                                           const DicomMap& input,
                                                           const std::string& originatorIp,
@@ -191,7 +273,6 @@
       }
     }
 
-
     /**
      * Retrieve the query level.
      **/
@@ -215,7 +296,7 @@
           LookupIdentifier(publicId, ResourceType_Study, input) ||
           LookupIdentifier(publicId, ResourceType_Patient, input))
       {
-        return new OrthancMoveRequestIterator(context_, targetAet, publicId, originatorAet, originatorId);
+        return CreateIterator(context_, targetAet, publicId, originatorAet, originatorId);
       }
       else
       {
@@ -236,7 +317,7 @@
 
     if (LookupIdentifier(publicId, level, input))
     {
-      return new OrthancMoveRequestIterator(context_, targetAet, publicId, originatorAet, originatorId);
+      return CreateIterator(context_, targetAet, publicId, originatorAet, originatorId);
     }
     else
     {
--- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -34,10 +34,10 @@
 #include "../PrecompiledHeadersServer.h"
 #include "OrthancRestApi.h"
 
+#include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../../Core/Logging.h"
-#include "../../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../ServerContext.h"
-#include "../OrthancInitialization.h"
+#include "../ServerJobs/ResourceModificationJob.h"
 
 #include <boost/lexical_cast.hpp>
 #include <boost/algorithm/string/predicate.hpp>
@@ -54,14 +54,39 @@
   }
 
 
+  static int GetPriority(const Json::Value& request)
+  {
+    static const char* PRIORITY = "Priority";
+    
+    if (request.isMember(PRIORITY))
+    {
+      if (request[PRIORITY].type() == Json::intValue)
+      {
+        return request[PRIORITY].asInt();
+      }
+      else
+      {
+        LOG(ERROR) << "Field \"" << PRIORITY << "\" of a modification request should be an integer";
+        throw OrthancException(ErrorCode_BadFileFormat);        
+      }
+    }
+    else
+    {
+      return 0;   // Default priority
+    }
+  }
+
+
   static void ParseModifyRequest(DicomModification& target,
+                                 int& priority,
                                  const RestApiPostCall& call)
   {
-    // curl http://localhost:8042/series/95a6e2bf-9296e2cc-bf614e2f-22b391ee-16e010e0/modify -X POST -d '{"Replace":{"InstitutionName":"My own clinic"}}'
+    // curl http://localhost:8042/series/95a6e2bf-9296e2cc-bf614e2f-22b391ee-16e010e0/modify -X POST -d '{"Replace":{"InstitutionName":"My own clinic"},"Priority":9}'
 
     Json::Value request;
     if (call.ParseJsonRequest(request))
     {
+      priority = GetPriority(request);
       target.ParseModifyRequest(request);
     }
     else
@@ -72,6 +97,7 @@
 
 
   static void ParseAnonymizationRequest(DicomModification& target,
+                                        int& priority,
                                         RestApiPostCall& call)
   {
     // curl http://localhost:8042/instances/6e67da51-d119d6ae-c5667437-87b9a8a5-0f07c49f/anonymize -X POST -d '{"Replace":{"PatientName":"hello","0010-0020":"world"},"Keep":["StudyDescription", "SeriesDescription"],"KeepPrivateTags": true,"Remove":["Modality"]}' > Anonymized.dcm
@@ -80,6 +106,8 @@
     if (call.ParseJsonRequest(request) &&
         request.isObject())
     {
+      priority = GetPriority(request);
+
       bool patientNameReplaced;
       target.ParseAnonymizationRequest(patientNameReplaced, request);
 
@@ -110,142 +138,36 @@
   }
 
 
-  static void AnonymizeOrModifyResource(DicomModification& modification,
-                                        MetadataType metadataType,
-                                        ChangeType changeType,
-                                        ResourceType resourceType,
-                                        RestApiPostCall& call)
+
+  static void SubmitJob(std::auto_ptr<DicomModification>& modification,
+                        bool isAnonymization,
+                        ResourceType level,
+                        int priority,
+                        RestApiPostCall& call)
   {
-    bool isFirst = true;
-    Json::Value result(Json::objectValue);
-
     ServerContext& context = OrthancRestApi::GetContext(call);
 
-    typedef std::list<std::string> Instances;
-    Instances instances;
-    std::string id = call.GetUriComponent("id", "");
-    context.GetIndex().GetChildInstances(instances, id);
-
-    if (instances.empty())
-    {
-      return;
-    }
-
-
-    /**
-     * Loop over all the instances of the resource.
-     **/
-
-    for (Instances::const_iterator it = instances.begin(); 
-         it != instances.end(); ++it)
-    {
-      LOG(INFO) << "Modifying instance " << *it;
-
-      std::auto_ptr<ServerContext::DicomCacheLocker> locker;
-
-      try
-      {
-        locker.reset(new ServerContext::DicomCacheLocker(OrthancRestApi::GetContext(call), *it));
-      }
-      catch (OrthancException&)
-      {
-        // This child instance has been removed in between
-        continue;
-      }
-
-
-      ParsedDicomFile& original = locker->GetDicom();
-      DicomInstanceHasher originalHasher = original.GetHasher();
-
-
-      /**
-       * Compute the resulting DICOM instance.
-       **/
-
-      std::auto_ptr<ParsedDicomFile> modified(original.Clone(true));
-      modification.Apply(*modified);
-
-      DicomInstanceToStore toStore;
-      toStore.SetRestOrigin(call);
-      toStore.SetParsedDicomFile(*modified);
-
-
-      /**
-       * Prepare the metadata information to associate with the
-       * resulting DICOM instance (AnonymizedFrom/ModifiedFrom).
-       **/
-
-      DicomInstanceHasher modifiedHasher = modified->GetHasher();
-
-      if (originalHasher.HashSeries() != modifiedHasher.HashSeries())
-      {
-        toStore.AddMetadata(ResourceType_Series, metadataType, originalHasher.HashSeries());
-      }
+    std::auto_ptr<ResourceModificationJob> job(new ResourceModificationJob(context));
+    
+    boost::shared_ptr<ResourceModificationJob::Output> output(new ResourceModificationJob::Output(level));
+    job->SetModification(modification.release(), isAnonymization);
+    job->SetOutput(output);
+    job->SetOrigin(call);
+    job->SetDescription("REST API");
 
-      if (originalHasher.HashStudy() != modifiedHasher.HashStudy())
-      {
-        toStore.AddMetadata(ResourceType_Study, metadataType, originalHasher.HashStudy());
-      }
-
-      if (originalHasher.HashPatient() != modifiedHasher.HashPatient())
-      {
-        toStore.AddMetadata(ResourceType_Patient, metadataType, originalHasher.HashPatient());
-      }
-
-      assert(*it == originalHasher.HashInstance());
-      toStore.AddMetadata(ResourceType_Instance, metadataType, *it);
-
-
-      /**
-       * Store the resulting DICOM instance into the Orthanc store.
-       **/
-
-      std::string modifiedInstance;
-      if (context.Store(modifiedInstance, toStore) != StoreStatus_Success)
+    context.AddChildInstances(*job, call.GetUriComponent("id", ""));
+    
+    if (context.GetJobsEngine().GetRegistry().SubmitAndWait(job.release(), priority))
+    {
+      Json::Value json;
+      if (output->Format(json))
       {
-        LOG(ERROR) << "Error while storing a modified instance " << *it;
-        throw OrthancException(ErrorCode_CannotStoreInstance);
-      }
-
-      // Sanity checks in debug mode
-      assert(modifiedInstance == modifiedHasher.HashInstance());
-
-
-      /**
-       * Compute the JSON object that is returned by the REST call.
-       **/
-
-      if (isFirst)
-      {
-        std::string newId;
-
-        switch (resourceType)
-        {
-          case ResourceType_Series:
-            newId = modifiedHasher.HashSeries();
-            break;
-
-          case ResourceType_Study:
-            newId = modifiedHasher.HashStudy();
-            break;
-
-          case ResourceType_Patient:
-            newId = modifiedHasher.HashPatient();
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_InternalError);
-        }
-
-        result["Type"] = EnumerationToString(resourceType);
-        result["ID"] = newId;
-        result["Path"] = GetBasePath(resourceType, newId);
-        result["PatientID"] = modifiedHasher.HashPatient();
-        isFirst = false;
+        call.GetOutput().AnswerJson(json);
+        return;
       }
     }
 
-    call.GetOutput().AnswerJson(result);
+    call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
   }
 
 
@@ -255,7 +177,8 @@
     DicomModification modification;
     modification.SetAllowManualIdentifiers(true);
 
-    ParseModifyRequest(modification, call);
+    int priority;
+    ParseModifyRequest(modification, priority, call);
 
     if (modification.IsReplaced(DICOM_TAG_PATIENT_ID))
     {
@@ -283,36 +206,35 @@
     DicomModification modification;
     modification.SetAllowManualIdentifiers(true);
 
-    ParseAnonymizationRequest(modification, call);
+    int priority;
+    ParseAnonymizationRequest(modification, priority, call);
 
     AnonymizeOrModifyInstance(modification, call);
   }
 
 
-  template <enum ChangeType changeType,
-            enum ResourceType resourceType>
+  template <enum ResourceType resourceType>
   static void ModifyResource(RestApiPostCall& call)
   {
-    DicomModification modification;
-
-    ParseModifyRequest(modification, call);
+    std::auto_ptr<DicomModification> modification(new DicomModification);
 
-    modification.SetLevel(resourceType);
-    AnonymizeOrModifyResource(modification, MetadataType_ModifiedFrom, 
-                              changeType, resourceType, call);
+    int priority;
+    ParseModifyRequest(*modification, priority, call);
+
+    modification->SetLevel(resourceType);
+    SubmitJob(modification, false, resourceType, priority, call);
   }
 
 
-  template <enum ChangeType changeType,
-            enum ResourceType resourceType>
+  template <enum ResourceType resourceType>
   static void AnonymizeResource(RestApiPostCall& call)
   {
-    DicomModification modification;
+    std::auto_ptr<DicomModification> modification(new DicomModification);
 
-    ParseAnonymizationRequest(modification, call);
+    int priority;
+    ParseAnonymizationRequest(*modification, priority, call);
 
-    AnonymizeOrModifyResource(modification, MetadataType_AnonymizedFrom, 
-                              changeType, resourceType, call);
+    SubmitJob(modification, true, resourceType, priority, call);
   }
 
 
@@ -321,7 +243,7 @@
                                    ParsedDicomFile& dicom)
   {
     DicomInstanceToStore toStore;
-    toStore.SetRestOrigin(call);
+    toStore.SetOrigin(DicomInstanceOrigin::FromRest(call));
     toStore.SetParsedDicomFile(dicom);
 
     ServerContext& context = OrthancRestApi::GetContext(call);
@@ -729,14 +651,14 @@
   void OrthancRestApi::RegisterAnonymizeModify()
   {
     Register("/instances/{id}/modify", ModifyInstance);
-    Register("/series/{id}/modify", ModifyResource<ChangeType_ModifiedSeries, ResourceType_Series>);
-    Register("/studies/{id}/modify", ModifyResource<ChangeType_ModifiedStudy, ResourceType_Study>);
-    Register("/patients/{id}/modify", ModifyResource<ChangeType_ModifiedPatient, ResourceType_Patient>);
+    Register("/series/{id}/modify", ModifyResource<ResourceType_Series>);
+    Register("/studies/{id}/modify", ModifyResource<ResourceType_Study>);
+    Register("/patients/{id}/modify", ModifyResource<ResourceType_Patient>);
 
     Register("/instances/{id}/anonymize", AnonymizeInstance);
-    Register("/series/{id}/anonymize", AnonymizeResource<ChangeType_AnonymizedSeries, ResourceType_Series>);
-    Register("/studies/{id}/anonymize", AnonymizeResource<ChangeType_AnonymizedStudy, ResourceType_Study>);
-    Register("/patients/{id}/anonymize", AnonymizeResource<ChangeType_AnonymizedPatient, ResourceType_Patient>);
+    Register("/series/{id}/anonymize", AnonymizeResource<ResourceType_Series>);
+    Register("/studies/{id}/anonymize", AnonymizeResource<ResourceType_Study>);
+    Register("/patients/{id}/anonymize", AnonymizeResource<ResourceType_Patient>);
 
     Register("/tools/create-dicom", CreateDicom);
   }
--- a/OrthancServer/OrthancRestApi/OrthancRestApi.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestApi.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -93,7 +93,7 @@
     std::string postData(call.GetBodyData(), call.GetBodySize());
 
     DicomInstanceToStore toStore;
-    toStore.SetRestOrigin(call);
+    toStore.SetOrigin(DicomInstanceOrigin::FromRest(call));
     toStore.SetBuffer(postData);
 
     std::string publicId;
--- a/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestArchive.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -34,651 +34,14 @@
 #include "../PrecompiledHeadersServer.h"
 #include "OrthancRestApi.h"
 
-#include "../../Core/DicomParsing/DicomDirWriter.h"
-#include "../../Core/FileStorage/StorageAccessor.h"
-#include "../../Core/Compression/HierarchicalZipWriter.h"
 #include "../../Core/HttpServer/FilesystemHttpSender.h"
-#include "../../Core/Logging.h"
-#include "../../Core/TemporaryFile.h"
-#include "../ServerContext.h"
-
-#include <stdio.h>
-
-#if defined(_MSC_VER)
-#define snprintf _snprintf
-#endif
-
-static const uint64_t MEGA_BYTES = 1024 * 1024;
-static const uint64_t GIGA_BYTES = 1024 * 1024 * 1024;
+#include "../ServerJobs/ArchiveJob.h"
 
 namespace Orthanc
 {
-  // Download of ZIP files ----------------------------------------------------
- 
-  static bool IsZip64Required(uint64_t uncompressedSize,
-                              unsigned int countInstances)
-  {
-    static const uint64_t  SAFETY_MARGIN = 64 * MEGA_BYTES;
-
-    /**
-     * Determine whether ZIP64 is required. Original ZIP format can
-     * store up to 2GB of data (some implementation supporting up to
-     * 4GB of data), and up to 65535 files.
-     * https://en.wikipedia.org/wiki/Zip_(file_format)#ZIP64
-     **/
-
-    const bool isZip64 = (uncompressedSize >= 2 * GIGA_BYTES - SAFETY_MARGIN ||
-                          countInstances >= 65535);
-
-    LOG(INFO) << "Creating a ZIP file with " << countInstances << " files of size "
-              << (uncompressedSize / MEGA_BYTES) << "MB using the "
-              << (isZip64 ? "ZIP64" : "ZIP32") << " file format";
-
-    return isZip64;
-  }
-
-
-  namespace
-  {
-    class ResourceIdentifiers
-    {
-    private:
-      ResourceType   level_;
-      std::string    patient_;
-      std::string    study_;
-      std::string    series_;
-      std::string    instance_;
-
-      static void GoToParent(ServerIndex& index,
-                             std::string& current)
-      {
-        std::string tmp;
-
-        if (index.LookupParent(tmp, current))
-        {
-          current = tmp;
-        }
-        else
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-      }
-
-
-    public:
-      ResourceIdentifiers(ServerIndex& index,
-                          const std::string& publicId)
-      {
-        if (!index.LookupResourceType(level_, publicId))
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-
-        std::string current = publicId;;
-        switch (level_)  // Do not add "break" below!
-        {
-          case ResourceType_Instance:
-            instance_ = current;
-            GoToParent(index, current);
-            
-          case ResourceType_Series:
-            series_ = current;
-            GoToParent(index, current);
-
-          case ResourceType_Study:
-            study_ = current;
-            GoToParent(index, current);
-
-          case ResourceType_Patient:
-            patient_ = current;
-            break;
-
-          default:
-            throw OrthancException(ErrorCode_InternalError);
-        }
-      }
-
-      ResourceType GetLevel() const
-      {
-        return level_;
-      }
-
-      const std::string& GetIdentifier(ResourceType level) const
-      {
-        // Some sanity check to ensure enumerations are not altered
-        assert(ResourceType_Patient < ResourceType_Study);
-        assert(ResourceType_Study < ResourceType_Series);
-        assert(ResourceType_Series < ResourceType_Instance);
-
-        if (level > level_)
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-
-        switch (level)
-        {
-          case ResourceType_Patient:
-            return patient_;
-
-          case ResourceType_Study:
-            return study_;
-
-          case ResourceType_Series:
-            return series_;
-
-          case ResourceType_Instance:
-            return instance_;
-
-          default:
-            throw OrthancException(ErrorCode_InternalError);
-        }
-      }
-    };
-
-
-    class IArchiveVisitor : public boost::noncopyable
-    {
-    public:
-      virtual ~IArchiveVisitor()
-      {
-      }
-
-      virtual void Open(ResourceType level,
-                        const std::string& publicId) = 0;
-
-      virtual void Close() = 0;
-
-      virtual void AddInstance(const std::string& instanceId,
-                               const FileInfo& dicom) = 0;
-    };
-
-
-    class ArchiveIndex
-    {
-    private:
-      struct Instance
-      {
-        std::string  id_;
-        FileInfo     dicom_;
-
-        Instance(const std::string& id,
-                 const FileInfo& dicom) : 
-          id_(id), dicom_(dicom)
-        {
-        }
-      };
-
-      // A "NULL" value for ArchiveIndex indicates a non-expanded node
-      typedef std::map<std::string, ArchiveIndex*>   Resources;
-
-      ResourceType         level_;
-      Resources            resources_;   // Only at patient/study/series level
-      std::list<Instance>  instances_;   // Only at instance level
-
-
-      void AddResourceToExpand(ServerIndex& index,
-                               const std::string& id)
-      {
-        if (level_ == ResourceType_Instance)
-        {
-          FileInfo tmp;
-          if (index.LookupAttachment(tmp, id, FileContentType_Dicom))
-          {
-            instances_.push_back(Instance(id, tmp));
-          }
-        }
-        else
-        {
-          resources_[id] = NULL;
-        }
-      }
-
-
-    public:
-      ArchiveIndex(ResourceType level) :
-        level_(level)
-      {
-      }
-
-      ~ArchiveIndex()
-      {
-        for (Resources::iterator it = resources_.begin();
-             it != resources_.end(); ++it)
-        {
-          delete it->second;
-        }
-      }
-
-
-      void Add(ServerIndex& index,
-               const ResourceIdentifiers& resource)
-      {
-        const std::string& id = resource.GetIdentifier(level_);
-        Resources::iterator previous = resources_.find(id);
-
-        if (level_ == ResourceType_Instance)
-        {
-          AddResourceToExpand(index, id);
-        }
-        else if (resource.GetLevel() == level_)
-        {
-          // Mark this resource for further expansion
-          if (previous != resources_.end())
-          {
-            delete previous->second;
-          }
-
-          resources_[id] = NULL;
-        }
-        else if (previous == resources_.end())
-        {
-          // This is the first time we meet this resource
-          std::auto_ptr<ArchiveIndex> child(new ArchiveIndex(GetChildResourceType(level_)));
-          child->Add(index, resource);
-          resources_[id] = child.release();
-        }
-        else if (previous->second != NULL)
-        {
-          previous->second->Add(index, resource);
-        }
-        else
-        {
-          // Nothing to do: This item is marked for further expansion
-        }
-      }
-
-
-      void Expand(ServerIndex& index)
-      {
-        if (level_ == ResourceType_Instance)
-        {
-          // Expanding an instance node makes no sense
-          return;
-        }
-
-        for (Resources::iterator it = resources_.begin();
-             it != resources_.end(); ++it)
-        {
-          if (it->second == NULL)
-          {
-            // This is resource is marked for expansion
-            std::list<std::string> children;
-            index.GetChildren(children, it->first);
-
-            std::auto_ptr<ArchiveIndex> child(new ArchiveIndex(GetChildResourceType(level_)));
-
-            for (std::list<std::string>::const_iterator 
-                   it2 = children.begin(); it2 != children.end(); ++it2)
-            {
-              child->AddResourceToExpand(index, *it2);
-            }
-
-            it->second = child.release();
-          }
-
-          assert(it->second != NULL);
-          it->second->Expand(index);
-        }        
-      }
-
-
-      void Apply(IArchiveVisitor& visitor) const
-      {
-        if (level_ == ResourceType_Instance)
-        {
-          for (std::list<Instance>::const_iterator 
-                 it = instances_.begin(); it != instances_.end(); ++it)
-          {
-            visitor.AddInstance(it->id_, it->dicom_);
-          }          
-        }
-        else
-        {
-          for (Resources::const_iterator it = resources_.begin();
-               it != resources_.end(); ++it)
-          {
-            assert(it->second != NULL);  // There must have been a call to "Expand()"
-            visitor.Open(level_, it->first);
-            it->second->Apply(visitor);
-            visitor.Close();
-          }
-        }
-      }
-    };
-
-
-    class StatisticsVisitor : public IArchiveVisitor
-    {
-    private:
-      uint64_t       size_;
-      unsigned int   instances_;
-      
-    public:
-      StatisticsVisitor() : size_(0), instances_(0)
-      {
-      }
-
-      uint64_t GetUncompressedSize() const
-      {
-        return size_;
-      }
-
-      unsigned int GetInstancesCount() const
-      {
-        return instances_;
-      }
-
-      virtual void Open(ResourceType level,
-                        const std::string& publicId)
-      {
-      }
-
-      virtual void Close()
-      {
-      }
-
-      virtual void AddInstance(const std::string& instanceId,
-                               const FileInfo& dicom)
-      {
-        instances_ ++;
-        size_ += dicom.GetUncompressedSize();
-      }
-    };
-
-
-    class PrintVisitor : public IArchiveVisitor
-    {
-    private:
-      std::ostream& out_;
-      std::string   indent_;
-
-    public:
-      PrintVisitor(std::ostream& out) : out_(out)
-      {
-      }
-
-      virtual void Open(ResourceType level,
-                        const std::string& publicId)
-      {
-        switch (level)
-        {
-          case ResourceType_Patient:  indent_ = "";       break;
-          case ResourceType_Study:    indent_ = "  ";     break;
-          case ResourceType_Series:   indent_ = "    ";   break;
-          default:
-            throw OrthancException(ErrorCode_InternalError);
-        }
-
-        out_ << indent_ << publicId << std::endl;
-      }
-
-      virtual void Close()
-      {
-      }
-
-      virtual void AddInstance(const std::string& instanceId,
-                               const FileInfo& dicom)
-      {
-        out_ << "      " << instanceId << std::endl;
-      }
-    };
-
-
-    class ArchiveWriterVisitor : public IArchiveVisitor
-    {
-    private:
-      HierarchicalZipWriter&  writer_;
-      ServerContext&            context_;
-      char                    instanceFormat_[24];
-      unsigned int            countInstances_;
-
-      static std::string GetTag(const DicomMap& tags,
-                                const DicomTag& tag)
-      {
-        const DicomValue* v = tags.TestAndGetValue(tag);
-        if (v != NULL &&
-            !v->IsBinary() &&
-            !v->IsNull())
-        {
-          return v->GetContent();
-        }
-        else
-        {
-          return "";
-        }
-      }
-
-    public:
-      ArchiveWriterVisitor(HierarchicalZipWriter& writer,
-                           ServerContext& context) :
-        writer_(writer),
-        context_(context),
-        countInstances_(0)
-      {
-        snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm");
-      }
-
-      virtual void Open(ResourceType level,
-                        const std::string& publicId)
-      {
-        std::string path;
-
-        DicomMap tags;
-        if (context_.GetIndex().GetMainDicomTags(tags, publicId, level, level))
-        {
-          switch (level)
-          {
-            case ResourceType_Patient:
-              path = GetTag(tags, DICOM_TAG_PATIENT_ID) + " " + GetTag(tags, DICOM_TAG_PATIENT_NAME);
-              break;
-
-            case ResourceType_Study:
-              path = GetTag(tags, DICOM_TAG_ACCESSION_NUMBER) + " " + GetTag(tags, DICOM_TAG_STUDY_DESCRIPTION);
-              break;
-
-            case ResourceType_Series:
-            {
-              std::string modality = GetTag(tags, DICOM_TAG_MODALITY);
-              path = modality + " " + GetTag(tags, DICOM_TAG_SERIES_DESCRIPTION);
-
-              if (modality.size() == 0)
-              {
-                snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm");
-              }
-              else if (modality.size() == 1)
-              {
-                snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%%07d.dcm", 
-                         toupper(modality[0]));
-              }
-              else if (modality.size() >= 2)
-              {
-                snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%c%%06d.dcm", 
-                         toupper(modality[0]), toupper(modality[1]));
-              }
-
-              countInstances_ = 0;
-
-              break;
-            }
-
-            default:
-              throw OrthancException(ErrorCode_InternalError);
-          }
-        }
-
-        path = Toolbox::StripSpaces(Toolbox::ConvertToAscii(path));
-
-        if (path.empty())
-        {
-          path = std::string("Unknown ") + EnumerationToString(level);
-        }
-
-        writer_.OpenDirectory(path.c_str());
-      }
-
-      virtual void Close()
-      {
-        writer_.CloseDirectory();
-      }
-
-      virtual void AddInstance(const std::string& instanceId,
-                               const FileInfo& dicom)
-      {
-        std::string content;
-        context_.ReadAttachment(content, dicom);
-
-        char filename[24];
-        snprintf(filename, sizeof(filename) - 1, instanceFormat_, countInstances_);
-        countInstances_ ++;
-
-        writer_.OpenFile(filename);
-        writer_.Write(content);
-      }
-
-      static void Apply(RestApiOutput& output,
-                        ServerContext& context,
-                        ArchiveIndex& archive,
-                        const std::string& filename)
-      {
-        archive.Expand(context.GetIndex());
-
-        StatisticsVisitor stats;
-        archive.Apply(stats);
-
-        const bool isZip64 = IsZip64Required(stats.GetUncompressedSize(), stats.GetInstancesCount());
-
-        // Create a RAII for the temporary file to manage the ZIP file
-        TemporaryFile tmp;
-
-        {
-          // Create a ZIP writer
-          HierarchicalZipWriter writer(tmp.GetPath().c_str());
-          writer.SetZip64(isZip64);
-
-          ArchiveWriterVisitor v(writer, context);
-          archive.Apply(v);
-        }
-
-        // Prepare the sending of the ZIP file
-        FilesystemHttpSender sender(tmp.GetPath());
-        sender.SetContentType("application/zip");
-        sender.SetContentFilename(filename);
-
-        // Send the ZIP
-        output.AnswerStream(sender);
-
-        // The temporary file is automatically removed thanks to the RAII
-      }
-    };
-
-    
-    class MediaWriterVisitor : public IArchiveVisitor
-    {
-    private:
-      HierarchicalZipWriter&  writer_;
-      DicomDirWriter          dicomDir_;
-      ServerContext&          context_;
-      unsigned int            countInstances_;
-
-    public:
-      MediaWriterVisitor(HierarchicalZipWriter& writer,
-                         ServerContext& context) :
-        writer_(writer),
-        context_(context),
-        countInstances_(0)
-      {
-      }
-
-      void EncodeDicomDir(std::string& result)
-      {
-        dicomDir_.Encode(result);
-      }
-
-      virtual void Open(ResourceType level,
-                        const std::string& publicId)
-      {
-      }
-
-      virtual void Close()
-      {
-      }
-
-      virtual void AddInstance(const std::string& instanceId,
-                               const FileInfo& dicom)
-      {
-        // "DICOM restricts the filenames on DICOM media to 8
-        // characters (some systems wrongly use 8.3, but this does not
-        // conform to the standard)."
-        std::string filename = "IM" + boost::lexical_cast<std::string>(countInstances_);
-        writer_.OpenFile(filename.c_str());
-
-        std::string content;
-        context_.ReadAttachment(content, dicom);
-        writer_.Write(content);
-
-        ParsedDicomFile parsed(content);
-        dicomDir_.Add("IMAGES", filename, parsed);
-
-        countInstances_ ++;
-      }
-
-      static void Apply(RestApiOutput& output,
-                        ServerContext& context,
-                        ArchiveIndex& archive,
-                        const std::string& filename,
-                        bool enableExtendedSopClass)
-      {
-        archive.Expand(context.GetIndex());
-
-        StatisticsVisitor stats;
-        archive.Apply(stats);
-
-        const bool isZip64 = IsZip64Required(stats.GetUncompressedSize(), stats.GetInstancesCount());
-
-        // Create a RAII for the temporary file to manage the ZIP file
-        TemporaryFile tmp;
-
-        {
-          // Create a ZIP writer
-          HierarchicalZipWriter writer(tmp.GetPath().c_str());
-          writer.SetZip64(isZip64);
-          writer.OpenDirectory("IMAGES");
-
-          // Create a DICOMDIR writer
-          MediaWriterVisitor v(writer, context);
-
-          // Request type-3 arguments to be added to the DICOMDIR
-          v.dicomDir_.EnableExtendedSopClass(enableExtendedSopClass);
-
-          archive.Apply(v);
-
-          // Add the DICOMDIR
-          writer.CloseDirectory();
-          writer.OpenFile("DICOMDIR");
-          std::string s;
-          v.EncodeDicomDir(s);
-          writer.Write(s);
-        }
-
-        // Prepare the sending of the ZIP file
-        FilesystemHttpSender sender(tmp.GetPath());
-        sender.SetContentType("application/zip");
-        sender.SetContentFilename(filename);
-
-        // Send the ZIP
-        output.AnswerStream(sender);
-
-        // The temporary file is automatically removed thanks to the RAII
-      }
-    };
-  }
-
-
-  static bool AddResourcesOfInterest(ArchiveIndex& archive,
+  static bool AddResourcesOfInterest(ArchiveJob& job,
                                      RestApiPostCall& call)
   {
-    ServerIndex& index = OrthancRestApi::GetIndex(call);
-
     Json::Value resources;
     if (call.ParseJsonRequest(resources) &&
         resources.type() == Json::arrayValue)
@@ -690,8 +53,7 @@
           return false;   // Bad request
         }
 
-        ResourceIdentifiers resource(index, resources[i].asString());
-        archive.Add(index, resource);
+        job.AddResource(resources[i].asString());
       }
 
       return true;
@@ -703,16 +65,46 @@
   }
 
 
+  static void SubmitJob(RestApiCall& call,
+                        boost::shared_ptr<TemporaryFile>& tmp,
+                        ServerContext& context,
+                        std::auto_ptr<ArchiveJob>& job,
+                        const std::string& filename)
+  {
+    if (job.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+
+    job->SetDescription("REST API");
+
+    if (context.GetJobsEngine().GetRegistry().SubmitAndWait(job.release(), 0 /* TODO priority */))
+    {
+      // The archive is now created: Prepare the sending of the ZIP file
+      FilesystemHttpSender sender(tmp->GetPath());
+      sender.SetContentType("application/zip");
+      sender.SetContentFilename(filename);
+
+      // Send the ZIP
+      call.GetOutput().AnswerStream(sender);
+    }
+    else
+    {
+      call.GetOutput().SignalError(HttpStatus_500_InternalServerError);
+    }      
+  }
+
+  
   static void CreateBatchArchive(RestApiPostCall& call)
   {
-    ArchiveIndex archive(ResourceType_Patient);  // root
+    ServerContext& context = OrthancRestApi::GetContext(call);
 
-    if (AddResourcesOfInterest(archive, call))
+    boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile);
+    std::auto_ptr<ArchiveJob> job(new ArchiveJob(tmp, context, false, false));
+
+    if (AddResourcesOfInterest(*job, call))
     {
-      ArchiveWriterVisitor::Apply(call.GetOutput(),
-                                  OrthancRestApi::GetContext(call),
-                                  archive,
-                                  "Archive.zip");
+      SubmitJob(call, tmp, context, job, "Archive.zip");
     }
   }  
 
@@ -720,51 +112,43 @@
   template <bool Extended>
   static void CreateBatchMedia(RestApiPostCall& call)
   {
-    ArchiveIndex archive(ResourceType_Patient);  // root
+    ServerContext& context = OrthancRestApi::GetContext(call);
 
-    if (AddResourcesOfInterest(archive, call))
+    boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile);
+    std::auto_ptr<ArchiveJob> job(new ArchiveJob(tmp, context, true, Extended));
+
+    if (AddResourcesOfInterest(*job, call))
     {
-      MediaWriterVisitor::Apply(call.GetOutput(),
-                                OrthancRestApi::GetContext(call),
-                                archive,
-                                "Archive.zip",
-                                Extended);
+      SubmitJob(call, tmp, context, job, "Archive.zip");
     }
-  }  
-
+  }
+  
 
   static void CreateArchive(RestApiGetCall& call)
   {
-    ServerIndex& index = OrthancRestApi::GetIndex(call);
+    ServerContext& context = OrthancRestApi::GetContext(call);
 
     std::string id = call.GetUriComponent("id", "");
-    ResourceIdentifiers resource(index, id);
-
-    ArchiveIndex archive(ResourceType_Patient);  // root
-    archive.Add(OrthancRestApi::GetIndex(call), resource);
 
-    ArchiveWriterVisitor::Apply(call.GetOutput(),
-                                OrthancRestApi::GetContext(call),
-                                archive,
-                                id + ".zip");
+    boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile);
+    std::auto_ptr<ArchiveJob> job(new ArchiveJob(tmp, context, false, false));
+    job->AddResource(id);
+
+    SubmitJob(call, tmp, context, job, id + ".zip");
   }
 
 
   static void CreateMedia(RestApiGetCall& call)
   {
-    ServerIndex& index = OrthancRestApi::GetIndex(call);
+    ServerContext& context = OrthancRestApi::GetContext(call);
 
     std::string id = call.GetUriComponent("id", "");
-    ResourceIdentifiers resource(index, id);
-
-    ArchiveIndex archive(ResourceType_Patient);  // root
-    archive.Add(OrthancRestApi::GetIndex(call), resource);
 
-    MediaWriterVisitor::Apply(call.GetOutput(),
-                              OrthancRestApi::GetContext(call),
-                              archive,
-                              id + ".zip",
-                              call.HasArgument("extended"));
+    boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile);
+    std::auto_ptr<ArchiveJob> job(new ArchiveJob(tmp, context, true, call.HasArgument("extended")));
+    job->AddResource(id);
+
+    SubmitJob(call, tmp, context, job, id + ".zip");
   }
 
 
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Wed Jun 20 18:03:20 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;
@@ -582,7 +607,7 @@
    ***************************************************************************/
 
   static bool GetInstancesToExport(Json::Value& otherArguments,
-                                   std::list<std::string>& instances,
+                                   SetOfInstancesJob& job,
                                    const std::string& remote,
                                    RestApiPostCall& call)
   {
@@ -656,21 +681,55 @@
       {
         context.GetIndex().LogExportedResource(stripped, remote);
       }
-       
-      std::list<std::string> tmp;
-      context.GetIndex().GetChildInstances(tmp, stripped);
 
-      for (std::list<std::string>::const_iterator
-             it = tmp.begin(); it != tmp.end(); ++it)
-      {
-        instances.push_back(*it);
-      }
+      context.AddChildInstances(job, stripped);
     }
 
     return true;
   }
 
 
+  static void SubmitJob(RestApiPostCall& call,
+                        const Json::Value& request,
+                        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);
+    
+    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);
@@ -678,56 +737,29 @@
     std::string remote = call.GetUriComponent("id", "");
 
     Json::Value request;
-    std::list<std::string> instances;
-    if (!GetInstancesToExport(request, instances, remote, call))
-    {
-      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::auto_ptr<DicomModalityStoreJob> job(new DicomModalityStoreJob(context));
 
-    if (moveOriginatorID < 0 || 
-        moveOriginatorID >= 65536)
+    if (GetInstancesToExport(request, *job, remote, call))
     {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-    
-    RemoteModalityParameters p = Configuration::GetModalityUsingSymbolicName(remote);
+      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 */);
 
-    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));
+      RemoteModalityParameters p = Configuration::GetModalityUsingSymbolicName(remote);
+
+      job->SetDescription("REST API");
+      job->SetLocalAet(localAet);
+      job->SetRemoteModality(p);
 
       if (moveOriginatorID != 0)
       {
-        command->SetMoveOriginator(moveOriginatorAET, static_cast<uint16_t>(moveOriginatorID));
+        job->SetMoveOriginator(moveOriginatorAET, moveOriginatorID);
       }
 
-      job.AddCommand(command.release()).AddInput(*it);
-    }
-
-    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, job.release());
     }
   }
 
@@ -757,18 +789,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
@@ -844,40 +881,17 @@
     std::string remote = call.GetUriComponent("id", "");
 
     Json::Value request;
-    std::list<std::string> instances;
-    if (!GetInstancesToExport(request, instances, remote, call))
-    {
-      return;
-    }
+    std::auto_ptr<OrthancPeerStoreJob> job(new OrthancPeerStoreJob(context));
 
-    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)
+    if (GetInstancesToExport(request, *job, remote, call))
     {
-      job.AddCommand(new StorePeerCommand(context, peer, false)).AddInput(*it);
-    }
-
-    job.SetDescription("HTTP request: POST 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);
+      WebServiceParameters peer;
+      Configuration::GetOrthancPeer(peer, remote);
+      
+      job->SetDescription("REST API");
+      job->SetPeer(peer);    
+      
+      SubmitJob(call, request, job.release());
     }
   }
 
@@ -984,15 +998,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 Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestSystem.cpp	Wed Jun 20 18:03:20 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,99 @@
   }
 
 
-  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");
+
+    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.Format(tmp);
+          v.append(tmp);
+        }
+      }
+      else
+      {
+        v.append(*it);
+      }
+    }
+    
+    call.GetOutput().AnswerJson(v);
+  }
+
+  static void GetJobInfo(RestApiGetCall& call)
+  {
+    std::string id = call.GetUriComponent("id", "");
+
+    JobInfo info;
+    if (OrthancRestApi::GetContext(call).GetJobsEngine().GetRegistry().GetJobInfo(info, id))
+    {
+      Json::Value json;
+      info.Format(json);
+      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 +378,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 Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/QueryRetrieveHandler.cpp	Wed Jun 20 18:03:20 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 Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/QueryRetrieveHandler.h	Wed Jun 20 18:03:20 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 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 Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/ServerContext.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -34,27 +34,22 @@
 #include "PrecompiledHeadersServer.h"
 #include "ServerContext.h"
 
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
 #include "../Core/FileStorage/StorageAccessor.h"
 #include "../Core/HttpServer/FilesystemHttpSender.h"
 #include "../Core/HttpServer/HttpStreamTranscoder.h"
 #include "../Core/Logging.h"
-#include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Plugins/Engine/OrthancPlugins.h"
+#include "OrthancInitialization.h"
+#include "OrthancRestApi/OrthancRestApi.h"
+#include "Search/LookupResource.h"
+#include "ServerJobs/OrthancJobUnserializer.h"
 #include "ServerToolbox.h"
-#include "OrthancInitialization.h"
 
 #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"
-
 
 #define ENABLE_DICOM_CACHE  1
 
@@ -71,11 +66,12 @@
 
 namespace Orthanc
 {
-  void ServerContext::ChangeThread(ServerContext* that)
+  void ServerContext::ChangeThread(ServerContext* that,
+                                   unsigned int sleepDelay)
   {
     while (!that->done_)
     {
-      std::auto_ptr<IDynamicObject> obj(that->pendingChanges_.Dequeue(100));
+      std::auto_ptr<IDynamicObject> obj(that->pendingChanges_.Dequeue(sleepDelay));
         
       if (obj.get() != NULL)
       {
@@ -112,29 +108,138 @@
   }
 
 
+  void ServerContext::SaveJobsThread(ServerContext* that,
+                                     unsigned int sleepDelay)
+  {
+    static const boost::posix_time::time_duration PERIODICITY =
+      boost::posix_time::seconds(10);
+    
+    boost::posix_time::ptime next =
+      boost::posix_time::microsec_clock::universal_time() + PERIODICITY;
+    
+    while (!that->done_)
+    {
+      boost::this_thread::sleep(boost::posix_time::milliseconds(sleepDelay));
+
+      if (that->haveJobsChanged_ ||
+          boost::posix_time::microsec_clock::universal_time() >= next)
+      {
+        that->haveJobsChanged_ = false;
+        that->SaveJobsEngine();
+        next = boost::posix_time::microsec_clock::universal_time() + PERIODICITY;
+      }
+    }
+  }
+  
+
+  void ServerContext::SignalJobSubmitted(const std::string& jobId)
+  {
+    haveJobsChanged_ = true;
+    lua_.SignalJobSubmitted(jobId);
+  }
+  
+
+  void ServerContext::SignalJobSuccess(const std::string& jobId)
+  {
+    haveJobsChanged_ = true;
+    lua_.SignalJobSuccess(jobId);
+  }
+
+  
+  void ServerContext::SignalJobFailure(const std::string& jobId)
+  {
+    haveJobsChanged_ = true;
+    lua_.SignalJobFailure(jobId);
+  }
+
+
+  void ServerContext::SetupJobsEngine(bool unitTesting,
+                                      bool loadJobsFromDatabase)
+  {
+    jobsEngine_.SetWorkersCount(Configuration::GetGlobalUnsignedIntegerParameter("ConcurrentJobs", 2));
+    jobsEngine_.SetThreadSleep(unitTesting ? 20 : 200);
+
+    if (loadJobsFromDatabase)
+    {
+      std::string serialized;
+      if (index_.LookupGlobalProperty(serialized, GlobalProperty_JobsRegistry))
+      {
+        LOG(WARNING) << "Reloading the jobs from the last execution of Orthanc";
+        OrthancJobUnserializer unserializer(*this);
+
+        try
+        {
+          jobsEngine_.LoadRegistryFromString(unserializer, serialized);
+        }
+        catch (OrthancException& e)
+        {
+          LOG(ERROR) << "Cannot unserialize the jobs engine: " << e.What();
+          throw;
+        }
+      }
+      else
+      {
+        LOG(INFO) << "The last execution of Orthanc has archived no job";
+      }
+    }
+    else
+    {
+      LOG(WARNING) << "Not reloading the jobs from the last execution of Orthanc";
+    }
+
+    //jobsEngine_.GetRegistry().SetMaxCompleted   // TODO
+
+    jobsEngine_.GetRegistry().SetObserver(*this);
+    jobsEngine_.Start();
+  }
+
+
+  void ServerContext::SaveJobsEngine()
+  {
+    LOG(INFO) << "Serializing the content of the jobs engine";
+    
+    try
+    {
+      Json::Value value;
+      jobsEngine_.GetRegistry().Serialize(value);
+
+      Json::FastWriter writer;
+      std::string serialized = writer.write(value);
+
+      index_.SetGlobalProperty(GlobalProperty_JobsRegistry, serialized);
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "Cannot serialize the jobs engine: " << e.What();
+    }
+  }
+
+
   ServerContext::ServerContext(IDatabaseWrapper& database,
-                               IStorageArea& area) :
-    index_(*this, database),
+                               IStorageArea& area,
+                               bool unitTesting,
+                               bool loadJobsFromDatabase) :
+    index_(*this, database, (unitTesting ? 20 : 500)),
     area_(area),
     compressionEnabled_(false),
     storeMD5_(true),
     provider_(*this),
     dicomCache_(provider_, DICOM_CACHE_SIZE),
-    scheduler_(Configuration::GetGlobalUnsignedIntegerParameter("LimitJobs", 10)),
     lua_(*this),
 #if ORTHANC_ENABLE_PLUGINS == 1
     plugins_(NULL),
 #endif
     done_(false),
+    haveJobsChanged_(false),
     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"));
 
-    changeThread_ = boost::thread(ChangeThread, this);
+    SetupJobsEngine(unitTesting, loadJobsFromDatabase);
+
+    changeThread_ = boost::thread(ChangeThread, this, (unitTesting ? 20 : 100));
+    saveJobsThread_ = boost::thread(SaveJobsThread, this, (unitTesting ? 20 : 100));
   }
 
 
@@ -165,10 +270,16 @@
         changeThread_.join();
       }
 
-      scu_.Finalize();
+      if (saveJobsThread_.joinable())
+      {
+        saveJobsThread_.join();
+      }
+
+      jobsEngine_.GetRegistry().ResetObserver();
+      SaveJobsEngine();
 
       // Do not change the order below!
-      scheduler_.Stop();
+      jobsEngine_.Stop();
       index_.Stop();
     }
   }
@@ -679,4 +790,19 @@
     }
   }
 
+
+  void ServerContext::AddChildInstances(SetOfInstancesJob& job,
+                                        const std::string& publicId)
+  {
+    std::list<std::string> instances;
+    GetIndex().GetChildInstances(instances, publicId);
+
+    job.Reserve(job.GetInstancesCount() + instances.size());
+
+    for (std::list<std::string>::const_iterator
+           it = instances.begin(); it != instances.end(); ++it)
+    {
+      job.AddInstance(*it);
+    }
+  }
 }
--- a/OrthancServer/ServerContext.h	Wed Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/ServerContext.h	Wed Jun 20 18:03:20 2018 +0200
@@ -33,21 +33,21 @@
 
 #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/JobsEngine/SetOfInstancesJob.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>
@@ -60,7 +60,7 @@
    * filesystem (including compression), as well as the index of the
    * DICOM store. It implements the required locking mechanisms.
    **/
-  class ServerContext
+  class ServerContext : private JobsRegistry::IObserver
   {
   private:
     class DicomCacheProvider : public ICachePageProvider
@@ -104,11 +104,26 @@
     typedef std::list<ServerListener>  ServerListeners;
 
 
-    static void ChangeThread(ServerContext* that);
+    static void ChangeThread(ServerContext* that,
+                             unsigned int sleepDelay);
+
+    static void SaveJobsThread(ServerContext* that,
+                               unsigned int sleepDelay);
 
     void ReadDicomAsJsonInternal(std::string& result,
                                  const std::string& instancePublicId);
 
+    void SetupJobsEngine(bool unitTesting,
+                         bool loadJobsFromDatabase);
+
+    void SaveJobsEngine();
+
+    virtual void SignalJobSubmitted(const std::string& jobId);
+
+    virtual void SignalJobSuccess(const std::string& jobId);
+
+    virtual void SignalJobFailure(const std::string& jobId);
+
     ServerIndex index_;
     IStorageArea& area_;
 
@@ -118,8 +133,7 @@
     DicomCacheProvider provider_;
     boost::mutex dicomCacheMutex_;
     MemoryCache dicomCache_;
-    ReusableDicomUserConnection scu_;
-    ServerScheduler scheduler_;
+    JobsEngine jobsEngine_;
 
     LuaScripting lua_;
 
@@ -131,8 +145,10 @@
     boost::recursive_mutex listenersMutex_;
 
     bool done_;
+    bool haveJobsChanged_;
     SharedMessageQueue  pendingChanges_;
     boost::thread  changeThread_;
+    boost::thread  saveJobsThread_;
         
     SharedArchive  queryRetrieveArchive_;
     std::string defaultLocalAet_;
@@ -159,7 +175,9 @@
     };
 
     ServerContext(IDatabaseWrapper& database,
-                  IStorageArea& area);
+                  IStorageArea& area,
+                  bool unitTesting,
+                  bool loadJobsFromDatabase);
 
     ~ServerContext();
 
@@ -238,14 +256,9 @@
       return storeMD5_;
     }
 
-    ReusableDicomUserConnection& GetReusableDicomUserConnection()
+    JobsEngine& GetJobsEngine()
     {
-      return scu_;
-    }
-
-    ServerScheduler& GetScheduler()
-    {
-      return scheduler_;
+      return jobsEngine_;
     }
 
     bool DeleteResource(Json::Value& target,
@@ -264,7 +277,7 @@
       return defaultLocalAet_;
     }
 
-    LuaScripting& GetLua()
+    LuaScripting& GetLuaScripting()
     {
       return lua_;
     }
@@ -297,5 +310,8 @@
 #endif
 
     bool HasPlugins() const;
+
+    void AddChildInstances(SetOfInstancesJob& job,
+                           const std::string& publicId);
   };
 }
--- a/OrthancServer/ServerEnumerations.h	Wed Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/ServerEnumerations.h	Wed Jun 20 18:03:20 2018 +0200
@@ -76,7 +76,8 @@
     GlobalProperty_DatabaseSchemaVersion = 1,   // Unused in the Orthanc core as of Orthanc 0.9.5
     GlobalProperty_FlushSleep = 2,
     GlobalProperty_AnonymizationSequence = 3,
-    GlobalProperty_DatabasePatchLevel = 4       // Reserved for internal use of the database plugins
+    GlobalProperty_DatabasePatchLevel = 4,      // Reserved for internal use of the database plugins
+    GlobalProperty_JobsRegistry = 5
   };
 
   enum MetadataType
--- a/OrthancServer/ServerIndex.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/ServerIndex.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -342,7 +342,8 @@
   }
 
 
-  void ServerIndex::FlushThread(ServerIndex* that)
+  void ServerIndex::FlushThread(ServerIndex* that,
+                                unsigned int threadSleep)
   {
     // By default, wait for 10 seconds before flushing
     unsigned int sleep = 10;
@@ -368,7 +369,7 @@
 
     while (!that->done_)
     {
-      boost::this_thread::sleep(boost::posix_time::seconds(1));
+      boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep));
       count++;
       if (count < sleep)
       {
@@ -538,7 +539,8 @@
 
 
   ServerIndex::ServerIndex(ServerContext& context,
-                           IDatabaseWrapper& db) : 
+                           IDatabaseWrapper& db,
+                           unsigned int threadSleep) : 
     done_(false),
     db_(db),
     maximumStorageSize_(0),
@@ -555,10 +557,11 @@
 
     if (db.HasFlushToDisk())
     {
-      flushThread_ = boost::thread(FlushThread, this);
+      flushThread_ = boost::thread(FlushThread, this, threadSleep);
     }
 
-    unstableResourcesMonitorThread_ = boost::thread(UnstableResourcesMonitorThread, this);
+    unstableResourcesMonitorThread_ = boost::thread
+      (UnstableResourcesMonitorThread, this, threadSleep);
   }
 
 
@@ -778,9 +781,10 @@
       // Attach the auto-computed metadata for the instance level,
       // reflecting these additions into the input metadata map
       SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_ReceptionDate, now);
-      SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_RemoteAet, instanceToStore.GetRemoteAet());
+      SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_RemoteAet,
+                          instanceToStore.GetOrigin().GetRemoteAetC());
       SetInstanceMetadata(instanceMetadata, instance, MetadataType_Instance_Origin, 
-                          EnumerationToString(instanceToStore.GetRequestOrigin()));
+                          EnumerationToString(instanceToStore.GetOrigin().GetRequestOrigin()));
         
       {
         std::string s;
@@ -1212,7 +1216,7 @@
     ResourceType type;
     if (!db_.LookupResource(id, type, publicId))
     {
-      throw OrthancException(ErrorCode_InternalError);
+      throw OrthancException(ErrorCode_InexistentItem);
     }
 
     std::string patientId;
@@ -1877,7 +1881,8 @@
   }
 
 
-  void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that)
+  void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that,
+                                                   unsigned int threadSleep)
   {
     int stableAge = Configuration::GetGlobalUnsignedIntegerParameter("StableAge", 60);
     if (stableAge <= 0)
@@ -1889,8 +1894,8 @@
 
     while (!that->done_)
     {
-      // Check for stable resources each second
-      boost::this_thread::sleep(boost::posix_time::seconds(1));
+      // Check for stable resources each few seconds
+      boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep));
 
       boost::mutex::scoped_lock lock(that->mutex_);
 
@@ -2091,13 +2096,20 @@
   }
 
 
+  bool ServerIndex::LookupGlobalProperty(std::string& value,
+                                         GlobalProperty property)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    return db_.LookupGlobalProperty(value, property);
+  }
+  
+
   std::string ServerIndex::GetGlobalProperty(GlobalProperty property,
                                              const std::string& defaultValue)
   {
-    boost::mutex::scoped_lock lock(mutex_);
-
     std::string value;
-    if (db_.LookupGlobalProperty(value, property))
+
+    if (LookupGlobalProperty(value, property))
     {
       return value;
     }
--- a/OrthancServer/ServerIndex.h	Wed Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/ServerIndex.h	Wed Jun 20 18:03:20 2018 +0200
@@ -74,9 +74,11 @@
     uint64_t maximumStorageSize_;
     unsigned int maximumPatients_;
 
-    static void FlushThread(ServerIndex* that);
+    static void FlushThread(ServerIndex* that,
+                            unsigned int threadSleep);
 
-    static void UnstableResourcesMonitorThread(ServerIndex* that);
+    static void UnstableResourcesMonitorThread(ServerIndex* that,
+                                               unsigned int threadSleep);
 
     void MainDicomTagsToJson(Json::Value& result,
                              int64_t resourceId,
@@ -124,7 +126,8 @@
 
   public:
     ServerIndex(ServerContext& context,
-                IDatabaseWrapper& database);
+                IDatabaseWrapper& database,
+                unsigned int threadSleep);
 
     ~ServerIndex();
 
@@ -255,6 +258,9 @@
     void SetGlobalProperty(GlobalProperty property,
                            const std::string& value);
 
+    bool LookupGlobalProperty(std::string& value,
+                              GlobalProperty property);
+
     std::string GetGlobalProperty(GlobalProperty property,
                                   const std::string& defaultValue);
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/ArchiveJob.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,904 @@
+/**
+ * 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 "ArchiveJob.h"
+
+#include "../../Core/Compression/HierarchicalZipWriter.h"
+#include "../../Core/DicomParsing/DicomDirWriter.h"
+#include "../../Core/Logging.h"
+#include "../../Core/OrthancException.h"
+
+#include <stdio.h>
+
+#if defined(_MSC_VER)
+#define snprintf _snprintf
+#endif
+
+static const uint64_t MEGA_BYTES = 1024 * 1024;
+static const uint64_t GIGA_BYTES = 1024 * 1024 * 1024;
+static const char* MEDIA_IMAGES_FOLDER = "IMAGES"; 
+
+namespace Orthanc
+{
+  static bool IsZip64Required(uint64_t uncompressedSize,
+                              unsigned int countInstances)
+  {
+    static const uint64_t      SAFETY_MARGIN = 64 * MEGA_BYTES;  // Should be large enough to hold DICOMDIR
+    static const unsigned int  FILES_MARGIN = 10;
+
+    /**
+     * Determine whether ZIP64 is required. Original ZIP format can
+     * store up to 2GB of data (some implementation supporting up to
+     * 4GB of data), and up to 65535 files.
+     * https://en.wikipedia.org/wiki/Zip_(file_format)#ZIP64
+     **/
+
+    const bool isZip64 = (uncompressedSize >= 2 * GIGA_BYTES - SAFETY_MARGIN ||
+                          countInstances >= 65535 - FILES_MARGIN);
+
+    LOG(INFO) << "Creating a ZIP file with " << countInstances << " files of size "
+              << (uncompressedSize / MEGA_BYTES) << "MB using the "
+              << (isZip64 ? "ZIP64" : "ZIP32") << " file format";
+
+    return isZip64;
+  }
+
+
+  class ArchiveJob::ResourceIdentifiers : public boost::noncopyable
+  {
+  private:
+    ResourceType   level_;
+    std::string    patient_;
+    std::string    study_;
+    std::string    series_;
+    std::string    instance_;
+
+    static void GoToParent(ServerIndex& index,
+                           std::string& current)
+    {
+      std::string tmp;
+
+      if (index.LookupParent(tmp, current))
+      {
+        current = tmp;
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
+    }
+
+
+  public:
+    ResourceIdentifiers(ServerIndex& index,
+                        const std::string& publicId)
+    {
+      if (!index.LookupResourceType(level_, publicId))
+      {
+        throw OrthancException(ErrorCode_UnknownResource);
+      }
+
+      std::string current = publicId;;
+      switch (level_)  // Do not add "break" below!
+      {
+        case ResourceType_Instance:
+          instance_ = current;
+          GoToParent(index, current);
+            
+        case ResourceType_Series:
+          series_ = current;
+          GoToParent(index, current);
+
+        case ResourceType_Study:
+          study_ = current;
+          GoToParent(index, current);
+
+        case ResourceType_Patient:
+          patient_ = current;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    const std::string& GetIdentifier(ResourceType level) const
+    {
+      // Some sanity check to ensure enumerations are not altered
+      assert(ResourceType_Patient < ResourceType_Study);
+      assert(ResourceType_Study < ResourceType_Series);
+      assert(ResourceType_Series < ResourceType_Instance);
+
+      if (level > level_)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      switch (level)
+      {
+        case ResourceType_Patient:
+          return patient_;
+
+        case ResourceType_Study:
+          return study_;
+
+        case ResourceType_Series:
+          return series_;
+
+        case ResourceType_Instance:
+          return instance_;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+  };
+
+
+  class ArchiveJob::IArchiveVisitor : public boost::noncopyable
+  {
+  public:
+    virtual ~IArchiveVisitor()
+    {
+    }
+
+    virtual void Open(ResourceType level,
+                      const std::string& publicId) = 0;
+
+    virtual void Close() = 0;
+
+    virtual void AddInstance(const std::string& instanceId,
+                             const FileInfo& dicom) = 0;
+  };
+
+
+  class ArchiveJob::ArchiveIndex : public boost::noncopyable
+  {
+  private:
+    struct Instance
+    {
+      std::string  id_;
+      FileInfo     dicom_;
+
+      Instance(const std::string& id,
+               const FileInfo& dicom) : 
+        id_(id), dicom_(dicom)
+      {
+      }
+    };
+
+    // A "NULL" value for ArchiveIndex indicates a non-expanded node
+    typedef std::map<std::string, ArchiveIndex*>   Resources;
+
+    ResourceType         level_;
+    Resources            resources_;   // Only at patient/study/series level
+    std::list<Instance>  instances_;   // Only at instance level
+
+
+    void AddResourceToExpand(ServerIndex& index,
+                             const std::string& id)
+    {
+      if (level_ == ResourceType_Instance)
+      {
+        FileInfo tmp;
+        if (index.LookupAttachment(tmp, id, FileContentType_Dicom))
+        {
+          instances_.push_back(Instance(id, tmp));
+        }
+      }
+      else
+      {
+        resources_[id] = NULL;
+      }
+    }
+
+
+  public:
+    ArchiveIndex(ResourceType level) :
+      level_(level)
+    {
+    }
+
+    ~ArchiveIndex()
+    {
+      for (Resources::iterator it = resources_.begin();
+           it != resources_.end(); ++it)
+      {
+        delete it->second;
+      }
+    }
+
+
+    void Add(ServerIndex& index,
+             const ResourceIdentifiers& resource)
+    {
+      const std::string& id = resource.GetIdentifier(level_);
+      Resources::iterator previous = resources_.find(id);
+
+      if (level_ == ResourceType_Instance)
+      {
+        AddResourceToExpand(index, id);
+      }
+      else if (resource.GetLevel() == level_)
+      {
+        // Mark this resource for further expansion
+        if (previous != resources_.end())
+        {
+          delete previous->second;
+        }
+
+        resources_[id] = NULL;
+      }
+      else if (previous == resources_.end())
+      {
+        // This is the first time we meet this resource
+        std::auto_ptr<ArchiveIndex> child(new ArchiveIndex(GetChildResourceType(level_)));
+        child->Add(index, resource);
+        resources_[id] = child.release();
+      }
+      else if (previous->second != NULL)
+      {
+        previous->second->Add(index, resource);
+      }
+      else
+      {
+        // Nothing to do: This item is marked for further expansion
+      }
+    }
+
+
+    void Expand(ServerIndex& index)
+    {
+      if (level_ == ResourceType_Instance)
+      {
+        // Expanding an instance node makes no sense
+        return;
+      }
+
+      for (Resources::iterator it = resources_.begin();
+           it != resources_.end(); ++it)
+      {
+        if (it->second == NULL)
+        {
+          // This is resource is marked for expansion
+          std::list<std::string> children;
+          index.GetChildren(children, it->first);
+
+          std::auto_ptr<ArchiveIndex> child(new ArchiveIndex(GetChildResourceType(level_)));
+
+          for (std::list<std::string>::const_iterator 
+                 it2 = children.begin(); it2 != children.end(); ++it2)
+          {
+            child->AddResourceToExpand(index, *it2);
+          }
+
+          it->second = child.release();
+        }
+
+        assert(it->second != NULL);
+        it->second->Expand(index);
+      }        
+    }
+
+
+    void Apply(IArchiveVisitor& visitor) const
+    {
+      if (level_ == ResourceType_Instance)
+      {
+        for (std::list<Instance>::const_iterator 
+               it = instances_.begin(); it != instances_.end(); ++it)
+        {
+          visitor.AddInstance(it->id_, it->dicom_);
+        }          
+      }
+      else
+      {
+        for (Resources::const_iterator it = resources_.begin();
+             it != resources_.end(); ++it)
+        {
+          assert(it->second != NULL);  // There must have been a call to "Expand()"
+          visitor.Open(level_, it->first);
+          it->second->Apply(visitor);
+          visitor.Close();
+        }
+      }
+    }
+  };
+
+
+
+  class ArchiveJob::ZipCommands : public boost::noncopyable
+  {
+  private:
+    enum Type
+    {
+      Type_OpenDirectory,
+      Type_CloseDirectory,
+      Type_WriteInstance
+    };
+
+    class Command : public boost::noncopyable
+    {
+    private:
+      Type          type_;
+      std::string   filename_;
+      std::string   instanceId_;
+      FileInfo      info_;
+
+    public:
+      explicit Command(Type type) :
+        type_(type)
+      {
+        assert(type_ == Type_CloseDirectory);
+      }
+        
+      Command(Type type,
+              const std::string& filename) :
+        type_(type),
+        filename_(filename)
+      {
+        assert(type_ == Type_OpenDirectory);
+      }
+        
+      Command(Type type,
+              const std::string& filename,
+              const std::string& instanceId,
+              const FileInfo& info) :
+        type_(type),
+        filename_(filename),
+        instanceId_(instanceId),
+        info_(info)
+      {
+        assert(type_ == Type_WriteInstance);
+      }
+        
+      void Apply(HierarchicalZipWriter& writer,
+                 ServerContext& context,
+                 DicomDirWriter* dicomDir,
+                 const std::string& dicomDirFolder) const
+      {
+        switch (type_)
+        {
+          case Type_OpenDirectory:
+            writer.OpenDirectory(filename_.c_str());
+            break;
+
+          case Type_CloseDirectory:
+            writer.CloseDirectory();
+            break;
+
+          case Type_WriteInstance:
+          {
+            std::string content;
+
+            try
+            {
+              context.ReadAttachment(content, info_);
+            }
+            catch (OrthancException& e)
+            {
+              LOG(WARNING) << "An instance was removed after the job was issued: " << instanceId_;
+              return;
+            }
+
+            //boost::this_thread::sleep(boost::posix_time::milliseconds(300));
+            
+            writer.OpenFile(filename_.c_str());
+            writer.Write(content);
+
+            if (dicomDir != NULL)
+            {
+              ParsedDicomFile parsed(content);
+              dicomDir->Add(dicomDirFolder, filename_, parsed);
+            }
+              
+            break;
+          }
+
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+    };
+      
+    std::deque<Command*>  commands_;
+    uint64_t              uncompressedSize_;
+    unsigned int          instancesCount_;
+
+      
+    void ApplyInternal(HierarchicalZipWriter& writer,
+                       ServerContext& context,
+                       size_t index,
+                       DicomDirWriter* dicomDir,
+                       const std::string& dicomDirFolder) const
+    {
+      if (index >= commands_.size())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+
+      commands_[index]->Apply(writer, context, dicomDir, dicomDirFolder);
+    }
+      
+  public:
+    ZipCommands() :
+      uncompressedSize_(0),
+      instancesCount_(0)
+    {
+    }
+      
+    ~ZipCommands()
+    {
+      for (std::deque<Command*>::iterator it = commands_.begin();
+           it != commands_.end(); ++it)
+      {
+        assert(*it != NULL);
+        delete *it;
+      }
+    }
+
+    size_t GetSize() const
+    {
+      return commands_.size();
+    }
+
+    unsigned int GetInstancesCount() const
+    {
+      return instancesCount_;
+    }
+
+    uint64_t GetUncompressedSize() const
+    {
+      return uncompressedSize_;
+    }
+
+    void Apply(HierarchicalZipWriter& writer,
+               ServerContext& context,
+               size_t index,
+               DicomDirWriter& dicomDir,
+               const std::string& dicomDirFolder) const
+    {
+      ApplyInternal(writer, context, index, &dicomDir, dicomDirFolder);
+    }
+
+    void Apply(HierarchicalZipWriter& writer,
+               ServerContext& context,
+               size_t index) const
+    {
+      ApplyInternal(writer, context, index, NULL, "");
+    }
+      
+    void AddOpenDirectory(const std::string& filename)
+    {
+      commands_.push_back(new Command(Type_OpenDirectory, filename));
+    }
+
+    void AddCloseDirectory()
+    {
+      commands_.push_back(new Command(Type_CloseDirectory));
+    }
+
+    void AddWriteInstance(const std::string& filename,
+                          const std::string& instanceId,
+                          const FileInfo& info)
+    {
+      commands_.push_back(new Command(Type_WriteInstance, filename, instanceId, info));
+      instancesCount_ ++;
+      uncompressedSize_ += info.GetUncompressedSize();
+    }
+
+    bool IsZip64() const
+    {
+      return IsZip64Required(GetUncompressedSize(), GetInstancesCount());
+    }
+  };
+    
+    
+
+  class ArchiveJob::ArchiveIndexVisitor : public IArchiveVisitor
+  {
+  private:
+    ZipCommands&    commands_;
+    ServerContext&  context_;
+    char            instanceFormat_[24];
+    unsigned int    counter_;
+
+    static std::string GetTag(const DicomMap& tags,
+                              const DicomTag& tag)
+    {
+      const DicomValue* v = tags.TestAndGetValue(tag);
+      if (v != NULL &&
+          !v->IsBinary() &&
+          !v->IsNull())
+      {
+        return v->GetContent();
+      }
+      else
+      {
+        return "";
+      }
+    }
+
+  public:
+    ArchiveIndexVisitor(ZipCommands& commands,
+                        ServerContext& context) :
+      commands_(commands),
+      context_(context),
+      counter_(0)
+    {
+      if (commands.GetSize() != 0)
+      {
+        throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      }
+        
+      snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm");
+    }
+
+    virtual void Open(ResourceType level,
+                      const std::string& publicId)
+    {
+      std::string path;
+
+      DicomMap tags;
+      if (context_.GetIndex().GetMainDicomTags(tags, publicId, level, level))
+      {
+        switch (level)
+        {
+          case ResourceType_Patient:
+            path = GetTag(tags, DICOM_TAG_PATIENT_ID) + " " + GetTag(tags, DICOM_TAG_PATIENT_NAME);
+            break;
+
+          case ResourceType_Study:
+            path = GetTag(tags, DICOM_TAG_ACCESSION_NUMBER) + " " + GetTag(tags, DICOM_TAG_STUDY_DESCRIPTION);
+            break;
+
+          case ResourceType_Series:
+          {
+            std::string modality = GetTag(tags, DICOM_TAG_MODALITY);
+            path = modality + " " + GetTag(tags, DICOM_TAG_SERIES_DESCRIPTION);
+
+            if (modality.size() == 0)
+            {
+              snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%%08d.dcm");
+            }
+            else if (modality.size() == 1)
+            {
+              snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%%07d.dcm", 
+                       toupper(modality[0]));
+            }
+            else if (modality.size() >= 2)
+            {
+              snprintf(instanceFormat_, sizeof(instanceFormat_) - 1, "%c%c%%06d.dcm", 
+                       toupper(modality[0]), toupper(modality[1]));
+            }
+
+            counter_ = 0;
+
+            break;
+          }
+
+          default:
+            throw OrthancException(ErrorCode_InternalError);
+        }
+      }
+
+      path = Toolbox::StripSpaces(Toolbox::ConvertToAscii(path));
+
+      if (path.empty())
+      {
+        path = std::string("Unknown ") + EnumerationToString(level);
+      }
+
+      commands_.AddOpenDirectory(path.c_str());
+    }
+
+    virtual void Close()
+    {
+      commands_.AddCloseDirectory();
+    }
+
+    virtual void AddInstance(const std::string& instanceId,
+                             const FileInfo& dicom)
+    {
+      char filename[24];
+      snprintf(filename, sizeof(filename) - 1, instanceFormat_, counter_);
+      counter_ ++;
+
+      commands_.AddWriteInstance(filename, instanceId, dicom);
+    }
+  };
+
+    
+  class ArchiveJob::MediaIndexVisitor : public IArchiveVisitor
+  {
+  private:
+    ZipCommands&    commands_;
+    ServerContext&  context_;
+    unsigned int    counter_;
+
+  public:
+    MediaIndexVisitor(ZipCommands& commands,
+                      ServerContext& context) :
+      commands_(commands),
+      context_(context),
+      counter_(0)
+    {
+    }
+
+    virtual void Open(ResourceType level,
+                      const std::string& publicId)
+    {
+    }
+
+    virtual void Close()
+    {
+    }
+
+    virtual void AddInstance(const std::string& instanceId,
+                             const FileInfo& dicom)
+    {
+      // "DICOM restricts the filenames on DICOM media to 8
+      // characters (some systems wrongly use 8.3, but this does not
+      // conform to the standard)."
+      std::string filename = "IM" + boost::lexical_cast<std::string>(counter_);
+      commands_.AddWriteInstance(filename, instanceId, dicom);
+
+      counter_ ++;
+    }
+  };
+
+
+  class ArchiveJob::ZipWriterIterator : public boost::noncopyable
+  {
+  private:
+    TemporaryFile&                        target_;
+    ServerContext&                        context_;
+    ZipCommands                           commands_;
+    std::auto_ptr<HierarchicalZipWriter>  zip_;
+    std::auto_ptr<DicomDirWriter>         dicomDir_;
+    bool                                  isMedia_;
+
+  public:
+    ZipWriterIterator(TemporaryFile& target,
+                      ServerContext& context,
+                      ArchiveIndex& archive,
+                      bool isMedia,
+                      bool enableExtendedSopClass) :
+      target_(target),
+      context_(context),
+      isMedia_(isMedia)
+    {
+      if (isMedia)
+      {
+        MediaIndexVisitor visitor(commands_, context);
+        archive.Expand(context.GetIndex());
+
+        commands_.AddOpenDirectory(MEDIA_IMAGES_FOLDER);        
+        archive.Apply(visitor);
+        commands_.AddCloseDirectory();
+
+        dicomDir_.reset(new DicomDirWriter);
+        dicomDir_->EnableExtendedSopClass(enableExtendedSopClass);
+      }
+      else
+      {
+        ArchiveIndexVisitor visitor(commands_, context);
+        archive.Expand(context.GetIndex());
+        archive.Apply(visitor);
+      }
+
+      zip_.reset(new HierarchicalZipWriter(target.GetPath().c_str()));
+      zip_->SetZip64(commands_.IsZip64());
+    }
+      
+    size_t GetStepsCount() const
+    {
+      return commands_.GetSize() + 1;
+    }
+
+    void RunStep(size_t index)
+    {
+      if (index > commands_.GetSize())
+      {
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+      }
+      else if (index == commands_.GetSize())
+      {
+        // Last step: Add the DICOMDIR
+        if (isMedia_)
+        {
+          assert(dicomDir_.get() != NULL);
+          std::string s;
+          dicomDir_->Encode(s);
+
+          zip_->OpenFile("DICOMDIR");
+          zip_->Write(s);
+        }
+      }
+      else
+      {
+        if (isMedia_)
+        {
+          assert(dicomDir_.get() != NULL);
+          commands_.Apply(*zip_, context_, index, *dicomDir_, MEDIA_IMAGES_FOLDER);
+        }
+        else
+        {
+          assert(dicomDir_.get() == NULL);
+          commands_.Apply(*zip_, context_, index);
+        }
+      }
+    }
+
+    unsigned int GetInstancesCount() const
+    {
+      return commands_.GetInstancesCount();
+    }
+
+    uint64_t GetUncompressedSize() const
+    {
+      return commands_.GetUncompressedSize();
+    }
+  };
+
+
+  ArchiveJob::ArchiveJob(boost::shared_ptr<TemporaryFile>& target,
+                         ServerContext& context,
+                         bool isMedia,
+                         bool enableExtendedSopClass) :
+    target_(target),
+    context_(context),
+    archive_(new ArchiveIndex(ResourceType_Patient)),  // root
+    isMedia_(isMedia),
+    enableExtendedSopClass_(enableExtendedSopClass),
+    currentStep_(0),
+    instancesCount_(0),
+    uncompressedSize_(0)
+  {
+    if (target.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+  }
+
+
+  void ArchiveJob::AddResource(const std::string& publicId)
+  {
+    if (writer_.get() != NULL)   // Already started
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+        
+    ResourceIdentifiers resource(context_.GetIndex(), publicId);
+    archive_->Add(context_.GetIndex(), resource);
+  }
+
+  
+  void ArchiveJob::SignalResubmit()
+  {
+    LOG(ERROR) << "Cannot resubmit the creation of an archive";
+    throw OrthancException(ErrorCode_BadSequenceOfCalls);
+  }
+
+  
+  void ArchiveJob::Start()
+  {
+    if (writer_.get() != NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    writer_.reset(new ZipWriterIterator(*target_, context_, *archive_,
+                                        isMedia_, enableExtendedSopClass_));
+
+    instancesCount_ = writer_->GetInstancesCount();
+    uncompressedSize_ = writer_->GetUncompressedSize();
+  }
+
+  
+  JobStepResult ArchiveJob::ExecuteStep()
+  {
+    assert(writer_.get() != NULL);
+
+    if (target_.unique())
+    {
+      LOG(WARNING) << "A client has disconnected while creating an archive";
+      return JobStepResult::Failure(ErrorCode_NetworkProtocol);          
+    }
+        
+    if (writer_->GetStepsCount() == 0)
+    {
+      writer_.reset(NULL);  // Flush all the results
+      return JobStepResult::Success();
+    }
+    else
+    {
+      writer_->RunStep(currentStep_);
+
+      currentStep_ ++;
+
+      if (currentStep_ == writer_->GetStepsCount())
+      {
+        writer_.reset(NULL);  // Flush all the results
+        return JobStepResult::Success();
+      }
+      else
+      {
+        return JobStepResult::Continue();
+      }
+    }
+  }
+
+
+  float ArchiveJob::GetProgress()
+  {
+    if (writer_.get() == NULL ||
+        writer_->GetStepsCount() == 0)
+    {
+      return 1;
+    }
+    else
+    {
+      return (static_cast<float>(currentStep_) /
+              static_cast<float>(writer_->GetStepsCount() - 1));
+    }
+  }
+
+    
+  void ArchiveJob::GetJobType(std::string& target)
+  {
+    if (isMedia_)
+    {
+      target = "Media";
+    }
+    else
+    {
+      target = "Archive";
+    }
+  }
+
+    
+  void ArchiveJob::GetPublicContent(Json::Value& value)
+  {
+    value["Description"] = description_;
+    value["InstancesCount"] = instancesCount_;
+    value["UncompressedSizeMB"] =
+      static_cast<unsigned int>(uncompressedSize_ / MEGA_BYTES);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/ArchiveJob.h	Wed Jun 20 18:03:20 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/JobsEngine/IJob.h"
+#include "../../Core/TemporaryFile.h"
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class ArchiveJob : public IJob
+  {
+  private:
+    class ArchiveIndex;
+    class ArchiveIndexVisitor;
+    class IArchiveVisitor;
+    class MediaIndexVisitor;
+    class ResourceIdentifiers;
+    class ZipCommands;
+    class ZipWriterIterator;
+    class ZipWriterIterator;
+    
+    boost::shared_ptr<TemporaryFile>  target_;
+    ServerContext&                    context_;
+    std::auto_ptr<ArchiveIndex>       archive_;
+    bool                              isMedia_;
+    bool                              enableExtendedSopClass_;
+    std::string                       description_;
+
+    std::auto_ptr<ZipWriterIterator>  writer_;
+    size_t                            currentStep_;
+    unsigned int                      instancesCount_;
+    uint64_t                          uncompressedSize_;
+
+  public:
+    ArchiveJob(boost::shared_ptr<TemporaryFile>& target,
+               ServerContext& context,
+               bool isMedia,
+               bool enableExtendedSopClass);
+
+    void SetDescription(const std::string& description)
+    {
+      description_ = description;
+    }
+
+    const std::string& GetDescription() const
+    {
+      return description_;
+    }
+
+    void AddResource(const std::string& publicId);
+
+    virtual void SignalResubmit();
+
+    virtual void Start();
+
+    virtual JobStepResult ExecuteStep();
+
+    virtual void ReleaseResources()
+    {
+    }
+
+    virtual float GetProgress();
+
+    virtual void GetJobType(std::string& target);
+    
+    virtual void GetPublicContent(Json::Value& value);
+
+    virtual bool Serialize(Json::Value& value)
+    {
+      return false;  // Cannot serialize this kind of job
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,222 @@
+/**
+ * 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"
+#include "../../Core/SerializationToolbox.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)
+  {
+    assert(IsStarted());
+    OpenConnection();
+
+    LOG(INFO) << "Sending instance " << instance << " to modality \"" 
+              << remote_.GetApplicationEntityTitle() << "\"";
+
+    std::string dicom;
+
+    try
+    {
+      context_.ReadDicom(dicom, instance);
+    }
+    catch (OrthancException& e)
+    {
+      LOG(WARNING) << "An instance was removed after the job was issued: " << instance;
+      return false;
+    }
+
+    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)
+  {
+    SetOfInstancesJob::GetPublicContent(value);
+    
+    value["LocalAet"] = localAet_;
+    value["RemoteAet"] = remote_.GetApplicationEntityTitle();
+
+    if (HasMoveOriginator())
+    {
+      value["MoveOriginatorAET"] = GetMoveOriginatorAet();
+      value["MoveOriginatorID"] = GetMoveOriginatorId();
+    }
+  }
+
+
+  static const char* LOCAL_AET = "LocalAet";
+  static const char* REMOTE = "Remote";
+  static const char* MOVE_ORIGINATOR_AET = "MoveOriginatorAet";
+  static const char* MOVE_ORIGINATOR_ID = "MoveOriginatorId";
+  
+
+  DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context,
+                                               const Json::Value& serialized) :
+    SetOfInstancesJob(serialized),
+    context_(context)
+  {
+    localAet_ = SerializationToolbox::ReadString(serialized, LOCAL_AET);
+    remote_ = RemoteModalityParameters(serialized[REMOTE]);
+    moveOriginatorAet_ = SerializationToolbox::ReadString(serialized, MOVE_ORIGINATOR_AET);
+    moveOriginatorId_ = static_cast<uint16_t>
+      (SerializationToolbox::ReadUnsignedInteger(serialized, MOVE_ORIGINATOR_ID));
+  }
+
+
+  bool DicomModalityStoreJob::Serialize(Json::Value& target)
+  {
+    if (!SetOfInstancesJob::Serialize(target))
+    {
+      return false;
+    }
+    else
+    {
+      target[LOCAL_AET] = localAet_;
+      remote_.Serialize(target[REMOTE]);
+      target[MOVE_ORIGINATOR_AET] = moveOriginatorAet_;
+      target[MOVE_ORIGINATOR_ID] = moveOriginatorId_;
+      return true;
+    }
+  }  
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.h	Wed Jun 20 18:03:20 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 "../../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);
+
+    DicomModalityStoreJob(ServerContext& context,
+                          const Json::Value& serialized);
+
+    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);
+
+    virtual bool Serialize(Json::Value& target);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/LuaJobManager.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,268 @@
+/**
+ * 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 "Operations/DeleteResourceOperation.h"
+#include "Operations/ModifyInstanceOperation.h"
+#include "Operations/StorePeerOperation.h"
+#include "Operations/StoreScuOperation.h"
+#include "Operations/SystemCallOperation.h"
+
+#include "../../Core/JobsEngine/Operations/NullOperationValue.h"
+#include "../../Core/JobsEngine/Operations/StringOperationValue.h"
+#include "Operations/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()
+  {
+    bool isEmpty;
+    
+    assert(jobLock_.get() != NULL);
+    isEmpty = (isNewJob_ &&
+               jobLock_->GetOperationsCount() == 0);
+    
+    jobLock_.reset(NULL);
+
+    if (isNewJob_)
+    {
+      if (isEmpty)
+      {
+        // No operation was added, discard the newly created job
+        isNewJob_ = false;
+        delete that_.currentJob_;
+        that_.currentJob_ = NULL;
+      }
+      else
+      {
+        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 Jun 20 18:03:20 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/Operations/DeleteResourceOperation.cpp	Wed Jun 20 18:03:20 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/Operations/DeleteResourceOperation.h	Wed Jun 20 18:03:20 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 "../../../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 = Json::objectValue;
+      result["Type"] = "DeleteResource";
+    }
+  };
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.h	Wed Jun 20 18:03:20 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 "../../../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 ReadDicom(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 = Json::objectValue;
+      target["Type"] = "DicomInstance";
+      target["ID"] = id_;
+    }
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,153 @@
+/**
+ * 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"
+#include "../../../Core/SerializationToolbox.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.SetOrigin(DicomInstanceOrigin::FromLua());
+      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 = Json::objectValue;
+    target["Type"] = "ModifyInstance";
+    target["Origin"] = EnumerationToString(origin_);
+    modification_->Serialize(target["Modification"]);
+  }
+
+
+  ModifyInstanceOperation::ModifyInstanceOperation(ServerContext& context,
+                                                   const Json::Value& serialized) :
+    context_(context)
+  {
+    if (SerializationToolbox::ReadString(serialized, "Type") != "ModifyInstance" ||
+        !serialized.isMember("Modification"))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    origin_ = StringToRequestOrigin(SerializationToolbox::ReadString(serialized, "Origin"));
+
+    modification_.reset(new DicomModification(serialized["Modification"]));
+  }
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.h	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,75 @@
+/**
+ * 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
+
+    ModifyInstanceOperation(ServerContext& context,
+                            const Json::Value& serialized);
+
+    const RequestOrigin& GetRequestOrigin() const
+    {
+      return origin_;
+    }
+    
+    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/Operations/StorePeerOperation.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,104 @@
+/**
+ * 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"
+#include "../../../Core/SerializationToolbox.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.ReadDicom(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 = Json::objectValue;
+    result["Type"] = "StorePeer";
+    peer_.Serialize(result["Peer"]);
+  }
+
+
+  StorePeerOperation::StorePeerOperation(const Json::Value& serialized)
+  {
+    if (SerializationToolbox::ReadString(serialized, "Type") != "StorePeer" ||
+        !serialized.isMember("Peer"))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    peer_ = WebServiceParameters(serialized["Peer"]);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/Operations/StorePeerOperation.h	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,66 @@
+/**
+ * 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)
+    {
+    }
+
+    StorePeerOperation(const Json::Value& serialized);
+
+    const WebServiceParameters& GetPeer() const
+    {
+      return 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/Operations/StoreScuOperation.cpp	Wed Jun 20 18:03:20 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/>.
+ **/
+
+
+#include "../../PrecompiledHeadersServer.h"
+#include "StoreScuOperation.h"
+
+#include "DicomInstanceOperationValue.h"
+
+#include "../../../Core/Logging.h"
+#include "../../../Core/OrthancException.h"
+#include "../../../Core/SerializationToolbox.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.ReadDicom(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 = Json::objectValue;
+    result["Type"] = "StoreScu";
+    result["LocalAET"] = localAet_;
+    modality_.Serialize(result["Modality"]);
+  }
+
+
+  StoreScuOperation::StoreScuOperation(const Json::Value& serialized)
+  {
+    if (SerializationToolbox::ReadString(serialized, "Type") != "StoreScu" ||
+        !serialized.isMember("LocalAET"))
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    localAet_ = SerializationToolbox::ReadString(serialized, "LocalAET");
+    modality_ = RemoteModalityParameters(serialized["Modality"]);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/Operations/StoreScuOperation.h	Wed Jun 20 18:03:20 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 "../../../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)
+    {
+    }
+
+    StoreScuOperation(const Json::Value& serialized);
+
+    const std::string& GetLocalAet() const
+    {
+      return localAet_;
+    }
+
+    const RemoteModalityParameters& GetRemoteModality() const
+    {
+      return 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/Operations/SystemCallOperation.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,165 @@
+/**
+ * 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/OrthancException.h"
+#include "../../../Core/SerializationToolbox.h"
+#include "../../../Core/TemporaryFile.h"
+#include "../../../Core/Toolbox.h"
+
+namespace Orthanc
+{
+  const std::string& SystemCallOperation::GetPreArgument(size_t i) const
+  {
+    if (i >= preArguments_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return preArguments_[i];
+    }
+  }
+
+  
+  const std::string& SystemCallOperation::GetPostArgument(size_t i) const
+  {
+    if (i >= postArguments_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      return postArguments_[i];
+    }
+  }
+
+
+  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.ReadDicom(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 = Json::objectValue;
+    result["Type"] = "SystemCall";
+    result["Command"] = command_;
+    SerializationToolbox::WriteArrayOfStrings(result, preArguments_, "PreArguments");
+    SerializationToolbox::WriteArrayOfStrings(result, postArguments_, "PostArguments");
+  }
+
+
+  SystemCallOperation::SystemCallOperation(const Json::Value& serialized)
+  {
+    if (SerializationToolbox::ReadString(serialized, "Type") != "SystemCall")
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+
+    command_ = SerializationToolbox::ReadString(serialized, "Command");
+    SerializationToolbox::ReadArrayOfStrings(preArguments_, serialized, "PreArguments");
+    SerializationToolbox::ReadArrayOfStrings(postArguments_, serialized, "PostArguments");
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/Operations/SystemCallOperation.h	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,102 @@
+/**
+ * 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 Json::Value& serialized);
+
+    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);
+    }
+
+    const std::string& GetCommand() const
+    {
+      return command_;
+    }
+
+    size_t GetPreArgumentsCount() const
+    {
+      return preArguments_.size();
+    }
+
+    size_t GetPostArgumentsCount() const
+    {
+      return postArguments_.size();
+    }
+
+    const std::string& GetPreArgument(size_t i) const;
+
+    const std::string& GetPostArgument(size_t i) const;
+
+    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/OrthancJobUnserializer.cpp	Wed Jun 20 18:03:20 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/>.
+ **/
+
+
+#include "../PrecompiledHeadersServer.h"
+#include "OrthancJobUnserializer.h"
+
+#include "../../Core/Logging.h"
+#include "../../Core/OrthancException.h"
+#include "../../Core/SerializationToolbox.h"
+
+#include "Operations/DeleteResourceOperation.h"
+#include "Operations/DicomInstanceOperationValue.h"
+#include "Operations/ModifyInstanceOperation.h"
+#include "Operations/StorePeerOperation.h"
+#include "Operations/StoreScuOperation.h"
+#include "Operations/SystemCallOperation.h"
+
+#include "DicomModalityStoreJob.h"
+#include "OrthancPeerStoreJob.h"
+#include "ResourceModificationJob.h"
+
+namespace Orthanc
+{
+  IJob* OrthancJobUnserializer::UnserializeJob(const Json::Value& source)
+  {
+    const std::string type = SerializationToolbox::ReadString(source, "Type");
+
+    if (type == "DicomModalityStore")
+    {
+      return new DicomModalityStoreJob(context_, source);
+    }
+    else if (type == "OrthancPeerStore")
+    {
+      return new OrthancPeerStoreJob(context_, source);
+    }
+    else if (type == "ResourceModification")
+    {
+      return new ResourceModificationJob(context_, source);
+    }
+    else
+    {
+      return GenericJobUnserializer::UnserializeJob(source);
+    }
+  }
+
+
+  IJobOperation* OrthancJobUnserializer::UnserializeOperation(const Json::Value& source)
+  {
+    const std::string type = SerializationToolbox::ReadString(source, "Type");
+
+    if (type == "DeleteResource")
+    {
+      return new DeleteResourceOperation(context_);
+    }
+    else if (type == "ModifyInstance")
+    {
+      return new ModifyInstanceOperation(context_, source);
+    }
+    else if (type == "StorePeer")
+    {
+      return new StorePeerOperation(source);
+    }
+    else if (type == "StoreScu")
+    {
+      return new StoreScuOperation(source);
+    }
+    else if (type == "SystemCall")
+    {
+      return new SystemCallOperation(source);
+    }
+    else
+    {
+      return GenericJobUnserializer::UnserializeOperation(source);
+    }
+  }
+
+
+  JobOperationValue* OrthancJobUnserializer::UnserializeValue(const Json::Value& source)
+  {
+    const std::string type = SerializationToolbox::ReadString(source, "Type");
+
+    if (type == "DicomInstance")
+    {
+      return new DicomInstanceOperationValue(context_, SerializationToolbox::ReadString(source, "ID"));
+    }
+    else
+    {
+      return GenericJobUnserializer::UnserializeValue(source);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/OrthancJobUnserializer.h	Wed Jun 20 18:03:20 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& value);
+
+    virtual IJobOperation* UnserializeOperation(const Json::Value& value);
+
+    virtual JobOperationValue* UnserializeValue(const Json::Value& value);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,129 @@
+/**
+ * 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() << "\"";
+
+    try
+    {
+      context_.ReadDicom(client_->GetBody(), instance);
+    }
+    catch (OrthancException& e)
+    {
+      LOG(WARNING) << "An instance was removed after the job was issued: " << instance;
+      return false;
+    }
+
+    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)
+  {
+    SetOfInstancesJob::GetPublicContent(value);
+    
+    Json::Value v;
+    peer_.ToJson(v, false /* don't include passwords */);
+    value["Peer"] = v;
+  }
+
+
+  static const char* PEER = "Peer";
+
+  OrthancPeerStoreJob::OrthancPeerStoreJob(ServerContext& context,
+                                           const Json::Value& serialized) :
+    SetOfInstancesJob(serialized),
+    context_(context)
+  {
+    peer_ = WebServiceParameters(serialized[PEER]);
+  }
+
+
+  bool OrthancPeerStoreJob::Serialize(Json::Value& target)
+  {
+    if (!SetOfInstancesJob::Serialize(target))
+    {
+      return false;
+    }
+    else
+    {
+      peer_.Serialize(target[PEER]);
+      return true;
+    }
+  }  
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.h	Wed Jun 20 18:03:20 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/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)
+    {
+    }
+
+    OrthancPeerStoreJob(ServerContext& context,
+                        const Json::Value& serialize);
+
+    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);
+
+    virtual bool Serialize(Json::Value& target);
+  };
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/ResourceModificationJob.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -0,0 +1,321 @@
+/**
+ * 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 "ResourceModificationJob.h"
+
+#include "../../Core/Logging.h"
+#include "../../Core/SerializationToolbox.h"
+
+namespace Orthanc
+{
+  ResourceModificationJob::Output::Output(ResourceType  level) :
+    level_(level),
+    isFirst_(true)
+  {
+    if (level_ != ResourceType_Patient &&
+        level_ != ResourceType_Study &&
+        level_ != ResourceType_Series)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }            
+  }
+
+
+  void ResourceModificationJob::Output::Update(DicomInstanceHasher& hasher)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+        
+    if (isFirst_)
+    {
+      switch (level_)
+      {
+        case ResourceType_Series:
+          id_ = hasher.HashSeries();
+          break;
+
+        case ResourceType_Study:
+          id_ = hasher.HashStudy();
+          break;
+
+        case ResourceType_Patient:
+          id_ = hasher.HashPatient();
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+
+      patientId_ = hasher.HashPatient();
+      isFirst_ = false;
+    }
+  }
+
+
+  bool ResourceModificationJob::Output::Format(Json::Value& target)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+        
+    if (isFirst_)
+    {
+      return false;
+    }
+    else
+    {
+      target = Json::objectValue;
+      target["Type"] = EnumerationToString(level_);
+      target["ID"] = id_;
+      target["Path"] = GetBasePath(level_, id_);
+      target["PatientID"] = patientId_;
+      return true;
+    }
+  }
+
+  
+  bool ResourceModificationJob::Output::GetIdentifier(std::string& id)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+        
+    if (isFirst_)
+    {
+      return false;
+    }
+    else
+    {
+      id = id_;
+      return true;
+    }
+  }
+
+
+  bool ResourceModificationJob::HandleInstance(const std::string& instance)
+  {
+    if (modification_.get() == NULL)
+    {
+      LOG(ERROR) << "No modification was provided for this job";
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+      
+    LOG(INFO) << "Modifying instance in a job: " << instance;
+
+    std::auto_ptr<ServerContext::DicomCacheLocker> locker;
+
+    try
+    {
+      locker.reset(new ServerContext::DicomCacheLocker(context_, instance));
+    }
+    catch (OrthancException&)
+    {
+      LOG(WARNING) << "An instance was removed after the job was issued: " << instance;
+      return false;
+    }
+
+
+    ParsedDicomFile& original = locker->GetDicom();
+    DicomInstanceHasher originalHasher = original.GetHasher();
+
+
+    /**
+     * Compute the resulting DICOM instance.
+     **/
+
+    std::auto_ptr<ParsedDicomFile> modified(original.Clone(true));
+    modification_->Apply(*modified);
+
+    DicomInstanceToStore toStore;
+    toStore.SetOrigin(origin_);
+    toStore.SetParsedDicomFile(*modified);
+
+
+    /**
+     * Prepare the metadata information to associate with the
+     * resulting DICOM instance (AnonymizedFrom/ModifiedFrom).
+     **/
+
+    DicomInstanceHasher modifiedHasher = modified->GetHasher();
+      
+    MetadataType metadataType = (isAnonymization_ ?
+                                 MetadataType_AnonymizedFrom :
+                                 MetadataType_ModifiedFrom);
+
+    if (originalHasher.HashSeries() != modifiedHasher.HashSeries())
+    {
+      toStore.AddMetadata(ResourceType_Series, metadataType, originalHasher.HashSeries());
+    }
+
+    if (originalHasher.HashStudy() != modifiedHasher.HashStudy())
+    {
+      toStore.AddMetadata(ResourceType_Study, metadataType, originalHasher.HashStudy());
+    }
+
+    if (originalHasher.HashPatient() != modifiedHasher.HashPatient())
+    {
+      toStore.AddMetadata(ResourceType_Patient, metadataType, originalHasher.HashPatient());
+    }
+
+    assert(instance == originalHasher.HashInstance());
+    toStore.AddMetadata(ResourceType_Instance, metadataType, instance);
+
+
+    /**
+     * Store the resulting DICOM instance into the Orthanc store.
+     **/
+
+    std::string modifiedInstance;
+    if (context_.Store(modifiedInstance, toStore) != StoreStatus_Success)
+    {
+      LOG(ERROR) << "Error while storing a modified instance " << instance;
+      throw OrthancException(ErrorCode_CannotStoreInstance);
+    }
+
+    // Sanity checks in debug mode
+    assert(modifiedInstance == modifiedHasher.HashInstance());
+
+
+    if (output_.get() != NULL)
+    {
+      output_->Update(modifiedHasher);
+    }
+
+    return true;
+  }
+    
+
+  
+  void ResourceModificationJob::SetModification(DicomModification* modification,
+                                                bool isAnonymization)
+  {
+    if (modification == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      modification_.reset(modification);
+      isAnonymization_ = isAnonymization;
+    }
+  }
+
+
+  void ResourceModificationJob::SetOutput(boost::shared_ptr<Output>& output)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      output_ = output;
+    }
+  }
+
+  
+  void ResourceModificationJob::SetOrigin(const DicomInstanceOrigin& origin)
+  {
+    if (IsStarted())
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      origin_ = origin;
+    }
+  }
+
+  
+  void ResourceModificationJob::SetOrigin(const RestApiCall& call)
+  {
+    SetOrigin(DicomInstanceOrigin::FromRest(call));
+  }
+
+
+  const DicomModification& ResourceModificationJob::GetModification() const
+  {
+    if (modification_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      return *modification_;
+    }
+  }
+
+
+  void ResourceModificationJob::GetPublicContent(Json::Value& value)
+  {
+    SetOfInstancesJob::GetPublicContent(value);
+
+    value["IsAnonymization"] = isAnonymization_;
+  }
+
+
+  static const char* MODIFICATION = "Modification";
+  static const char* ORIGIN = "Origin";
+  static const char* IS_ANONYMIZATION = "IsAnonymization";
+  
+
+  ResourceModificationJob::ResourceModificationJob(ServerContext& context,
+                                                   const Json::Value& serialized) :
+    SetOfInstancesJob(serialized),
+    context_(context)
+  {
+    isAnonymization_ = SerializationToolbox::ReadBoolean(serialized, IS_ANONYMIZATION);
+    origin_ = DicomInstanceOrigin(serialized[ORIGIN]);
+    modification_.reset(new DicomModification(serialized[MODIFICATION]));
+  }
+  
+  bool ResourceModificationJob::Serialize(Json::Value& value)
+  {
+    if (!SetOfInstancesJob::Serialize(value))
+    {
+      return false;
+    }
+    else
+    {
+      value[IS_ANONYMIZATION] = isAnonymization_;
+      origin_.Serialize(value[ORIGIN]);
+      
+      Json::Value tmp;
+      modification_->Serialize(tmp);
+      value[MODIFICATION] = tmp;
+
+      return true;
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/ServerJobs/ResourceModificationJob.h	Wed Jun 20 18:03:20 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/>.
+ **/
+
+
+#pragma once
+
+#include "../../Core/JobsEngine/SetOfInstancesJob.h"
+#include "../ServerContext.h"
+
+namespace Orthanc
+{
+  class ResourceModificationJob : public SetOfInstancesJob
+  {
+  public:
+    class Output : public boost::noncopyable
+    {
+    private:
+      boost::mutex  mutex_;
+      ResourceType  level_;
+      bool          isFirst_;
+      std::string   id_;
+      std::string   patientId_;
+
+    public:
+      Output(ResourceType  level);
+
+      ResourceType GetLevel() const
+      {
+        return level_;
+      }
+
+      void Update(DicomInstanceHasher& hasher);
+
+      bool Format(Json::Value& target);
+
+      bool GetIdentifier(std::string& id);
+    };
+    
+  private:
+    ServerContext&                    context_;
+    std::auto_ptr<DicomModification>  modification_;
+    boost::shared_ptr<Output>         output_;
+    bool                              isAnonymization_;
+    DicomInstanceOrigin               origin_;
+
+  protected:
+    virtual bool HandleInstance(const std::string& instance);
+    
+  public:
+    ResourceModificationJob(ServerContext& context) :
+      context_(context),
+      isAnonymization_(false)
+    {
+    }
+
+    ResourceModificationJob(ServerContext& context,
+                            const Json::Value& serialized);
+
+    void SetModification(DicomModification* modification,   // Takes ownership
+                         bool isAnonymization);
+
+    void SetOutput(boost::shared_ptr<Output>& output);
+
+    void SetOrigin(const DicomInstanceOrigin& origin);
+
+    void SetOrigin(const RestApiCall& call);
+
+    const DicomModification& GetModification() const;
+
+    bool IsAnonymization() const
+    {
+      return isAnonymization_;
+    }
+
+    const DicomInstanceOrigin& GetOrigin() const
+    {
+      return origin_;
+    }
+
+    virtual void ReleaseResources()
+    {
+    }
+
+    virtual void GetJobType(std::string& target)
+    {
+      target = "ResourceModification";
+    }
+
+    virtual void GetPublicContent(Json::Value& value);
+    
+    virtual bool Serialize(Json::Value& value);
+  };
+}
--- a/OrthancServer/main.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/OrthancServer/main.cpp	Wed Jun 20 18:03:20 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"
@@ -76,7 +75,8 @@
     if (dicomFile.size() > 0)
     {
       DicomInstanceToStore toStore;
-      toStore.SetDicomProtocolOrigin(remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str());
+      toStore.SetOrigin(DicomInstanceOrigin::FromDicomProtocol
+                        (remoteIp.c_str(), remoteAet.c_str(), calledAet.c_str()));
       toStore.SetBuffer(dicomFile);
       toStore.SetSummary(dicomSummary);
       toStore.SetJson(dicomJson);
@@ -168,9 +168,9 @@
 class OrthancApplicationEntityFilter : public IApplicationEntityFilter
 {
 private:
-  ServerContext& context_;
-  bool           alwaysAllowEcho_;
-  bool           alwaysAllowStore_;
+  ServerContext&  context_;
+  bool            alwaysAllowEcho_;
+  bool            alwaysAllowStore_;
 
 public:
   OrthancApplicationEntityFilter(ServerContext& context) :
@@ -264,13 +264,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 +291,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 +311,7 @@
 class MyIncomingHttpRequestFilter : public IIncomingHttpRequestFilter
 {
 private:
-  ServerContext& context_;
+  ServerContext&   context_;
   OrthancPlugins*  plugins_;
 
 public:
@@ -327,7 +327,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 +337,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)
       {
@@ -488,6 +488,8 @@
     << "  --upgrade\t\tallow Orthanc to upgrade the version of the" << std::endl
     << "\t\t\tdatabase (beware that the database will become" << std::endl
     << "\t\t\tincompatible with former versions of Orthanc)" << std::endl
+    << "  --no-jobs\t\tDon't restart the jobs that were stored during" << std::endl
+    << "\t\t\tthe last execution of Orthanc" << std::endl
     << "  --version\t\toutput version information and exit" << std::endl
     << std::endl
     << "Exit status:" << std::endl
@@ -574,6 +576,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 +643,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 +673,8 @@
   }
 #endif
 
-  context.GetLua().Execute("Initialize");
+  context.GetLuaScripting().Start();
+  context.GetLuaScripting().Execute("Initialize");
 
   bool restart;
 
@@ -723,7 +708,8 @@
     }
   }
 
-  context.GetLua().Execute("Finalize");
+  context.GetLuaScripting().Execute("Finalize");
+  context.GetLuaScripting().Stop();
 
 #if ORTHANC_ENABLE_PLUGINS == 1
   if (context.HasPlugins())
@@ -972,7 +958,8 @@
 
 static bool ConfigureServerContext(IDatabaseWrapper& database,
                                    IStorageArea& storageArea,
-                                   OrthancPlugins *plugins)
+                                   OrthancPlugins *plugins,
+                                   bool loadJobsFromDatabase)
 {
   // These configuration options must be set before creating the
   // ServerContext, otherwise the possible Lua scripts will not be
@@ -985,7 +972,7 @@
 
   DicomUserConnection::SetDefaultTimeout(Configuration::GetGlobalUnsignedIntegerParameter("DicomScuTimeout", 10));
 
-  ServerContext context(database, storageArea);
+  ServerContext context(database, storageArea, false /* not running unit tests */, loadJobsFromDatabase);
   context.SetCompressionEnabled(Configuration::GetGlobalBoolParameter("StorageCompression", false));
   context.SetStoreMD5ForAttachments(Configuration::GetGlobalBoolParameter("StoreMD5ForAttachments", true));
 
@@ -1008,7 +995,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)
@@ -1052,7 +1043,8 @@
 static bool ConfigureDatabase(IDatabaseWrapper& database,
                               IStorageArea& storageArea,
                               OrthancPlugins *plugins,
-                              bool upgradeDatabase)
+                              bool upgradeDatabase,
+                              bool loadJobsFromDatabase)
 {
   database.Open();
 
@@ -1071,7 +1063,8 @@
     throw OrthancException(ErrorCode_IncompatibleDatabaseVersion);
   }
 
-  bool success = ConfigureServerContext(database, storageArea, plugins);
+  bool success = ConfigureServerContext
+    (database, storageArea, plugins, loadJobsFromDatabase);
 
   database.Close();
 
@@ -1081,7 +1074,8 @@
 
 static bool ConfigurePlugins(int argc, 
                              char* argv[],
-                             bool upgradeDatabase)
+                             bool upgradeDatabase,
+                             bool loadJobsFromDatabase)
 {
   std::auto_ptr<IDatabaseWrapper>  databasePtr;
   std::auto_ptr<IStorageArea>  storage;
@@ -1116,14 +1110,16 @@
   assert(database != NULL);
   assert(storage.get() != NULL);
 
-  return ConfigureDatabase(*database, *storage, &plugins, upgradeDatabase);
+  return ConfigureDatabase(*database, *storage, &plugins,
+                           upgradeDatabase, loadJobsFromDatabase);
 
 #elif ORTHANC_ENABLE_PLUGINS == 0
   // The plugins are disabled
   databasePtr.reset(Configuration::CreateDatabaseWrapper());
   storage.reset(Configuration::CreateStorageArea());
 
-  return ConfigureDatabase(*databasePtr, *storage, NULL, upgradeDatabase);
+  return ConfigureDatabase(*databasePtr, *storage, NULL,
+                           upgradeDatabase, loadJobsFromDatabase);
 
 #else
 #  error The macro ORTHANC_ENABLE_PLUGINS must be set to 0 or 1
@@ -1133,9 +1129,10 @@
 
 static bool StartOrthanc(int argc, 
                          char* argv[],
-                         bool upgradeDatabase)
+                         bool upgradeDatabase,
+                         bool loadJobsFromDatabase)
 {
-  return ConfigurePlugins(argc, argv, upgradeDatabase);
+  return ConfigurePlugins(argc, argv, upgradeDatabase, loadJobsFromDatabase);
 }
 
 
@@ -1152,6 +1149,7 @@
   Logging::Initialize();
 
   bool upgradeDatabase = false;
+  bool loadJobsFromDatabase = true;
   const char* configurationFile = NULL;
 
 
@@ -1242,6 +1240,10 @@
     {
       upgradeDatabase = true;
     }
+    else if (argument == "--no-jobs")
+    {
+      loadJobsFromDatabase = false;
+    }
     else if (boost::starts_with(argument, "--config="))
     {
       // TODO WHAT IS THE ENCODING?
@@ -1305,7 +1307,7 @@
     {
       OrthancInitialize(configurationFile);
 
-      bool restart = StartOrthanc(argc, argv, upgradeDatabase);
+      bool restart = StartOrthanc(argc, argv, upgradeDatabase, loadJobsFromDatabase);
       if (restart)
       {
         OrthancFinalize();
--- a/Plugins/Engine/OrthancPlugins.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/Plugins/Engine/OrthancPlugins.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -1544,7 +1544,7 @@
     switch (service)
     {
       case _OrthancPluginService_GetInstanceRemoteAet:
-        *p.resultString = instance.GetRemoteAet();
+        *p.resultString = instance.GetOrigin().GetRemoteAetC();
         return;
 
       case _OrthancPluginService_GetInstanceSize:
@@ -1585,7 +1585,7 @@
       }
 
       case _OrthancPluginService_GetInstanceOrigin:   // New in Orthanc 0.9.5
-        *p.resultOrigin = Plugins::Convert(instance.GetRequestOrigin());
+        *p.resultOrigin = Plugins::Convert(instance.GetOrigin().GetRequestOrigin());
         return;
 
       default:
@@ -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 Jun 20 18:02:36 2018 +0200
+++ b/Plugins/Engine/OrthancPlugins.h	Wed Jun 20 18:03:20 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 Jun 20 18:02:36 2018 +0200
+++ b/Plugins/Include/orthanc/OrthancCPlugin.h	Wed Jun 20 18:03:20 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/OpenSslConfiguration.cmake	Wed Jun 20 18:02:36 2018 +0200
+++ b/Resources/CMake/OpenSslConfiguration.cmake	Wed Jun 20 18:03:20 2018 +0200
@@ -34,7 +34,7 @@
     -DOPENSSL_NO_KRB5 
     -DOPENSSL_NO_MD2 
     -DOPENSSL_NO_MDC2 
-    -DOPENSSL_NO_MD4
+    #-DOPENSSL_NO_MD4   # MD4 is necessary for MariaDB/MySQL client
     -DOPENSSL_NO_RC2 
     -DOPENSSL_NO_RC4 
     -DOPENSSL_NO_RC5 
@@ -75,6 +75,7 @@
     ${OPENSSL_SOURCES_DIR}/crypto/evp
     ${OPENSSL_SOURCES_DIR}/crypto/hmac
     ${OPENSSL_SOURCES_DIR}/crypto/lhash
+    ${OPENSSL_SOURCES_DIR}/crypto/md4
     ${OPENSSL_SOURCES_DIR}/crypto/md5
     ${OPENSSL_SOURCES_DIR}/crypto/modes
     ${OPENSSL_SOURCES_DIR}/crypto/objects
@@ -96,6 +97,12 @@
     ${OPENSSL_SOURCES_DIR}/ssl
     )
 
+  if (ENABLE_OPENSSL_ENGINES)
+    list(APPEND OPENSSL_SOURCES_SUBDIRS
+      ${OPENSSL_SOURCES_DIR}/engines
+      )
+  endif()
+
   if (ENABLE_PKCS11)
     list(APPEND OPENSSL_SOURCES_SUBDIRS
       # EC, ECDH and ECDSA are necessary for PKCS11
@@ -136,6 +143,9 @@
     ${OPENSSL_SOURCES_DIR}/crypto/evp/e_dsa.c
     ${OPENSSL_SOURCES_DIR}/crypto/evp/m_ripemd.c
     ${OPENSSL_SOURCES_DIR}/crypto/lhash/lh_test.c
+    ${OPENSSL_SOURCES_DIR}/crypto/md4/md4.c
+    ${OPENSSL_SOURCES_DIR}/crypto/md4/md4s.cpp
+    ${OPENSSL_SOURCES_DIR}/crypto/md4/md4test.c
     ${OPENSSL_SOURCES_DIR}/crypto/md5/md5s.cpp
     ${OPENSSL_SOURCES_DIR}/crypto/pkcs7/bio_ber.c
     ${OPENSSL_SOURCES_DIR}/crypto/pkcs7/pk7_enc.c
@@ -203,6 +213,10 @@
       ${OPENSSL_SOURCES}
       PROPERTIES COMPILE_DEFINITIONS
       "OPENSSL_SYSNAME_WIN32;SO_WIN32;WIN32_LEAN_AND_MEAN;L_ENDIAN")
+
+    if (ENABLE_OPENSSL_ENGINES)
+      link_libraries(crypt32)
+    endif()
   endif()
 
   source_group(ThirdParty\\OpenSSL REGULAR_EXPRESSION ${OPENSSL_SOURCES_DIR}/.*)
--- a/Resources/CMake/OrthancFrameworkConfiguration.cmake	Wed Jun 20 18:02:36 2018 +0200
+++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake	Wed Jun 20 18:03:20 2018 +0200
@@ -43,6 +43,7 @@
 if (NOT ENABLE_CRYPTO_OPTIONS)
   unset(ENABLE_SSL CACHE)
   unset(ENABLE_PKCS11 CACHE)
+  unset(ENABLE_OPENSSL_ENGINES CACHE)
   unset(USE_SYSTEM_OPENSSL CACHE)
   unset(USE_SYSTEM_LIBP11 CACHE)
   add_definitions(
@@ -130,6 +131,7 @@
   ${ORTHANC_ROOT}/Core/DicomFormat/DicomTag.cpp
   ${ORTHANC_ROOT}/Core/DicomFormat/DicomValue.cpp
   ${ORTHANC_ROOT}/Core/Enumerations.cpp
+  ${ORTHANC_ROOT}/Core/FileStorage/MemoryStorageArea.cpp
   ${ORTHANC_ROOT}/Core/Images/Font.cpp
   ${ORTHANC_ROOT}/Core/Images/FontRegistry.cpp
   ${ORTHANC_ROOT}/Core/Images/IImageWriter.cpp
@@ -137,7 +139,16 @@
   ${ORTHANC_ROOT}/Core/Images/ImageAccessor.cpp
   ${ORTHANC_ROOT}/Core/Images/ImageBuffer.cpp
   ${ORTHANC_ROOT}/Core/Images/ImageProcessing.cpp
+  ${ORTHANC_ROOT}/Core/JobsEngine/GenericJobUnserializer.cpp
+  ${ORTHANC_ROOT}/Core/JobsEngine/JobInfo.cpp
+  ${ORTHANC_ROOT}/Core/JobsEngine/JobStatus.cpp
+  ${ORTHANC_ROOT}/Core/JobsEngine/JobStepResult.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/Logging.cpp
+  ${ORTHANC_ROOT}/Core/SerializationToolbox.cpp
   ${ORTHANC_ROOT}/Core/Toolbox.cpp
   ${ORTHANC_ROOT}/Core/WebServiceParameters.cpp
   )
@@ -177,6 +188,7 @@
     include(${CMAKE_CURRENT_LIST_DIR}/OpenSslConfiguration.cmake)
     add_definitions(-DORTHANC_ENABLE_SSL=1)
   else()
+    unset(ENABLE_OPENSSL_ENGINES CACHE)
     unset(USE_SYSTEM_OPENSSL CACHE)
     add_definitions(-DORTHANC_ENABLE_SSL=0)
   endif()
@@ -423,13 +435,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 +509,9 @@
   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/JobsEngine.cpp
+    ${ORTHANC_ROOT}/Core/JobsEngine/JobsRegistry.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/CMake/OrthancFrameworkParameters.cmake	Wed Jun 20 18:02:36 2018 +0200
+++ b/Resources/CMake/OrthancFrameworkParameters.cmake	Wed Jun 20 18:03:20 2018 +0200
@@ -98,6 +98,7 @@
 set(ENABLE_WEB_SERVER OFF CACHE INTERNAL "Enable embedded Web server")
 set(ENABLE_DCMTK OFF CACHE INTERNAL "Enable DCMTK")
 set(ENABLE_DCMTK_NETWORKING OFF CACHE INTERNAL "Enable DICOM networking in DCMTK")
+set(ENABLE_OPENSSL_ENGINES OFF CACHE INTERNAL "Enable support of engines in OpenSSL")
 
 set(HAS_EMBEDDED_RESOURCES OFF CACHE INTERNAL
   "Whether resources are auto-generated using EmbedResources.py")
--- a/Resources/Configuration.json	Wed Jun 20 18:02:36 2018 +0200
+++ b/Resources/Configuration.json	Wed Jun 20 18:03:20 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,16 @@
     // "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" ]
-  }
+  },
+
+  // Whether to run DICOM C-Move operations synchronously. If set to
+  // "false" (the default), each incoming C-Move request results in
+  // creating a new background job. Until Orthanc 1.3.2, the default
+  // behavior was to use synchronous C-Move.
+  "SynchronousCMove" : false,
+
+  // 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 Jun 20 18:02:36 2018 +0200
+++ b/Resources/ErrorCodes.json	Wed Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:03:20 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 Jun 20 18:02:36 2018 +0200
+++ b/UnitTestsSources/MultiThreadingTests.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -34,18 +34,166 @@
 #include "PrecompiledHeadersUnitTests.h"
 #include "gtest/gtest.h"
 
-#include "../OrthancServer/Scheduler/ServerScheduler.h"
+#include "../Core/FileStorage/MemoryStorageArea.h"
+#include "../Core/JobsEngine/JobsEngine.h"
+#include "../Core/Logging.h"
+#include "../Core/MultiThreading/SharedMessageQueue.h"
 #include "../Core/OrthancException.h"
+#include "../Core/SerializationToolbox.h"
 #include "../Core/SystemToolbox.h"
 #include "../Core/Toolbox.h"
-#include "../Core/MultiThreading/Locker.h"
-#include "../Core/MultiThreading/Mutex.h"
-#include "../Core/MultiThreading/ReaderWriterLock.h"
+#include "../OrthancServer/DatabaseWrapper.h"
+#include "../OrthancServer/ServerContext.h"
+#include "../OrthancServer/ServerJobs/LuaJobManager.h"
+#include "../OrthancServer/ServerJobs/OrthancJobUnserializer.h"
+
+#include "../Core/JobsEngine/Operations/JobOperationValues.h"
+#include "../Core/JobsEngine/Operations/NullOperationValue.h"
+#include "../Core/JobsEngine/Operations/StringOperationValue.h"
+#include "../OrthancServer/ServerJobs/Operations/DicomInstanceOperationValue.h"
+
+#include "../Core/JobsEngine/Operations/LogJobOperation.h"
+#include "../OrthancServer/ServerJobs/Operations/DeleteResourceOperation.h"
+#include "../OrthancServer/ServerJobs/Operations/ModifyInstanceOperation.h"
+#include "../OrthancServer/ServerJobs/Operations/StorePeerOperation.h"
+#include "../OrthancServer/ServerJobs/Operations/StoreScuOperation.h"
+#include "../OrthancServer/ServerJobs/Operations/SystemCallOperation.h"
+
+#include "../OrthancServer/ServerJobs/ArchiveJob.h"
+#include "../OrthancServer/ServerJobs/DicomModalityStoreJob.h"
+#include "../OrthancServer/ServerJobs/OrthancPeerStoreJob.h"
+#include "../OrthancServer/ServerJobs/ResourceModificationJob.h"
+
 
 using namespace Orthanc;
 
 namespace
 {
+  class DummyJob : public IJob
+  {
+  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 bool Serialize(Json::Value& value)
+    {
+      value = Json::objectValue;
+      value["Type"] = "DummyJob";
+      return true;
+    }
+
+    virtual void GetPublicContent(Json::Value& value)
+    {
+      value["hello"] = "world";
+    }
+  };
+
+
+  class DummyInstancesJob : public SetOfInstancesJob
+  {
+  protected:
+    virtual bool HandleInstance(const std::string& instance)
+    {
+      return (instance != "nope");
+    }
+
+  public:
+    DummyInstancesJob()
+    {
+    }
+    
+    DummyInstancesJob(const Json::Value& value) :
+      SetOfInstancesJob(value)
+    {
+    }
+    
+    virtual void ReleaseResources()
+    {
+    }
+
+    virtual void GetJobType(std::string& s)
+    {
+      s = "DummyInstancesJob";
+    }
+  };
+
+
+  class DummyUnserializer : public GenericJobUnserializer
+  {
+  public:
+    virtual IJob* UnserializeJob(const Json::Value& value)
+    {
+      if (SerializationToolbox::ReadString(value, "Type") == "DummyInstancesJob")
+      {
+        return new DummyInstancesJob(value);
+      }
+      else if (SerializationToolbox::ReadString(value, "Type") == "DummyJob")
+      {
+        return new DummyJob;
+      }
+      else
+      {
+        return GenericJobUnserializer::UnserializeJob(value);
+      }
+    }
+  };
+
+    
   class DynamicInteger : public IDynamicObject
   {
   private:
@@ -106,156 +254,1240 @@
 }
 
 
-TEST(MultiThreading, Mutex)
+
+
+static bool CheckState(JobsRegistry& registry,
+                       const std::string& id,
+                       JobState state)
 {
-  Mutex mutex;
-  Locker locker(mutex);
+  JobState s;
+  if (registry.GetState(s, id))
+  {
+    return state == s;
+  }
+  else
+  {
+    return false;
+  }
 }
 
 
-TEST(MultiThreading, ReaderWriterLock)
+static bool CheckErrorCode(JobsRegistry& registry,
+                           const std::string& id,
+                           ErrorCode code)
 {
-  ReaderWriterLock lock;
-
+  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, 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, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, i2, JobState_Failure));
+  ASSERT_TRUE(CheckState(registry, i3, 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, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, i3, 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());
+  }
+
+  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, JobState_Pending));
+  ASSERT_TRUE(CheckState(registry, i2, 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, JobState_Running));
+    ASSERT_TRUE(CheckState(registry, i2, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, i1, JobState_Failure));
+  ASSERT_TRUE(CheckState(registry, i2, 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, JobState_Pending));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    job.MarkFailure();
+
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+
+    registry.Resubmit(id);
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    ASSERT_EQ(id, job.GetId());
+
+    job.MarkSuccess();
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
+}
+
+
+TEST(JobsRegistry, Retry)
+{
+  JobsRegistry registry;
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+    job.MarkRetry(0);
+
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Retry));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Retry));
+  
+  registry.ScheduleRetries();
+  ASSERT_TRUE(CheckState(registry, id, 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, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
+}
+
+
+TEST(JobsRegistry, PausePending)
+{
+  JobsRegistry registry;
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  registry.Pause(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+
+  registry.Pause(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+
+  registry.Resume(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+}
+
+
+TEST(JobsRegistry, PauseRunning)
+{
+  JobsRegistry registry;
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+
+    registry.Resubmit(id);
+    job.MarkPause();
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+
+  registry.Resubmit(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+
+  registry.Resume(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+
+    job.MarkSuccess();
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
+}
+
+
+TEST(JobsRegistry, PauseRetry)
+{
+  JobsRegistry registry;
+
+  std::string id;
+  registry.Submit(id, new DummyJob(), 10);
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+
+  {
+    JobsRegistry::RunningJob job(registry, 0);
+    ASSERT_TRUE(job.IsValid());
+
+    job.MarkRetry(0);
+    ASSERT_TRUE(CheckState(registry, id, JobState_Running));
   }
 
-  printf("**\n"); fflush(stdout);
-  SystemToolbox::USleep(1000000);
-  printf("**\n"); fflush(stdout);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Retry));
+
+  registry.Pause(id);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+
+  registry.Resume(id);
+  ASSERT_TRUE(CheckState(registry, id, 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, JobState_Running));
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, 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, JobState_Pending));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+            
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+  
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+  
+  ASSERT_TRUE(registry.Resubmit(id));
+  ASSERT_TRUE(CheckState(registry, id, 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, JobState_Running));
   }
 
-  SystemToolbox::ServerBarrier();
-  printf("DONE\n"); fflush(stdout);
+  ASSERT_TRUE(CheckState(registry, id, JobState_Success));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, 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, JobState_Running));
+
+    job.MarkCanceled();
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+
+  ASSERT_TRUE(registry.Resubmit(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Pending));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+
+  ASSERT_TRUE(registry.Pause(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Paused));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
+
+  ASSERT_TRUE(registry.Resubmit(id));
+  ASSERT_TRUE(CheckState(registry, id, 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, JobState_Running));
+
+    job.MarkRetry(500);
+  }
+
+  ASSERT_TRUE(CheckState(registry, id, JobState_Retry));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_Success));
+
+  ASSERT_TRUE(registry.Cancel(id));
+  ASSERT_TRUE(CheckState(registry, id, JobState_Failure));
+  ASSERT_TRUE(CheckErrorCode(registry, id, ErrorCode_CanceledJob));
 }
 
 
 
-class Tutu : public IServerCommand
+TEST(JobsEngine, SubmitAndWait)
+{
+  JobsEngine engine;
+  engine.SetThreadSleep(10);
+  engine.SetWorkersCount(3);
+  engine.Start();
+
+  ASSERT_TRUE(engine.GetRegistry().SubmitAndWait(new DummyJob(), rand() % 10));
+  ASSERT_FALSE(engine.GetRegistry().SubmitAndWait(new DummyJob(true), rand() % 10));
+
+  engine.Stop();
+}
+
+
+TEST(JobsEngine, DISABLED_SequenceOfOperationsJob)
 {
-private:
-  int factor_;
+  JobsEngine engine;
+  engine.SetThreadSleep(10);
+  engine.SetWorkersCount(3);
+  engine.Start();
+
+  std::string id;
+  SequenceOfOperationsJob* job = NULL;
+
+  {
+    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));
+
+  {
+    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);
+  }
+
+  boost::this_thread::sleep(boost::posix_time::milliseconds(2000));
 
-public:
-  Tutu(int f) : factor_(f)
+  engine.Stop();
+
+}
+
+
+TEST(JobsEngine, DISABLED_Lua)
+{
+  JobsEngine engine;
+  engine.SetThreadSleep(10);
+  engine.SetWorkersCount(2);
+  engine.Start();
+
+  LuaJobManager lua;
+  lua.SetMaxOperationsPerJob(5);
+  lua.SetTrailingOperationTimeout(200);
+
+  for (size_t i = 0; i < 30; i++)
   {
+    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);
   }
 
-  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;
+  boost::this_thread::sleep(boost::posix_time::milliseconds(2000));
+
+  engine.Stop();
+}
+
 
-      printf("%d * %d = %d\n", a, factor_, b);
-
-      //if (a == 84) { printf("BREAK\n"); return false; }
+static bool CheckSameJson(const Json::Value& a,
+                          const Json::Value& b)
+{
+  std::string s = a.toStyledString();
+  std::string t = b.toStyledString();
 
-      outputs.push_back(boost::lexical_cast<std::string>(b));
-    }
-
-    SystemToolbox::USleep(30000);
-
+  if (s == t)
+  {
     return true;
   }
-};
+  else
+  {
+    LOG(ERROR) << "Expected serialization: " << s;
+    LOG(ERROR) << "Actual serialization: " << t;
+    return false;
+  }
+}
+
+
+static bool CheckIdempotentSerialization(IJobUnserializer& unserializer,
+                                         IJob& job)
+{
+  Json::Value a = 42;
+  
+  if (!job.Serialize(a))
+  {
+    return false;
+  }
+  else
+  {
+    std::auto_ptr<IJob> unserialized(unserializer.UnserializeJob(a));
+  
+    Json::Value b = 43;
+    if (unserialized->Serialize(b))
+    {
+      return CheckSameJson(a, b);
+    }
+    else
+    {
+      return false;
+    }
+  }
+}
+
+
+static bool CheckIdempotentSerialization(IJobUnserializer& unserializer,
+                                         IJobOperation& operation)
+{
+  Json::Value a = 42;
+  operation.Serialize(a);
+  
+  std::auto_ptr<IJobOperation> unserialized(unserializer.UnserializeOperation(a));
+  
+  Json::Value b = 43;
+  unserialized->Serialize(b);
+
+  return CheckSameJson(a, b);
+}
+
+
+static bool CheckIdempotentSerialization(IJobUnserializer& unserializer,
+                                         JobOperationValue& value)
+{
+  Json::Value a = 42;
+  value.Serialize(a);
+  
+  std::auto_ptr<JobOperationValue> unserialized(unserializer.UnserializeValue(a));
+  
+  Json::Value b = 43;
+  unserialized->Serialize(b);
+
+  return CheckSameJson(a, b);
+}
 
 
-static void Tata(ServerScheduler* s, ServerJob* j, bool* done)
+TEST(JobsSerialization, BadFileFormat)
 {
-  typedef IServerCommand::ListOfStrings  ListOfStrings;
+  GenericJobUnserializer unserializer;
+
+  Json::Value s;
+
+  s = Json::objectValue;
+  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+
+  s = Json::arrayValue;
+  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+
+  s = "hello";
+  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
 
-  while (!(*done))
+  s = 42;
+  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+}
+
+
+TEST(JobsSerialization, JobOperationValues)
+{
+  Json::Value s;
+
+  {
+    JobOperationValues values;
+    values.Append(new NullOperationValue);
+    values.Append(new StringOperationValue("hello"));
+    values.Append(new StringOperationValue("world"));
+
+    s = 42;
+    values.Serialize(s);
+  }
+
   {
-    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);
+    GenericJobUnserializer unserializer;
+    std::auto_ptr<JobOperationValues> values(JobOperationValues::Unserialize(unserializer, s));
+    ASSERT_EQ(3u, values->GetSize());
+    ASSERT_EQ(JobOperationValue::Type_Null, values->GetValue(0).GetType());
+    ASSERT_EQ(JobOperationValue::Type_String, values->GetValue(1).GetType());
+    ASSERT_EQ(JobOperationValue::Type_String, values->GetValue(2).GetType());
+
+    ASSERT_EQ("hello", dynamic_cast<const StringOperationValue&>(values->GetValue(1)).GetContent());
+    ASSERT_EQ("world", dynamic_cast<const StringOperationValue&>(values->GetValue(2)).GetContent());
+  }
+}
+
+
+TEST(JobsSerialization, GenericValues)
+{
+  GenericJobUnserializer unserializer;
+  Json::Value s;
+
+  {
+    NullOperationValue null;
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, null));
+    null.Serialize(s);
+  }
+
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+
+  std::auto_ptr<JobOperationValue> value;
+  value.reset(unserializer.UnserializeValue(s));
+  
+  ASSERT_EQ(JobOperationValue::Type_Null, value->GetType());
+
+  {
+    StringOperationValue str("Hello");
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, str));
+    str.Serialize(s);
+  }
+
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+  value.reset(unserializer.UnserializeValue(s));
+
+  ASSERT_EQ(JobOperationValue::Type_String, value->GetType());
+  ASSERT_EQ("Hello", dynamic_cast<StringOperationValue&>(*value).GetContent());
+}
+
+
+TEST(JobsSerialization, GenericOperations)
+{   
+  DummyUnserializer unserializer;
+  Json::Value s;
+
+  {
+    LogJobOperation operation;
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+    operation.Serialize(s);
+  }
+
+  ASSERT_THROW(unserializer.UnserializeJob(s), OrthancException);
+  ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+
+  {
+    std::auto_ptr<IJobOperation> operation;
+    operation.reset(unserializer.UnserializeOperation(s));
+
+    // Make sure that we have indeed unserialized a log operation
+    ASSERT_THROW(dynamic_cast<DeleteResourceOperation&>(*operation), std::bad_cast);
+    dynamic_cast<LogJobOperation&>(*operation);
   }
 }
 
 
-TEST(MultiThreading, ServerScheduler)
+TEST(JobsSerialization, GenericJobs)
+{   
+  Json::Value s;
+
+  // This tests SetOfInstancesJob
+  
+  {
+    DummyInstancesJob job;
+    job.SetDescription("description");
+    job.AddInstance("hello");
+    job.AddInstance("nope");
+    job.AddInstance("world");
+    job.SetPermissive(true);
+    ASSERT_THROW(job.ExecuteStep(), OrthancException);  // Not started yet
+    job.Start();
+    job.ExecuteStep();
+    job.ExecuteStep();
+
+    {
+      DummyUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSerialization(unserializer, job));
+    }
+    
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    DummyUnserializer unserializer;
+    ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+    ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+
+    std::auto_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    const DummyInstancesJob& tmp = dynamic_cast<const DummyInstancesJob&>(*job);
+    ASSERT_FALSE(tmp.IsStarted());
+    ASSERT_TRUE(tmp.IsPermissive());
+    ASSERT_EQ("description", tmp.GetDescription());
+    ASSERT_EQ(3u, tmp.GetInstancesCount());
+    ASSERT_EQ(2u, tmp.GetPosition());
+    ASSERT_EQ(1u, tmp.GetFailedInstances().size());
+    ASSERT_EQ("hello", tmp.GetInstance(0));
+    ASSERT_EQ("nope", tmp.GetInstance(1));
+    ASSERT_EQ("world", tmp.GetInstance(2));
+    ASSERT_TRUE(tmp.IsFailedInstance("nope"));
+  }
+
+  // SequenceOfOperationsJob
+
+  {
+    SequenceOfOperationsJob job;
+    job.SetDescription("hello");
+
+    {
+      SequenceOfOperationsJob::Lock lock(job);
+      size_t a = lock.AddOperation(new LogJobOperation);
+      size_t b = lock.AddOperation(new LogJobOperation);
+      lock.Connect(a, b);
+      lock.AddInput(a, StringOperationValue("hello"));
+      lock.AddInput(a, StringOperationValue("world"));
+      lock.SetDicomAssociationTimeout(200);
+      lock.SetTrailingOperationTimeout(300);
+    }
+
+    ASSERT_EQ(JobStepCode_Continue, job.ExecuteStep().GetCode());
+
+    {
+      GenericJobUnserializer unserializer;
+      ASSERT_TRUE(CheckIdempotentSerialization(unserializer, job));
+    }
+    
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    GenericJobUnserializer unserializer;
+    ASSERT_THROW(unserializer.UnserializeValue(s), OrthancException);
+    ASSERT_THROW(unserializer.UnserializeOperation(s), OrthancException);
+
+    std::auto_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    std::string tmp;
+    dynamic_cast<SequenceOfOperationsJob&>(*job).GetDescription(tmp);
+    ASSERT_EQ("hello", tmp);
+  }  
+}
+
+
+static bool IsSameTagValue(ParsedDicomFile& dicom1,
+                           ParsedDicomFile& dicom2,
+                           DicomTag tag)
 {
-  ServerScheduler scheduler(10);
+  std::string a, b;
+  return (dicom1.GetTagValue(a, tag) &&
+          dicom2.GetTagValue(b, tag) &&
+          (a == b));
+}
+                       
+
+
+TEST(JobsSerialization, DicomModification)
+{   
+  Json::Value s;
+
+  ParsedDicomFile source(true);
+  source.Insert(DICOM_TAG_STUDY_DESCRIPTION, "Test 1", false);
+  source.Insert(DICOM_TAG_SERIES_DESCRIPTION, "Test 2", false);
+  source.Insert(DICOM_TAG_PATIENT_NAME, "Test 3", false);
+
+  std::auto_ptr<ParsedDicomFile> modified(source.Clone(true));
+
+  {
+    DicomModification modification;
+    modification.SetLevel(ResourceType_Series);
+    modification.Clear(DICOM_TAG_STUDY_DESCRIPTION);
+    modification.Remove(DICOM_TAG_SERIES_DESCRIPTION);
+    modification.Replace(DICOM_TAG_PATIENT_NAME, "Test 4", true);
+
+    modification.Apply(*modified);
+
+    s = 42;
+    modification.Serialize(s);
+  }
+
+  {
+    DicomModification modification(s);
+    ASSERT_EQ(ResourceType_Series, modification.GetLevel());
+    
+    std::auto_ptr<ParsedDicomFile> second(source.Clone(true));
+    modification.Apply(*second);
 
-  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);
+    std::string s;
+    ASSERT_TRUE(second->GetTagValue(s, DICOM_TAG_STUDY_DESCRIPTION));
+    ASSERT_TRUE(s.empty());
+    ASSERT_FALSE(second->GetTagValue(s, DICOM_TAG_SERIES_DESCRIPTION));
+    ASSERT_TRUE(second->GetTagValue(s, DICOM_TAG_PATIENT_NAME));
+    ASSERT_EQ("Test 4", s);
+
+    ASSERT_TRUE(IsSameTagValue(source, *modified, DICOM_TAG_STUDY_INSTANCE_UID));
+    ASSERT_TRUE(IsSameTagValue(source, *second, DICOM_TAG_STUDY_INSTANCE_UID));
+
+    ASSERT_FALSE(IsSameTagValue(source, *second, DICOM_TAG_SERIES_INSTANCE_UID));
+    ASSERT_TRUE(IsSameTagValue(*modified, *second, DICOM_TAG_SERIES_INSTANCE_UID));
+  }
+}
+
+
+TEST(JobsSerialization, DicomInstanceOrigin)
+{   
+  Json::Value s;
+
+  {
+    DicomInstanceOrigin origin;
+
+    s = 42;
+    origin.Serialize(s);
+  }
+
+  {
+    DicomInstanceOrigin origin(s);
+    ASSERT_EQ(RequestOrigin_Unknown, origin.GetRequestOrigin());
+    ASSERT_THROW(origin.GetRemoteIp(), OrthancException);
+    ASSERT_EQ("", std::string(origin.GetRemoteAetC()));
+    ASSERT_THROW(origin.GetCalledAet(), OrthancException);
+    ASSERT_THROW(origin.GetHttpUsername(), OrthancException);
+  }
+
+  {
+    DicomInstanceOrigin origin(DicomInstanceOrigin::FromDicomProtocol("host", "aet", "called"));
+
+    s = 42;
+    origin.Serialize(s);
+  }
+
+  {
+    DicomInstanceOrigin origin(s);
+    ASSERT_EQ(RequestOrigin_DicomProtocol, origin.GetRequestOrigin());
+    ASSERT_EQ("host", origin.GetRemoteIp());
+    ASSERT_EQ("aet", std::string(origin.GetRemoteAetC()));
+    ASSERT_EQ("called", origin.GetCalledAet());
+    ASSERT_THROW(origin.GetHttpUsername(), OrthancException);
+  }
+
+  {
+    DicomInstanceOrigin origin(DicomInstanceOrigin::FromHttp("host", "username"));
+
+    s = 42;
+    origin.Serialize(s);
+  }
+
+  {
+    DicomInstanceOrigin origin(s);
+    ASSERT_EQ(RequestOrigin_RestApi, origin.GetRequestOrigin());
+    ASSERT_EQ("host", origin.GetRemoteIp());
+    ASSERT_EQ("", std::string(origin.GetRemoteAetC()));
+    ASSERT_THROW(origin.GetCalledAet(), OrthancException);
+    ASSERT_EQ("username", origin.GetHttpUsername());
+  }
 
-  f3.SetConnectedToSink(true);
-  f5.SetConnectedToSink(true);
+  {
+    DicomInstanceOrigin origin(DicomInstanceOrigin::FromLua());
+
+    s = 42;
+    origin.Serialize(s);
+  }
+
+  {
+    DicomInstanceOrigin origin(s);
+    ASSERT_EQ(RequestOrigin_Lua, origin.GetRequestOrigin());
+  }
+
+  {
+    DicomInstanceOrigin origin(DicomInstanceOrigin::FromPlugins());
+
+    s = 42;
+    origin.Serialize(s);
+  }
+
+  {
+    DicomInstanceOrigin origin(s);
+    ASSERT_EQ(RequestOrigin_Plugins, origin.GetRequestOrigin());
+  }
+}
+
 
-  job.SetDescription("tutu");
+namespace
+{
+  class OrthancJobsSerialization : public testing::Test
+  {
+  private:
+    MemoryStorageArea              storage_;
+    DatabaseWrapper                db_;   // The SQLite DB is in memory
+    std::auto_ptr<ServerContext>   context_;
+    TimeoutDicomConnectionManager  manager_;
+
+  public:
+    OrthancJobsSerialization()
+    {
+      db_.Open();
+      context_.reset(new ServerContext(db_, storage_, true /* running unit tests */,
+                                       false /* don't reload jobs */));
+    }
 
-  bool done = false;
-  boost::thread t(Tata, &scheduler, &job, &done);
+    virtual ~OrthancJobsSerialization()
+    {
+      context_->Stop();
+      context_.reset(NULL);
+      db_.Close();
+    }
+
+    ServerContext& GetContext() 
+    {
+      return *context_;
+    }
+
+    bool CreateInstance(std::string& id)
+    {
+      // Create a sample DICOM file
+      ParsedDicomFile dicom(true);
+      dicom.Replace(DICOM_TAG_PATIENT_NAME, std::string("JODOGNE"),
+                    false, DicomReplaceMode_InsertIfAbsent);
+
+      DicomInstanceToStore toStore;
+      toStore.SetParsedDicomFile(dicom);
+
+      return (context_->Store(id, toStore) == StoreStatus_Success);
+    }
+  };
+}
 
 
-  //scheduler.Submit(job);
+TEST_F(OrthancJobsSerialization, Values)
+{
+  std::string id;
+  ASSERT_TRUE(CreateInstance(id));
+
+  Json::Value s;
+  OrthancJobUnserializer unserializer(GetContext());
+    
+  {
+    DicomInstanceOperationValue instance(GetContext(), id);
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, instance));
+    instance.Serialize(s);
+  }
+
+  std::auto_ptr<JobOperationValue> value;
+  value.reset(unserializer.UnserializeValue(s));
+  ASSERT_EQ(JobOperationValue::Type_DicomInstance, value->GetType());
+  ASSERT_EQ(id, dynamic_cast<DicomInstanceOperationValue&>(*value).GetId());
+
+  {
+    std::string content;
+    dynamic_cast<DicomInstanceOperationValue&>(*value).ReadDicom(content);
 
-  IServerCommand::ListOfStrings l;
-  scheduler.SubmitAndWait(l, job);
+    ParsedDicomFile dicom(content);
+    ASSERT_TRUE(dicom.GetTagValue(content, DICOM_TAG_PATIENT_NAME));
+    ASSERT_EQ("JODOGNE", content);
+  }
+}
+
+
+TEST_F(OrthancJobsSerialization, Operations)
+{
+  std::string id;
+  ASSERT_TRUE(CreateInstance(id));
+
+  Json::Value s;
+  OrthancJobUnserializer unserializer(GetContext()); 
+
+  // DeleteResourceOperation
+  
+  {
+    DeleteResourceOperation operation(GetContext());
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+    operation.Serialize(s);
+  }
+
+  std::auto_ptr<IJobOperation> operation;
+
+  {
+    operation.reset(unserializer.UnserializeOperation(s));
+
+    ASSERT_THROW(dynamic_cast<LogJobOperation&>(*operation), std::bad_cast);
+    dynamic_cast<DeleteResourceOperation&>(*operation);
+  }
+
+  // StorePeerOperation
 
-  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()));
+  {
+    WebServiceParameters peer;
+    peer.SetUrl("http://localhost/");
+    peer.SetUsername("username");
+    peer.SetPassword("password");
+    peer.SetPkcs11Enabled(true);
+
+    StorePeerOperation operation(peer);
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+    operation.Serialize(s);
+  }
+
+  {
+    operation.reset(unserializer.UnserializeOperation(s));
+
+    const StorePeerOperation& tmp = dynamic_cast<StorePeerOperation&>(*operation);
+    ASSERT_EQ("http://localhost/", tmp.GetPeer().GetUrl());
+    ASSERT_EQ("username", tmp.GetPeer().GetUsername());
+    ASSERT_EQ("password", tmp.GetPeer().GetPassword());
+    ASSERT_TRUE(tmp.GetPeer().IsPkcs11Enabled());
+  }
+
+  // StoreScuOperation
+
+  {
+    RemoteModalityParameters modality;
+    modality.SetApplicationEntityTitle("REMOTE");
+    modality.SetHost("192.168.1.1");
+    modality.SetPort(1000);
+    modality.SetManufacturer(ModalityManufacturer_StoreScp);
+
+    StoreScuOperation operation("TEST", modality);
 
-  for (IServerCommand::ListOfStrings::iterator i = l.begin(); i != l.end(); i++)
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+    operation.Serialize(s);
+  }
+
+  {
+    operation.reset(unserializer.UnserializeOperation(s));
+
+    const StoreScuOperation& tmp = dynamic_cast<StoreScuOperation&>(*operation);
+    ASSERT_EQ("REMOTE", tmp.GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ("192.168.1.1", tmp.GetRemoteModality().GetHost());
+    ASSERT_EQ(1000, tmp.GetRemoteModality().GetPort());
+    ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetRemoteModality().GetManufacturer());
+    ASSERT_EQ("TEST", tmp.GetLocalAet());
+  }
+
+  // SystemCallOperation
+
   {
-    printf("** %s\n", i->c_str());
+    SystemCallOperation operation(std::string("echo"));
+    operation.AddPreArgument("a");
+    operation.AddPreArgument("b");
+    operation.AddPostArgument("c");
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+    operation.Serialize(s);
+  }
+
+  {
+    operation.reset(unserializer.UnserializeOperation(s));
+
+    const SystemCallOperation& tmp = dynamic_cast<SystemCallOperation&>(*operation);
+    ASSERT_EQ("echo", tmp.GetCommand());
+    ASSERT_EQ(2u, tmp.GetPreArgumentsCount());
+    ASSERT_EQ(1u, tmp.GetPostArgumentsCount());
+    ASSERT_EQ("a", tmp.GetPreArgument(0));
+    ASSERT_EQ("b", tmp.GetPreArgument(1));
+    ASSERT_EQ("c", tmp.GetPostArgument(0));
   }
 
-  //SystemToolbox::ServerBarrier();
-  //SystemToolbox::USleep(3000000);
+  // ModifyInstanceOperation
 
-  scheduler.Stop();
+  {
+    std::auto_ptr<DicomModification> modification(new DicomModification);
+    modification->SetupAnonymization(DicomVersion_2008);
+    
+    ModifyInstanceOperation operation(GetContext(), RequestOrigin_Lua, modification.release());
 
-  done = true;
-  if (t.joinable())
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, operation));
+    operation.Serialize(s);
+  }
+
   {
-    t.join();
+    operation.reset(unserializer.UnserializeOperation(s));
+
+    const ModifyInstanceOperation& tmp = dynamic_cast<ModifyInstanceOperation&>(*operation);
+    ASSERT_EQ(RequestOrigin_Lua, tmp.GetRequestOrigin());
+    ASSERT_TRUE(tmp.GetModification().IsRemoved(DICOM_TAG_STUDY_DESCRIPTION));
   }
 }
+
+
+TEST_F(OrthancJobsSerialization, Jobs)
+{
+  // ArchiveJob
+
+  Json::Value s;
+
+  {
+    boost::shared_ptr<TemporaryFile> tmp(new TemporaryFile);
+    ArchiveJob job(tmp, GetContext(), false, false);
+    ASSERT_FALSE(job.Serialize(s));  // Cannot serialize this
+  }
+
+  // DicomModalityStoreJob
+
+  OrthancJobUnserializer unserializer(GetContext()); 
+
+  {
+    RemoteModalityParameters modality;
+    modality.SetApplicationEntityTitle("REMOTE");
+    modality.SetHost("192.168.1.1");
+    modality.SetPort(1000);
+    modality.SetManufacturer(ModalityManufacturer_StoreScp);
+
+    DicomModalityStoreJob job(GetContext());
+    job.SetLocalAet("LOCAL");
+    job.SetRemoteModality(modality);
+    job.SetMoveOriginator("MOVESCU", 42);
+
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, job));
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    std::auto_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    DicomModalityStoreJob& tmp = dynamic_cast<DicomModalityStoreJob&>(*job);
+    ASSERT_EQ("LOCAL", tmp.GetLocalAet());
+    ASSERT_EQ("REMOTE", tmp.GetRemoteModality().GetApplicationEntityTitle());
+    ASSERT_EQ("192.168.1.1", tmp.GetRemoteModality().GetHost());
+    ASSERT_EQ(1000, tmp.GetRemoteModality().GetPort());
+    ASSERT_EQ(ModalityManufacturer_StoreScp, tmp.GetRemoteModality().GetManufacturer());
+    ASSERT_TRUE(tmp.HasMoveOriginator());
+    ASSERT_EQ("MOVESCU", tmp.GetMoveOriginatorAet());
+    ASSERT_EQ(42, tmp.GetMoveOriginatorId());
+  }
+
+  // OrthancPeerStoreJob
+
+  {
+    WebServiceParameters peer;
+    peer.SetUrl("http://localhost/");
+    peer.SetUsername("username");
+    peer.SetPassword("password");
+    peer.SetPkcs11Enabled(true);
+
+    OrthancPeerStoreJob job(GetContext());
+    job.SetPeer(peer);
+    
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, job));
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    std::auto_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    OrthancPeerStoreJob& tmp = dynamic_cast<OrthancPeerStoreJob&>(*job);
+    ASSERT_EQ("http://localhost/", tmp.GetPeer().GetUrl());
+    ASSERT_EQ("username", tmp.GetPeer().GetUsername());
+    ASSERT_EQ("password", tmp.GetPeer().GetPassword());
+    ASSERT_TRUE(tmp.GetPeer().IsPkcs11Enabled());
+  }
+
+  // ResourceModificationJob
+
+  {
+    std::auto_ptr<DicomModification> modification(new DicomModification);
+    modification->SetupAnonymization(DicomVersion_2008);    
+
+    ResourceModificationJob job(GetContext());
+    job.SetModification(modification.release(), true);
+    job.SetOrigin(DicomInstanceOrigin::FromLua());
+    
+    ASSERT_TRUE(CheckIdempotentSerialization(unserializer, job));
+    ASSERT_TRUE(job.Serialize(s));
+  }
+
+  {
+    std::auto_ptr<IJob> job;
+    job.reset(unserializer.UnserializeJob(s));
+
+    ResourceModificationJob& tmp = dynamic_cast<ResourceModificationJob&>(*job);
+    ASSERT_TRUE(tmp.IsAnonymization());
+    ASSERT_EQ(RequestOrigin_Lua, tmp.GetOrigin().GetRequestOrigin());
+    ASSERT_TRUE(tmp.GetModification().IsRemoved(DICOM_TAG_STUDY_DESCRIPTION));
+  }
+}
+
+
+TEST(JobsSerialization, Registry)
+{   
+  Json::Value s;
+  std::string i1, i2;
+
+  {
+    JobsRegistry registry;
+    registry.Submit(i1, new DummyJob(), 10);
+    registry.Submit(i2, new SequenceOfOperationsJob(), 30);
+    registry.Serialize(s);
+  }
+
+  {
+    DummyUnserializer unserializer;
+    JobsRegistry registry(unserializer, s);
+
+    Json::Value t;
+    registry.Serialize(t);
+    ASSERT_TRUE(CheckSameJson(s, t));
+  }
+}
--- a/UnitTestsSources/SQLiteChromiumTests.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/UnitTestsSources/SQLiteChromiumTests.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -51,35 +51,38 @@
  ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/connection_unittest.cc
  ********************************************************************/
 
-class SQLConnectionTest : public testing::Test 
+namespace
 {
-public:
-  SQLConnectionTest()
-  {
-  }
-
-  virtual ~SQLConnectionTest()
-  {
-  }
-
-  virtual void SetUp() 
+  class SQLConnectionTest : public testing::Test 
   {
-    db_.OpenInMemory();
-  }
+  public:
+    SQLConnectionTest()
+    {
+    }
 
-  virtual void TearDown() 
-  {
-    db_.Close();
-  }
+    virtual ~SQLConnectionTest()
+    {
+    }
+
+    virtual void SetUp() 
+    {
+      db_.OpenInMemory();
+    }
 
-  Connection& db() 
-  { 
-    return db_; 
-  }
+    virtual void TearDown() 
+    {
+      db_.Close();
+    }
 
-private:
-  Connection db_;
-};
+    Connection& db() 
+    { 
+      return db_; 
+    }
+
+  private:
+    Connection db_;
+  };
+}
 
 
 
@@ -266,23 +269,26 @@
  ** http://src.chromium.org/viewvc/chrome/trunk/src/sql/transaction_unittest.cc
  ********************************************************************/
 
-class SQLTransactionTest : public SQLConnectionTest
+namespace
 {
-public:
-  virtual void SetUp()
+  class SQLTransactionTest : public SQLConnectionTest
   {
-    SQLConnectionTest::SetUp();
-    ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
-  }
+  public:
+    virtual void SetUp()
+    {
+      SQLConnectionTest::SetUp();
+      ASSERT_TRUE(db().Execute("CREATE TABLE foo (a, b)"));
+    }
 
-  // Returns the number of rows in table "foo".
-  int CountFoo() 
-  {
-    Statement count(db(), "SELECT count(*) FROM foo");
-    count.Step();
-    return count.ColumnInt(0);
-  }
-};
+    // Returns the number of rows in table "foo".
+    int CountFoo() 
+    {
+      Statement count(db(), "SELECT count(*) FROM foo");
+      count.Step();
+      return count.ColumnInt(0);
+    }
+  };
+}
 
 
 TEST_F(SQLTransactionTest, Commit) {
--- a/UnitTestsSources/ServerIndexTests.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/UnitTestsSources/ServerIndexTests.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -675,7 +675,8 @@
   FilesystemStorage storage(path);
   DatabaseWrapper db;   // The SQLite DB is in memory
   db.Open();
-  ServerContext context(db, storage);
+  ServerContext context(db, storage, true /* running unit tests */,
+                        false /* don't reload jobs */);
   ServerIndex& index = context.GetIndex();
 
   ASSERT_EQ(1u, index.IncrementGlobalSequence(GlobalProperty_AnonymizationSequence));
@@ -773,7 +774,8 @@
   FilesystemStorage storage(path);
   DatabaseWrapper db;   // The SQLite DB is in memory
   db.Open();
-  ServerContext context(db, storage);
+  ServerContext context(db, storage, true /* running unit tests */,
+                        false /* don't reload jobs */);
   ServerIndex& index = context.GetIndex();
 
   index.SetMaximumStorageSize(10);
--- a/UnitTestsSources/UnitTestsMain.cpp	Wed Jun 20 18:02:36 2018 +0200
+++ b/UnitTestsSources/UnitTestsMain.cpp	Wed Jun 20 18:03:20 2018 +0200
@@ -677,6 +677,14 @@
   }
 
   ASSERT_THROW(StringToValueRepresentation("nope", true), OrthancException);
+
+  ASSERT_EQ(JobState_Pending, StringToJobState(EnumerationToString(JobState_Pending)));
+  ASSERT_EQ(JobState_Running, StringToJobState(EnumerationToString(JobState_Running)));
+  ASSERT_EQ(JobState_Success, StringToJobState(EnumerationToString(JobState_Success)));
+  ASSERT_EQ(JobState_Failure, StringToJobState(EnumerationToString(JobState_Failure)));
+  ASSERT_EQ(JobState_Paused, StringToJobState(EnumerationToString(JobState_Paused)));
+  ASSERT_EQ(JobState_Retry, StringToJobState(EnumerationToString(JobState_Retry)));
+  ASSERT_THROW(StringToJobState("nope"), OrthancException);
 }