changeset 2862:c59679710d4b

merge
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 05 Oct 2018 17:48:02 +0200
parents 9b4251721f22 (current diff) 8b00e4cb4a6b (diff)
children 7cac2bc1986e
files Core/ICommand.h
diffstat 15 files changed, 716 insertions(+), 329 deletions(-) [+]
line wrap: on
line diff
--- a/Core/ICommand.h	Fri Oct 05 17:46:02 2018 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,49 +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 "IDynamicObject.h"
-
-namespace Orthanc
-{
-  /**
-   * This class is the base class for the "Command" design pattern.
-   * http://en.wikipedia.org/wiki/Command_pattern
-   **/
-  class ICommand : public IDynamicObject
-  {
-  public:
-    virtual bool Execute() = 0;
-  };
-}
--- a/Core/IDynamicObject.h	Fri Oct 05 17:46:02 2018 +0200
+++ b/Core/IDynamicObject.h	Fri Oct 05 17:48:02 2018 +0200
@@ -50,4 +50,27 @@
     {
     }
   };
+
+  /**
+   * This class is a simple implementation of a IDynamicObject that stores a single typed value
+   */
+  template <typename T>
+  class SingleValueObject : public Orthanc::IDynamicObject
+  {
+  private:
+    T                  value_;
+  public:
+    SingleValueObject(const T& value) :
+      value_(value)
+    {
+    }
+    virtual ~SingleValueObject()
+    {
+    }
+
+    const T& GetValue() const
+    {
+        return value_;
+    }
+  };
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/SetOfCommandsJob.cpp	Fri Oct 05 17:48:02 2018 +0200
@@ -0,0 +1,302 @@
+/**
+ * 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 "SetOfCommandsJob.h"
+
+#include "../Logging.h"
+#include "../OrthancException.h"
+#include "../SerializationToolbox.h"
+
+#include <cassert>
+
+namespace Orthanc
+{
+  SetOfCommandsJob::SetOfCommandsJob() :
+    started_(false),
+    permissive_(false),
+    position_(0)
+  {
+  }
+
+
+  SetOfCommandsJob::~SetOfCommandsJob()
+  {
+    for (size_t i = 0; i < commands_.size(); i++)
+    {
+      assert(commands_[i] != NULL);
+      delete commands_[i];
+    }
+  }
+
+    
+  void SetOfCommandsJob::Reserve(size_t size)
+  {
+    if (started_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      commands_.reserve(size);
+    }
+  }
+
+    
+  void SetOfCommandsJob::AddCommand(ICommand* command)
+  {
+    if (command == NULL)
+    {
+      throw OrthancException(ErrorCode_NullPointer);
+    }
+    else if (started_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      commands_.push_back(command);
+    }
+  }
+
+
+  void SetOfCommandsJob::SetPermissive(bool permissive)
+  {
+    if (started_)
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+    else
+    {
+      permissive_ = permissive;
+    }
+  }
+
+
+  void SetOfCommandsJob::Reset()
+  {
+    if (started_)
+    {
+      position_ = 0;
+    }
+    else
+    {
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+  }
+
+    
+  float SetOfCommandsJob::GetProgress()
+  {
+    if (commands_.empty())
+    {
+      return 1;
+    }
+    else
+    {
+      return (static_cast<float>(position_) /
+              static_cast<float>(commands_.size()));
+    }
+  }
+
+
+  const SetOfCommandsJob::ICommand& SetOfCommandsJob::GetCommand(size_t index) const
+  {
+    if (index >= commands_.size())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+    else
+    {
+      assert(commands_[index] != NULL);
+      return *commands_[index];
+    }
+  }
+      
+
+  JobStepResult SetOfCommandsJob::Step()
+  {
+    if (!started_)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    if (commands_.empty() &&
+        position_ == 0)
+    {
+      // No command to handle: We're done
+      position_ = 1;
+      return JobStepResult::Success();
+    }
+    
+    if (position_ >= commands_.size())
+    {
+      // Already done
+      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+    }
+
+    try
+    {
+      // Not at the trailing step: Handle the current command
+      if (!commands_[position_]->Execute())
+      {
+        // Error
+        if (!permissive_)
+        {
+          return JobStepResult::Failure(ErrorCode_InternalError);
+        }
+      }
+    }
+    catch (OrthancException& e)
+    {
+      if (permissive_)
+      {
+        LOG(WARNING) << "Ignoring an error in a permissive job: " << e.What();
+      }
+      else
+      {
+        return JobStepResult::Failure(e.GetErrorCode());
+      }
+    }
+
+    position_ += 1;
+
+    if (position_ == commands_.size())
+    {
+      // We're done
+      return JobStepResult::Success();
+    }
+    else
+    {
+      return JobStepResult::Continue();
+    }
+  }
+
+
+
+  static const char* KEY_DESCRIPTION = "Description";
+  static const char* KEY_PERMISSIVE = "Permissive";
+  static const char* KEY_POSITION = "Position";
+  static const char* KEY_TYPE = "Type";
+  static const char* KEY_COMMANDS = "Commands";
+
+  
+  void SetOfCommandsJob::GetPublicContent(Json::Value& value)
+  {
+    value[KEY_DESCRIPTION] = GetDescription();
+  }    
+
+
+  bool SetOfCommandsJob::Serialize(Json::Value& target)
+  {
+    target = Json::objectValue;
+
+    std::string type;
+    GetJobType(type);
+    target[KEY_TYPE] = type;
+    
+    target[KEY_PERMISSIVE] = permissive_;
+    target[KEY_POSITION] = static_cast<unsigned int>(position_);
+    target[KEY_DESCRIPTION] = description_;
+
+    target[KEY_COMMANDS] = Json::arrayValue;
+    Json::Value& tmp = target[KEY_COMMANDS];
+
+    for (size_t i = 0; i < commands_.size(); i++)
+    {
+      assert(commands_[i] != NULL);
+      
+      Json::Value command;
+      commands_[i]->Serialize(command);
+      tmp.append(command);
+    }
+
+    return true;
+  }
+
+
+  SetOfCommandsJob::SetOfCommandsJob(ICommandUnserializer* unserializer,
+                                     const Json::Value& source) :
+    started_(false)
+  {
+    std::auto_ptr<ICommandUnserializer> raii(unserializer);
+
+    permissive_ = SerializationToolbox::ReadBoolean(source, KEY_PERMISSIVE);
+    position_ = SerializationToolbox::ReadUnsignedInteger(source, KEY_POSITION);
+    description_ = SerializationToolbox::ReadString(source, KEY_DESCRIPTION);
+    
+    if (!source.isMember(KEY_COMMANDS) ||
+        source[KEY_COMMANDS].type() != Json::arrayValue)
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+    else
+    {
+      const Json::Value& tmp = source[KEY_COMMANDS];
+      commands_.resize(tmp.size());
+
+      for (Json::Value::ArrayIndex i = 0; i < tmp.size(); i++)
+      {
+        try
+        {
+          commands_[i] = unserializer->Unserialize(tmp[i]);
+        }
+        catch (OrthancException&)
+        {
+        }
+
+        if (commands_[i] == NULL)
+        {
+          for (size_t j = 0; j < i; j++)
+          {
+            delete commands_[j];
+          }
+
+          throw OrthancException(ErrorCode_BadFileFormat);
+        }
+      }
+    }
+
+    if (commands_.empty())
+    {
+      if (position_ > 1)
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+    else if (position_ > commands_.size())
+    {
+      throw OrthancException(ErrorCode_BadFileFormat);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/JobsEngine/SetOfCommandsJob.h	Fri Oct 05 17:48:02 2018 +0200
@@ -0,0 +1,135 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2018 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "IJob.h"
+
+#include <set>
+
+namespace Orthanc
+{
+  class SetOfCommandsJob : public IJob
+  {
+  public:
+    class ICommand : public boost::noncopyable
+    {
+    public:
+      virtual ~ICommand()
+      {
+      }
+
+      virtual bool Execute() = 0;
+
+      virtual void Serialize(Json::Value& target) const = 0;
+    };
+
+    class ICommandUnserializer : public boost::noncopyable
+    {
+    public:
+      virtual ~ICommandUnserializer()
+      {
+      }
+      
+      virtual ICommand* Unserialize(const Json::Value& source) const = 0;
+    };
+    
+  private:
+    bool                    started_;
+    std::vector<ICommand*>  commands_;
+    bool                    permissive_;
+    size_t                  position_;
+    std::string             description_;
+
+  public:
+    SetOfCommandsJob();
+
+    SetOfCommandsJob(ICommandUnserializer* unserializer  /* takes ownership */,
+                     const Json::Value& source);
+
+    virtual ~SetOfCommandsJob();
+
+    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 GetCommandsCount() const
+    {
+      return commands_.size();
+    }
+
+    void AddCommand(ICommand* command);  // Takes ownership
+
+    bool IsPermissive() const
+    {
+      return permissive_;
+    }
+
+    void SetPermissive(bool permissive);
+
+    virtual void Reset();
+    
+    virtual void Start()
+    {
+      started_ = true;
+    }
+    
+    virtual float GetProgress();
+
+    bool IsStarted() const
+    {
+      return started_;
+    }
+
+    const ICommand& GetCommand(size_t index) const;
+      
+    virtual JobStepResult Step();
+    
+    virtual void GetPublicContent(Json::Value& value);
+    
+    virtual bool Serialize(Json::Value& target);
+  };
+}
--- a/Core/JobsEngine/SetOfInstancesJob.cpp	Fri Oct 05 17:46:02 2018 +0200
+++ b/Core/JobsEngine/SetOfInstancesJob.cpp	Fri Oct 05 17:48:02 2018 +0200
@@ -37,249 +37,204 @@
 #include "../OrthancException.h"
 #include "../SerializationToolbox.h"
 
+#include <cassert>
+
 namespace Orthanc
 {
-  SetOfInstancesJob::SetOfInstancesJob(bool hasTrailingStep) :
-    hasTrailingStep_(hasTrailingStep),
-    started_(false),
-    permissive_(false),
-    position_(0)
+  class SetOfInstancesJob::InstanceCommand : public SetOfInstancesJob::ICommand
+  {
+  private:
+    SetOfInstancesJob& that_;
+    std::string        instance_;
+
+  public:
+    InstanceCommand(SetOfInstancesJob& that,
+                    const std::string& instance) :
+      that_(that),
+      instance_(instance)
+    {
+    }
+
+    const std::string& GetInstance() const
+    {
+      return instance_;
+    }
+      
+    virtual bool Execute()
+    {
+      if (!that_.HandleInstance(instance_))
+      {
+        that_.failedInstances_.insert(instance_);
+        return false;
+      }
+      else
+      {
+        return true;
+      }
+    }
+
+    virtual void Serialize(Json::Value& target) const
+    {
+      target = instance_;
+    }
+  };
+
+
+  class SetOfInstancesJob::TrailingStepCommand : public SetOfInstancesJob::ICommand
+  {
+  private:
+    SetOfInstancesJob& that_;
+
+  public:
+    TrailingStepCommand(SetOfInstancesJob& that) :
+      that_(that)
+    {
+    }       
+      
+    virtual bool Execute()
+    {
+      return that_.HandleTrailingStep();
+    }
+
+    virtual void Serialize(Json::Value& target) const
+    {
+      target = Json::nullValue;
+    }
+  };
+
+
+  class SetOfInstancesJob::InstanceUnserializer :
+    public SetOfInstancesJob::ICommandUnserializer
+  {
+  private:
+    SetOfInstancesJob& that_;
+
+  public:
+    InstanceUnserializer(SetOfInstancesJob& that) :
+      that_(that)
+    {
+    }
+
+    virtual ICommand* Unserialize(const Json::Value& source) const
+    {
+      if (source.type() == Json::nullValue)
+      {
+        return new TrailingStepCommand(that_);
+      }
+      else if (source.type() == Json::stringValue)
+      {
+        return new InstanceCommand(that_, source.asString());
+      }
+      else
+      {
+        throw OrthancException(ErrorCode_BadFileFormat);
+      }
+    }
+  };
+    
+
+  SetOfInstancesJob::SetOfInstancesJob() :
+    hasTrailingStep_(false)
   {
   }
 
     
-  void SetOfInstancesJob::Reserve(size_t size)
-  {
-    if (started_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      instances_.reserve(size);
-    }
-  }
-
-    
-  size_t SetOfInstancesJob::GetStepsCount() const
-  {
-    if (HasTrailingStep())
-    {
-      return instances_.size() + 1;
-    }
-    else
-    {
-      return instances_.size();
-    }
-  }
-  
-
   void SetOfInstancesJob::AddInstance(const std::string& instance)
   {
-    if (started_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      instances_.push_back(instance);
-    }
+    AddCommand(new InstanceCommand(*this, instance));
   }
 
 
-  void SetOfInstancesJob::SetPermissive(bool permissive)
+  void SetOfInstancesJob::AddTrailingStep()
   {
-    if (started_)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
+    AddCommand(new TrailingStepCommand(*this));
+    hasTrailingStep_ = true;
+  }
+  
+  
+  size_t SetOfInstancesJob::GetInstancesCount() const
+  {
+    if (hasTrailingStep_)
     {
-      permissive_ = permissive;
-    }
-  }
-
-
-  void SetOfInstancesJob::Reset()
-  {
-    if (started_)
-    {
-      position_ = 0;
-      failedInstances_.clear();
+      assert(GetCommandsCount() > 0);
+      return GetCommandsCount() - 1;
     }
     else
     {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
+      return GetCommandsCount();
     }
   }
 
-    
-  float SetOfInstancesJob::GetProgress()
-  {
-    const size_t steps = GetStepsCount();
-    
-    if (steps == 0)
-    {
-      return 0;
-    }
-    else
-    {
-      return (static_cast<float>(position_) /
-              static_cast<float>(steps));
-    }
-  }
-
-
+  
   const std::string& SetOfInstancesJob::GetInstance(size_t index) const
   {
-    if (index >= instances_.size())
+    if (index >= GetInstancesCount())
     {
       throw OrthancException(ErrorCode_ParameterOutOfRange);
     }
     else
     {
-      return instances_[index];
-    }
-  }
-      
-
-  JobStepResult SetOfInstancesJob::Step()
-  {
-    if (!started_)
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-    
-    const size_t steps = GetStepsCount();
-
-    if (steps == 0 &&
-        position_ == 0)
-    {
-      // Nothing to handle (no instance, nor trailing step): We're done
-      position_ = 1;
-      return JobStepResult::Success();
-    }
-    
-    if (position_ >= steps)
-    {
-      // Already done
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-
-    bool isTrailingStep = (hasTrailingStep_ &&
-                           position_ + 1 == steps);
-    
-    bool ok;
-      
-    try
-    {
-      if (isTrailingStep)
-      {
-        ok = HandleTrailingStep();
-      }
-      else
-      {
-        // Not at the trailing step: Handle the current instance
-        ok = HandleInstance(instances_[position_]);
-      }
-
-      if (!ok && !permissive_)
-      {
-        return JobStepResult::Failure(ErrorCode_InternalError);
-      }
-    }
-    catch (OrthancException&)
-    {
-      if (permissive_)
-      {
-        ok = false;
-      }
-      else
-      {
-        throw;
-      }
-    }
-
-    if (!ok &&
-        !isTrailingStep)
-    {
-      failedInstances_.insert(instances_[position_]);
-    }
-
-    position_ += 1;
-
-    if (position_ == steps)
-    {
-      // We're done
-      return JobStepResult::Success();
-    }
-    else
-    {
-      return JobStepResult::Continue();
+      return dynamic_cast<const InstanceCommand&>(GetCommand(index)).GetInstance();
     }
   }
 
 
+  void SetOfInstancesJob::Start()
+  {
+    SetOfCommandsJob::Start();    
+  }
 
-  static const char* KEY_DESCRIPTION = "Description";
-  static const char* KEY_PERMISSIVE = "Permissive";
-  static const char* KEY_POSITION = "Position";
-  static const char* KEY_TYPE = "Type";
-  static const char* KEY_INSTANCES = "Instances";
-  static const char* KEY_FAILED_INSTANCES = "FailedInstances";
-  static const char* KEY_TRAILING_STEP = "TrailingStep";
+
+  void SetOfInstancesJob::Reset()
+  {
+    SetOfCommandsJob::Reset();
 
-  
-  void SetOfInstancesJob::GetPublicContent(Json::Value& value)
+    failedInstances_.clear();
+  }
+
+
+  void SetOfInstancesJob::GetPublicContent(Json::Value& target)
   {
-    value[KEY_DESCRIPTION] = GetDescription();
-    value["InstancesCount"] = static_cast<uint32_t>(instances_.size());
-    value["FailedInstancesCount"] = static_cast<uint32_t>(failedInstances_.size());
+    SetOfCommandsJob::GetPublicContent(target);
+    target["InstancesCount"] = static_cast<uint32_t>(GetInstancesCount());
+    target["FailedInstancesCount"] = static_cast<uint32_t>(failedInstances_.size());
   }    
 
 
-  bool SetOfInstancesJob::Serialize(Json::Value& value)
-  {
-    value = Json::objectValue;
 
-    std::string type;
-    GetJobType(type);
-    value[KEY_TYPE] = type;
-    
-    value[KEY_PERMISSIVE] = permissive_;
-    value[KEY_POSITION] = static_cast<unsigned int>(position_);
-    value[KEY_DESCRIPTION] = description_;
-    value[KEY_TRAILING_STEP] = hasTrailingStep_;
-
-    SerializationToolbox::WriteArrayOfStrings(value, instances_, KEY_INSTANCES);
-    SerializationToolbox::WriteSetOfStrings(value, failedInstances_, KEY_FAILED_INSTANCES);
+  static const char* KEY_TRAILING_STEP = "TrailingStep";
+  static const char* KEY_FAILED_INSTANCES = "FailedInstances";
 
-    return true;
+  bool SetOfInstancesJob::Serialize(Json::Value& target) 
+  {
+    if (SetOfCommandsJob::Serialize(target))
+    {
+      target[KEY_TRAILING_STEP] = hasTrailingStep_;
+      SerializationToolbox::WriteSetOfStrings(target, failedInstances_, KEY_FAILED_INSTANCES);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
   }
-
+  
 
-  SetOfInstancesJob::SetOfInstancesJob(const Json::Value& value) :
-    started_(false),
-    permissive_(SerializationToolbox::ReadBoolean(value, KEY_PERMISSIVE)),
-    position_(SerializationToolbox::ReadUnsignedInteger(value, KEY_POSITION)),
-    description_(SerializationToolbox::ReadString(value, KEY_DESCRIPTION))
+  SetOfInstancesJob::SetOfInstancesJob(const Json::Value& source) :
+    SetOfCommandsJob(new InstanceUnserializer(*this), source)
   {
-    SerializationToolbox::ReadArrayOfStrings(instances_, value, KEY_INSTANCES);
-    SerializationToolbox::ReadSetOfStrings(failedInstances_, value, KEY_FAILED_INSTANCES);
+    SerializationToolbox::ReadSetOfStrings(failedInstances_, source, KEY_FAILED_INSTANCES);
 
-    if (value.isMember(KEY_TRAILING_STEP))
+    if (source.isMember(KEY_TRAILING_STEP))
     {
-      hasTrailingStep_ = SerializationToolbox::ReadBoolean(value, KEY_TRAILING_STEP);
+      hasTrailingStep_ = SerializationToolbox::ReadBoolean(source, KEY_TRAILING_STEP);
     }
     else
     {
       // Backward compatibility with Orthanc <= 1.4.2
       hasTrailingStep_ = false;
     }
+  }
+  
 
-    if (position_ > GetStepsCount() + 1)
-    {
-      throw OrthancException(ErrorCode_BadFileFormat);
-    }
-  }
 }
--- a/Core/JobsEngine/SetOfInstancesJob.h	Fri Oct 05 17:46:02 2018 +0200
+++ b/Core/JobsEngine/SetOfInstancesJob.h	Fri Oct 05 17:48:02 2018 +0200
@@ -34,86 +34,48 @@
 #pragma once
 
 #include "IJob.h"
+#include "SetOfCommandsJob.h"
 
 #include <set>
 
 namespace Orthanc
 {
-  class SetOfInstancesJob : public IJob
+  class SetOfInstancesJob : public SetOfCommandsJob
   {
   private:
-    bool                      hasTrailingStep_;
-    bool                      started_;
-    std::vector<std::string>  instances_;
-    bool                      permissive_;
-    size_t                    position_;
-    std::set<std::string>     failedInstances_;
-    std::string               description_;
+    class InstanceCommand;
+    class TrailingStepCommand;
+    class InstanceUnserializer;
+    
+    bool                   hasTrailingStep_;
+    std::set<std::string>  failedInstances_;
 
   protected:
     virtual bool HandleInstance(const std::string& instance) = 0;
 
     virtual bool HandleTrailingStep() = 0;
 
+    // Hiding this method, use AddInstance() instead
+    using SetOfCommandsJob::AddCommand;
+
   public:
-    SetOfInstancesJob(bool hasTrailingStep);
+    SetOfInstancesJob();
+
+    SetOfInstancesJob(const Json::Value& source);  // Unserialization
+
+    void AddInstance(const std::string& instance);
 
-    SetOfInstancesJob(const Json::Value& s);  // Unserialization
+    void AddTrailingStep(); 
+
+    size_t GetInstancesCount() const;
+    
+    const std::string& GetInstance(size_t index) const;
 
     bool HasTrailingStep() const
     {
       return hasTrailingStep_;
     }
-    
-    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();
-    }
-    
-    size_t GetStepsCount() const;
-
-    void AddInstance(const std::string& instance);
-
-    bool IsPermissive() const
-    {
-      return permissive_;
-    }
-
-    void SetPermissive(bool permissive);
-
-    virtual void Reset();
-    
-    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_;
@@ -123,11 +85,13 @@
     {
       return failedInstances_.find(instance) != failedInstances_.end();
     }
-    
-    virtual JobStepResult Step();
-    
-    virtual void GetPublicContent(Json::Value& value);
-    
-    virtual bool Serialize(Json::Value& value);
+
+    virtual void Start();
+
+    virtual void Reset();
+
+    virtual void GetPublicContent(Json::Value& target);
+
+    virtual bool Serialize(Json::Value& target);
   };
 }
--- a/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Fri Oct 05 17:46:02 2018 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestAnonymizeModify.cpp	Fri Oct 05 17:48:02 2018 +0200
@@ -681,6 +681,8 @@
       job->AddSourceSeries(series[i]);
     }
 
+    job->AddTrailingStep();
+
     static const char* KEEP_SOURCE = "KeepSource";
     if (request.isMember(KEEP_SOURCE))
     {
@@ -768,6 +770,8 @@
       job->AddSource(resources[i]);
     }
 
+    job->AddTrailingStep();
+
     static const char* KEEP_SOURCE = "KeepSource";
     if (request.isMember(KEEP_SOURCE))
     {
--- a/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp	Fri Oct 05 17:46:02 2018 +0200
+++ b/OrthancServer/ServerJobs/DicomModalityStoreJob.cpp	Fri Oct 05 17:48:02 2018 +0200
@@ -92,7 +92,6 @@
 
 
   DicomModalityStoreJob::DicomModalityStoreJob(ServerContext& context) :
-    SetOfInstancesJob(false /* no trailing step */),
     context_(context),
     localAet_("ORTHANC"),
     moveOriginatorId_(0)  // By default, not a C-MOVE
--- a/OrthancServer/ServerJobs/MergeStudyJob.cpp	Fri Oct 05 17:46:02 2018 +0200
+++ b/OrthancServer/ServerJobs/MergeStudyJob.cpp	Fri Oct 05 17:48:02 2018 +0200
@@ -170,7 +170,6 @@
   
   MergeStudyJob::MergeStudyJob(ServerContext& context,
                                const std::string& targetStudy) :
-    SetOfInstancesJob(true /* with trailing step */),
     context_(context),
     keepSource_(false),
     targetStudy_(targetStudy)
--- a/OrthancServer/ServerJobs/OrthancPeerStoreJob.h	Fri Oct 05 17:46:02 2018 +0200
+++ b/OrthancServer/ServerJobs/OrthancPeerStoreJob.h	Fri Oct 05 17:48:02 2018 +0200
@@ -55,7 +55,6 @@
 
   public:
     OrthancPeerStoreJob(ServerContext& context) :
-      SetOfInstancesJob(false /* no trailing step */),
       context_(context)
     {
     }
--- a/OrthancServer/ServerJobs/ResourceModificationJob.h	Fri Oct 05 17:46:02 2018 +0200
+++ b/OrthancServer/ServerJobs/ResourceModificationJob.h	Fri Oct 05 17:48:02 2018 +0200
@@ -79,7 +79,6 @@
 
   public:
     ResourceModificationJob(ServerContext& context) :
-      SetOfInstancesJob(false /* no trailing step */),
       context_(context),
       isAnonymization_(false)
     {
--- a/OrthancServer/ServerJobs/SplitStudyJob.cpp	Fri Oct 05 17:46:02 2018 +0200
+++ b/OrthancServer/ServerJobs/SplitStudyJob.cpp	Fri Oct 05 17:48:02 2018 +0200
@@ -161,7 +161,6 @@
   
   SplitStudyJob::SplitStudyJob(ServerContext& context,
                                const std::string& sourceStudy) :
-    SetOfInstancesJob(true /* with trailing step */),
     context_(context),
     keepSource_(false),
     sourceStudy_(sourceStudy),
--- a/Resources/CMake/OrthancFrameworkConfiguration.cmake	Fri Oct 05 17:46:02 2018 +0200
+++ b/Resources/CMake/OrthancFrameworkConfiguration.cmake	Fri Oct 05 17:48:02 2018 +0200
@@ -167,6 +167,7 @@
     ${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/SetOfCommandsJob.cpp
     ${ORTHANC_ROOT}/Core/JobsEngine/SetOfInstancesJob.cpp
     )
 endif()
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Resources/Graveyard/Multithreading/ICommand.h	Fri Oct 05 17:48:02 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 "IDynamicObject.h"
+
+namespace Orthanc
+{
+  /**
+   * This class is the base class for the "Command" design pattern.
+   * http://en.wikipedia.org/wiki/Command_pattern
+   **/
+  class ICommand : public IDynamicObject
+  {
+  public:
+    virtual bool Execute() = 0;
+  };
+}
--- a/UnitTestsSources/MultiThreadingTests.cpp	Fri Oct 05 17:46:02 2018 +0200
+++ b/UnitTestsSources/MultiThreadingTests.cpp	Fri Oct 05 17:48:02 2018 +0200
@@ -178,8 +178,7 @@
     }
 
   public:
-    DummyInstancesJob(bool hasTrailingStep) :
-      SetOfInstancesJob(hasTrailingStep),
+    DummyInstancesJob() :
       trailingStepDone_(false)
     {
     }
@@ -189,7 +188,7 @@
     {
       if (HasTrailingStep())
       {
-        trailingStepDone_ = (GetPosition() == GetStepsCount());
+        trailingStepDone_ = (GetPosition() == GetCommandsCount());
       }
       else
       {
@@ -861,7 +860,7 @@
               job.HasTrailingStep() == unserialized->HasTrailingStep() &&
               job.GetPosition() == unserialized->GetPosition() &&
               job.GetInstancesCount() == unserialized->GetInstancesCount() &&
-              job.GetStepsCount() == unserialized->GetStepsCount());
+              job.GetCommandsCount() == unserialized->GetCommandsCount());
     }
     else
     {
@@ -1027,7 +1026,7 @@
   // This tests SetOfInstancesJob
   
   {
-    DummyInstancesJob job(false);
+    DummyInstancesJob job;
     job.SetDescription("description");
     job.AddInstance("hello");
     job.AddInstance("nope");
@@ -1605,6 +1604,7 @@
       a = job.GetTargetStudyUid();
       ASSERT_TRUE(job.LookupTargetSeriesUid(b, series));
 
+      job.AddTrailingStep();
       job.Start();
       ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode());
       ASSERT_EQ(JobStepCode_Success, job.Step().GetCode());
@@ -1663,6 +1663,7 @@
     
     ASSERT_EQ(job.GetTargetStudy(), study);
 
+    job.AddTrailingStep();
     job.Start();
     ASSERT_EQ(JobStepCode_Continue, job.Step().GetCode());
     ASSERT_EQ(JobStepCode_Success, job.Step().GetCode());
@@ -1719,8 +1720,8 @@
   {
     Json::Value s;
     
-    DummyInstancesJob job(false);
-    ASSERT_EQ(0, job.GetStepsCount());
+    DummyInstancesJob job;
+    ASSERT_EQ(0, job.GetCommandsCount());
     ASSERT_EQ(0, job.GetInstancesCount());
 
     job.Start();
@@ -1748,10 +1749,10 @@
   {
     Json::Value s;
     
-    DummyInstancesJob job(false);
+    DummyInstancesJob job;
     job.AddInstance("hello");
     job.AddInstance("world");
-    ASSERT_EQ(2, job.GetStepsCount());
+    ASSERT_EQ(2, job.GetCommandsCount());
     ASSERT_EQ(2, job.GetInstancesCount());
 
     job.Start();
@@ -1788,11 +1789,14 @@
   {
     Json::Value s;
     
-    DummyInstancesJob job(true);
-    ASSERT_EQ(1, job.GetStepsCount());
+    DummyInstancesJob job;
     ASSERT_EQ(0, job.GetInstancesCount());
+    ASSERT_EQ(0, job.GetCommandsCount());
+    job.AddTrailingStep();
+    ASSERT_EQ(0, job.GetInstancesCount());
+    ASSERT_EQ(1, job.GetCommandsCount());
 
-    job.Start();
+    job.Start(); // This adds the trailing step
     ASSERT_EQ(0, job.GetPosition());
     ASSERT_TRUE(job.HasTrailingStep());
     ASSERT_FALSE(job.IsTrailingStepDone());
@@ -1817,12 +1821,16 @@
   {
     Json::Value s;
     
-    DummyInstancesJob job(true);
+    DummyInstancesJob job;
     job.AddInstance("hello");
-    ASSERT_EQ(2, job.GetStepsCount());
+    ASSERT_EQ(1, job.GetInstancesCount());
+    ASSERT_EQ(1, job.GetCommandsCount());
+    job.AddTrailingStep();
     ASSERT_EQ(1, job.GetInstancesCount());
-
+    ASSERT_EQ(2, job.GetCommandsCount());
+    
     job.Start();
+    ASSERT_EQ(2, job.GetCommandsCount());
     ASSERT_EQ(0, job.GetPosition());
     ASSERT_TRUE(job.HasTrailingStep());
     ASSERT_FALSE(job.IsTrailingStepDone());