changeset 4587:888868a5dc4e db-changes

ServerIndex now uses StatelessDatabaseOperations
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 10 Mar 2021 15:59:03 +0100
parents 1d96fe7e054e
children 94147ce2f097
files OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp OrthancServer/Sources/Database/StatelessDatabaseOperations.h OrthancServer/Sources/ServerIndex.cpp OrthancServer/Sources/ServerIndex.h
diffstat 4 files changed, 144 insertions(+), 3586 deletions(-) [+]
line wrap: on
line diff
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Tue Mar 09 18:24:59 2021 +0100
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.cpp	Wed Mar 10 15:59:03 2021 +0100
@@ -668,8 +668,24 @@
   StatelessDatabaseOperations::StatelessDatabaseOperations(IDatabaseWrapper& db) : 
     db_(db),
     maxRetries_(10),
-    mainDicomTagsRegistry_(new MainDicomTagsRegistry)
+    mainDicomTagsRegistry_(new MainDicomTagsRegistry),
+    hasFlushToDisk_(db.HasFlushToDisk())
+  {
+  }
+
+
+  void StatelessDatabaseOperations::FlushToDisk()
   {
+    boost::mutex::scoped_lock lock(databaseMutex_);
+        
+    try
+    {
+      db_.FlushToDisk();
+    }
+    catch (OrthancException&)
+    {
+      LOG(ERROR) << "Cannot flush the SQLite database to the disk (is your filesystem full?)";
+    }
   }
 
 
@@ -2415,21 +2431,25 @@
   }
 
 
-  void StatelessDatabaseOperations::LogChange(ChangeType changeType,
+  void StatelessDatabaseOperations::LogChange(int64_t internalId,
+                                              ChangeType changeType,
                                               const std::string& publicId,
                                               ResourceType level)
   {
     class Operations : public IReadWriteOperations
     {
     private:
+      int64_t             internalId_;
       ChangeType          changeType_;
       const std::string&  publicId_;
       ResourceType        level_;
       
     public:
-      Operations(ChangeType changeType,
+      Operations(int64_t internalId,
+                 ChangeType changeType,
                  const std::string& publicId,
                  ResourceType level) :
+        internalId_(internalId),
         changeType_(changeType),
         publicId_(publicId),
         level_(level)
@@ -2440,13 +2460,18 @@
       {
         int64_t id;
         ResourceType type;
-        if (transaction.LookupResource(id, type, publicId_))
+        if (transaction.LookupResource(id, type, publicId_) &&
+            id == internalId_)
         {
-          // Make sure that the resource is still existing. Ignore if
-          // the resource has been deleted, because this function
-          // might e.g. be called from
-          // "StatelessDatabaseOperations::UnstableResourcesMonitorThread()" (for
-          // which a deleted resource not an error case)
+          /**
+           * Make sure that the resource is still existing, with the
+           * same internal ID, which indicates the absence of bouncing
+           * (if deleting then recreating the same resource). Don't
+           * throw an exception if the resource has been deleted,
+           * because this function might e.g. be called from
+           * "StatelessDatabaseOperations::UnstableResourcesMonitorThread()"
+           * (for which a deleted resource is *not* an error case).
+           **/
           if (type == level_)
           {
             transaction.LogChange(id, changeType_, type, publicId_);
@@ -2460,7 +2485,7 @@
       }
     };
 
-    Operations operations(changeType, publicId, level);
+    Operations operations(internalId, changeType, publicId, level);
     Apply(operations);
   }
 
--- a/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Tue Mar 09 18:24:59 2021 +0100
+++ b/OrthancServer/Sources/Database/StatelessDatabaseOperations.h	Wed Mar 10 15:59:03 2021 +0100
@@ -404,11 +404,12 @@
     class MainDicomTagsRegistry;
     class Transaction;
 
-    IDatabaseWrapper&  db_;
-    boost::mutex databaseMutex_;  // TODO - REMOVE
+    IDatabaseWrapper&                            db_;
+    boost::mutex                                 databaseMutex_;  // TODO - REMOVE
     std::unique_ptr<ITransactionContextFactory>  factory_;
-    unsigned int maxRetries_;
-    std::unique_ptr<MainDicomTagsRegistry>  mainDicomTagsRegistry_;
+    unsigned int                                 maxRetries_;
+    std::unique_ptr<MainDicomTagsRegistry>       mainDicomTagsRegistry_;
+    bool                                         hasFlushToDisk_;
 
     void NormalizeLookup(std::vector<DatabaseConstraint>& target,
                          const DatabaseLookup& source,
@@ -433,6 +434,13 @@
       return db_.GetDatabaseVersion();
     }
 
+    void FlushToDisk();
+
+    bool HasFlushToDisk() const
+    {
+      return hasFlushToDisk_;
+    }
+
     void Apply(IReadOnlyOperations& operations);
   
     void Apply(IReadWriteOperations& operations);
@@ -568,7 +576,8 @@
     void DeleteAttachment(const std::string& publicId,
                           FileContentType type);
 
-    void LogChange(ChangeType changeType,
+    void LogChange(int64_t internalId,
+                   ChangeType changeType,
                    const std::string& publicId,
                    ResourceType level);
 
--- a/OrthancServer/Sources/ServerIndex.cpp	Tue Mar 09 18:24:59 2021 +0100
+++ b/OrthancServer/Sources/ServerIndex.cpp	Wed Mar 10 15:59:03 2021 +0100
@@ -38,45 +38,19 @@
 #define NOMINMAX
 #endif
 
-#include "../../OrthancFramework/Sources/DicomFormat/DicomArray.h"
-#include "../../OrthancFramework/Sources/DicomParsing/FromDcmtkBridge.h"
-#include "../../OrthancFramework/Sources/DicomParsing/ParsedDicomFile.h"
 #include "../../OrthancFramework/Sources/Logging.h"
 #include "../../OrthancFramework/Sources/Toolbox.h"
 
-#include "Database/ResourcesContent.h"
 #include "OrthancConfiguration.h"
-#include "Search/DatabaseLookup.h"
-#include "Search/DicomTagConstraint.h"
 #include "ServerContext.h"
 #include "ServerIndexChange.h"
 #include "ServerToolbox.h"
 
-#include <boost/lexical_cast.hpp>
-#include <boost/tuple/tuple.hpp>
-#include <stdio.h>
-#include <stack>
 
 static const uint64_t MEGA_BYTES = 1024 * 1024;
 
 namespace Orthanc
 {
-  static void CopyListToVector(std::vector<std::string>& target,
-                               const std::list<std::string>& source)
-  {
-    target.resize(source.size());
-
-    size_t pos = 0;
-    
-    for (std::list<std::string>::const_iterator
-           it = source.begin(); it != source.end(); ++it)
-    {
-      target[pos] = *it;
-      pos ++;
-    }      
-  }
-
-
   class ServerIndex::Listener : public IDatabaseListener,
                                 public ServerIndex::ITransactionContext
   {
@@ -127,8 +101,7 @@
   public:
     explicit Listener(ServerContext& context) :
       context_(context),
-      insideTransaction_(false),
-      sizeOfAddedAttachments_(0)
+      insideTransaction_(false)
     {
       Reset();
       assert(ResourceType_Patient < ResourceType_Study &&
@@ -283,6 +256,23 @@
     {
       return context_.GetIndex().IsUnstableResource(id);
     }
+
+    virtual void Commit() ORTHANC_OVERRIDE
+    {
+      // We can remove the files once the SQLite transaction has
+      // been successfully committed. Some files might have to be
+      // deleted because of recycling.
+      CommitFilesToRemove();
+      
+      // Send all the pending changes to the Orthanc plugins
+      CommitChanges();
+    }
+
+    virtual int64_t GetCompressedSizeDelta() ORTHANC_OVERRIDE
+    {
+      return (static_cast<int64_t>(GetSizeOfAddedAttachments()) -
+              static_cast<int64_t>(GetSizeOfFilesToRemove()));
+    }
   };
 
 
@@ -298,6 +288,12 @@
       Context(ServerIndex& index) :
         listener_(*index.listener_)
       {
+        listener_.StartTransaction();
+      }
+
+      ~Context()
+      {
+        listener_.EndTransaction();
       }
 
       virtual bool IsUnstableResource(int64_t id) ORTHANC_OVERRIDE
@@ -327,6 +323,16 @@
       {
         listener_.SignalChange(change);
       }
+
+      virtual void Commit() ORTHANC_OVERRIDE
+      {
+        listener_.Commit();
+      }
+
+      virtual int64_t GetCompressedSizeDelta() ORTHANC_OVERRIDE
+      {
+        return listener_.GetCompressedSizeDelta();
+      }
     };
 
     ServerIndex& index_;
@@ -344,56 +350,6 @@
   };    
   
   
-  class ServerIndex::Transaction : public boost::noncopyable
-  {
-  private:
-    ServerIndex& index_;
-    std::unique_ptr<IDatabaseWrapper::ITransaction> transaction_;
-    bool isCommitted_;
-    
-  public:
-    explicit Transaction(ServerIndex& index,
-                         TransactionType type) : 
-      index_(index),
-      isCommitted_(false)
-    {
-      transaction_.reset(index_.db_.StartTransaction(type));
-      index_.listener_->StartTransaction();
-    }
-
-    ~Transaction()
-    {
-      index_.listener_->EndTransaction();
-
-      if (!isCommitted_)
-      {
-        transaction_->Rollback();
-      }
-    }
-
-    void Commit()
-    {
-      if (!isCommitted_)
-      {
-        int64_t delta = (static_cast<int64_t>(index_.listener_->GetSizeOfAddedAttachments()) -
-                         static_cast<int64_t>(index_.listener_->GetSizeOfFilesToRemove()));
-
-        transaction_->Commit(delta);
-
-        // We can remove the files once the SQLite transaction has
-        // been successfully committed. Some files might have to be
-        // deleted because of recycling.
-        index_.listener_->CommitFilesToRemove();
-
-        // Send all the pending changes to the Orthanc plugins
-        index_.listener_->CommitChanges();
-
-        isCommitted_ = true;
-      }
-    }
-  };
-
-
   class ServerIndex::UnstableResourcePayload
   {
   private:
@@ -562,19 +518,7 @@
       if (count >= countThreshold)
       {
         Logging::Flush();
-
-        {
-          boost::mutex::scoped_lock lock(that->databaseMutex_);
-        
-          try
-          {
-            that->db_.FlushToDisk();
-          }
-          catch (OrthancException&)
-          {
-            LOG(ERROR) << "Cannot flush the SQLite database to the disk (is your filesystem full?)";
-          }
-        }
+        that->FlushToDisk();
         
         count = 0;
       }
@@ -584,22 +528,6 @@
   }
 
 
-  void ServerIndex::ReadWriteTransaction::LogChange(int64_t internalId,
-                                                    ChangeType changeType,
-                                                    ResourceType resourceType,
-                                                    const std::string& publicId)
-  {
-    ServerIndexChange change(changeType, resourceType, publicId);
-
-    if (changeType <= ChangeType_INTERNAL_LastLogged)
-    {
-      db_.LogChange(internalId, change);
-    }
-
-    GetTransactionContext().SignalChange(change);
-  }
-
-
   bool ServerIndex::IsUnstableResource(int64_t id)
   {
     boost::mutex::scoped_lock lock(monitoringMutex_);
@@ -609,16 +537,14 @@
 
   ServerIndex::ServerIndex(ServerContext& context,
                            IDatabaseWrapper& db,
-                           unsigned int threadSleepGranularityMilliseconds) : 
+                           unsigned int threadSleepGranularityMilliseconds) :
+    StatelessDatabaseOperations(db),
     done_(false),
-    db_(db),
     maximumStorageSize_(0),
-    maximumPatients_(0),
-    mainDicomTagsRegistry_(new MainDicomTagsRegistry),
-    maxRetries_(10)
+    maximumPatients_(0)
   {
     listener_.reset(new Listener(context));
-    db_.SetListener(*listener_);
+    db.SetListener(*listener_);
 
     SetTransactionContextFactory(new TransactionContextFactory(*this));
 
@@ -626,7 +552,7 @@
     // execution of Orthanc
     StandaloneRecycling(maximumStorageSize_, maximumPatients_);
 
-    if (db.HasFlushToDisk())
+    if (HasFlushToDisk())
     {
       flushThread_ = boost::thread(FlushThread, this, threadSleepGranularityMilliseconds);
     }
@@ -636,7 +562,6 @@
   }
 
 
-
   ServerIndex::~ServerIndex()
   {
     if (!done_)
@@ -647,15 +572,13 @@
   }
 
 
-
   void ServerIndex::Stop()
   {
     if (!done_)
     {
       done_ = true;
 
-      if (db_.HasFlushToDisk() &&
-          flushThread_.joinable())
+      if (flushThread_.joinable())
       {
         flushThread_.join();
       }
@@ -668,121 +591,6 @@
   }
 
 
-
-  SeriesStatus ServerIndex::ReadOnlyTransaction::GetSeriesStatus(int64_t id,
-                                                                 int64_t expectedNumberOfInstances)
-  {
-    std::list<std::string> values;
-    db_.GetChildrenMetadata(values, id, MetadataType_Instance_IndexInSeries);
-
-    std::set<int64_t> instances;
-
-    for (std::list<std::string>::const_iterator
-           it = values.begin(); it != values.end(); ++it)
-    {
-      int64_t index;
-
-      try
-      {
-        index = boost::lexical_cast<int64_t>(*it);
-      }
-      catch (boost::bad_lexical_cast&)
-      {
-        return SeriesStatus_Unknown;
-      }
-      
-      if (!(index > 0 && index <= expectedNumberOfInstances))
-      {
-        // Out-of-range instance index
-        return SeriesStatus_Inconsistent;
-      }
-
-      if (instances.find(index) != instances.end())
-      {
-        // Twice the same instance index
-        return SeriesStatus_Inconsistent;
-      }
-
-      instances.insert(index);
-    }
-
-    if (static_cast<int64_t>(instances.size()) == expectedNumberOfInstances)
-    {
-      return SeriesStatus_Complete;
-    }
-    else
-    {
-      return SeriesStatus_Missing;
-    }
-  }
-
-
-  void ServerIndex::ReadOnlyTransaction::MainDicomTagsToJson(Json::Value& target,
-                                                             int64_t resourceId,
-                                                             ResourceType resourceType)
-  {
-    DicomMap tags;
-    db_.GetMainDicomTags(tags, resourceId);
-
-    if (resourceType == ResourceType_Study)
-    {
-      DicomMap t1, t2;
-      tags.ExtractStudyInformation(t1);
-      tags.ExtractPatientInformation(t2);
-
-      target["MainDicomTags"] = Json::objectValue;
-      FromDcmtkBridge::ToJson(target["MainDicomTags"], t1, true);
-
-      target["PatientMainDicomTags"] = Json::objectValue;
-      FromDcmtkBridge::ToJson(target["PatientMainDicomTags"], t2, true);
-    }
-    else
-    {
-      target["MainDicomTags"] = Json::objectValue;
-      FromDcmtkBridge::ToJson(target["MainDicomTags"], tags, true);
-    }
-  }
-
-  
-  template <typename T>
-  static void FormatLog(Json::Value& target,
-                        const std::list<T>& log,
-                        const std::string& name,
-                        bool done,
-                        int64_t since,
-                        bool hasLast,
-                        int64_t last)
-  {
-    Json::Value items = Json::arrayValue;
-    for (typename std::list<T>::const_iterator
-           it = log.begin(); it != log.end(); ++it)
-    {
-      Json::Value item;
-      it->Format(item);
-      items.append(item);
-    }
-
-    target = Json::objectValue;
-    target[name] = items;
-    target["Done"] = done;
-
-    if (!hasLast)
-    {
-      // Best-effort guess of the last index in the sequence
-      if (log.empty())
-      {
-        last = since;
-      }
-      else
-      {
-        last = log.back().GetSeq();
-      }
-    }
-    
-    target["Last"] = static_cast<int>(last);
-  }
-
-
   void ServerIndex::SetMaximumPatientCount(unsigned int count) 
   {
     {
@@ -802,6 +610,7 @@
     StandaloneRecycling(maximumStorageSize_, maximumPatients_);
   }
 
+  
   void ServerIndex::SetMaximumStorageSize(uint64_t size) 
   {
     {
@@ -844,39 +653,52 @@
       // Check for stable resources each few seconds
       boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleepGranularityMilliseconds));
 
-      boost::mutex::scoped_lock lock(that->monitoringMutex_);
-
-      while (!that->unstableResources_.IsEmpty() &&
-             that->unstableResources_.GetOldestPayload().GetAge() > static_cast<unsigned int>(stableAge))
+      for (;;)
       {
-        // This DICOM resource has not received any new instance for
-        // some time. It can be considered as stable.
-          
-        UnstableResourcePayload payload;
-        int64_t id = that->unstableResources_.RemoveOldest(payload);
+        UnstableResourcePayload stableResource;
+        int64_t stableId;
+
+        {      
+          boost::mutex::scoped_lock lock(that->monitoringMutex_);
 
-        // Ensure that the resource is still existing before logging the change
-        if (that->db_.IsExistingResource(id))
-        {
-          switch (payload.GetResourceType())
+          if (!that->unstableResources_.IsEmpty() &&
+              that->unstableResources_.GetOldestPayload().GetAge() > static_cast<unsigned int>(stableAge))
+          {
+            // This DICOM resource has not received any new instance for
+            // some time. It can be considered as stable.
+            stableId = that->unstableResources_.RemoveOldest(stableResource);
+            //LOG(TRACE) << "Stable resource: " << EnumerationToString(stableResource.GetResourceType()) << " " << stableId;
+          }
+          else
           {
-            case ResourceType_Patient:
-              that->LogChange(ChangeType_StablePatient, payload.GetPublicId(), ResourceType_Patient);
-              break;
-
-            case ResourceType_Study:
-              that->LogChange(ChangeType_StableStudy, payload.GetPublicId(), ResourceType_Study);
-              break;
+            // No more stable DICOM resource, leave the internal loop
+            break;
+          }
+        }
 
-            case ResourceType_Series:
-              that->LogChange(ChangeType_StableSeries, payload.GetPublicId(), ResourceType_Series);
-              break;
-
-            default:
-              throw OrthancException(ErrorCode_InternalError);
-          }
-
-          //LOG(INFO) << "Stable resource: " << EnumerationToString(payload.type_) << " " << id;
+        /**
+         * WARNING: Don't protect the calls to "LogChange()" using
+         * "monitoringMutex_", as this could lead to deadlocks in
+         * other threads (typically, if "Store()" is being running in
+         * another thread, which leads to calls to "MarkAsUnstable()",
+         * which leads to two lockings of "monitoringMutex_").
+         **/
+        switch (stableResource.GetResourceType())
+        {
+          case ResourceType_Patient:
+            that->LogChange(stableId, ChangeType_StablePatient, stableResource.GetPublicId(), ResourceType_Patient);
+            break;
+            
+          case ResourceType_Study:
+            that->LogChange(stableId, ChangeType_StableStudy, stableResource.GetPublicId(), ResourceType_Study);
+            break;
+            
+          case ResourceType_Series:
+            that->LogChange(stableId, ChangeType_StableSeries, stableResource.GetPublicId(), ResourceType_Series);
+            break;
+            
+          default:
+            throw OrthancException(ErrorCode_InternalError);
         }
       }
     }
@@ -902,2298 +724,10 @@
   }
 
 
-
-  void ServerIndex::NormalizeLookup(std::vector<DatabaseConstraint>& target,
-                                    const DatabaseLookup& source,
-                                    ResourceType queryLevel) const
-  {
-    assert(mainDicomTagsRegistry_.get() != NULL);
-
-    target.clear();
-    target.reserve(source.GetConstraintsCount());
-
-    for (size_t i = 0; i < source.GetConstraintsCount(); i++)
-    {
-      ResourceType level;
-      DicomTagType type;
-      
-      mainDicomTagsRegistry_->LookupTag(level, type, source.GetConstraint(i).GetTag());
-
-      if (type == DicomTagType_Identifier ||
-          type == DicomTagType_Main)
-      {
-        // Use the fact that patient-level tags are copied at the study level
-        if (level == ResourceType_Patient &&
-            queryLevel != ResourceType_Patient)
-        {
-          level = ResourceType_Study;
-        }
-        
-        target.push_back(source.GetConstraint(i).ConvertToDatabaseConstraint(level, type));
-      }
-    }
-  }
-
-
-
-
-
-  /***
-   ** PROTOTYPING FOR DB REFACTORING BELOW
-   ***/
-    
-  namespace
-  {
-    /**
-     * Some handy templates to reduce the verbosity in the definitions
-     * of the internal classes.
-     **/
-    
-    template <typename Operations,
-              typename Tuple>
-    class TupleOperationsWrapper : public ServerIndex::IReadOnlyOperations
-    {
-    protected:
-      Operations&   operations_;
-      const Tuple&  tuple_;
-    
-    public:
-      TupleOperationsWrapper(Operations& operations,
-                             const Tuple& tuple) :
-        operations_(operations),
-        tuple_(tuple)
-      {
-      }
-    
-      virtual void Apply(ServerIndex::ReadOnlyTransaction& transaction) ORTHANC_OVERRIDE
-      {
-        operations_.ApplyTuple(transaction, tuple_);
-      }
-    };
-
-
-    template <typename T1>
-    class ReadOnlyOperationsT1 : public boost::noncopyable
-    {
-    public:
-      typedef typename boost::tuple<T1>  Tuple;
-      
-      virtual ~ReadOnlyOperationsT1()
-      {
-      }
-
-      virtual void ApplyTuple(ServerIndex::ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) = 0;
-
-      void Apply(ServerIndex& index,
-                 T1 t1)
-      {
-        const Tuple tuple(t1);
-        TupleOperationsWrapper<ReadOnlyOperationsT1, Tuple> wrapper(*this, tuple);
-        index.Apply(wrapper);
-      }
-    };
-
-
-    template <typename T1,
-              typename T2>
-    class ReadOnlyOperationsT2 : public boost::noncopyable
-    {
-    public:
-      typedef typename boost::tuple<T1, T2>  Tuple;
-      
-      virtual ~ReadOnlyOperationsT2()
-      {
-      }
-
-      virtual void ApplyTuple(ServerIndex::ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) = 0;
-
-      void Apply(ServerIndex& index,
-                 T1 t1,
-                 T2 t2)
-      {
-        const Tuple tuple(t1, t2);
-        TupleOperationsWrapper<ReadOnlyOperationsT2, Tuple> wrapper(*this, tuple);
-        index.Apply(wrapper);
-      }
-    };
-
-
-    template <typename T1,
-              typename T2,
-              typename T3>
-    class ReadOnlyOperationsT3 : public boost::noncopyable
-    {
-    public:
-      typedef typename boost::tuple<T1, T2, T3>  Tuple;
-      
-      virtual ~ReadOnlyOperationsT3()
-      {
-      }
-
-      virtual void ApplyTuple(ServerIndex::ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) = 0;
-
-      void Apply(ServerIndex& index,
-                 T1 t1,
-                 T2 t2,
-                 T3 t3)
-      {
-        const Tuple tuple(t1, t2, t3);
-        TupleOperationsWrapper<ReadOnlyOperationsT3, Tuple> wrapper(*this, tuple);
-        index.Apply(wrapper);
-      }
-    };
-
-
-    template <typename T1,
-              typename T2,
-              typename T3,
-              typename T4>
-    class ReadOnlyOperationsT4 : public boost::noncopyable
-    {
-    public:
-      typedef typename boost::tuple<T1, T2, T3, T4>  Tuple;
-      
-      virtual ~ReadOnlyOperationsT4()
-      {
-      }
-
-      virtual void ApplyTuple(ServerIndex::ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) = 0;
-
-      void Apply(ServerIndex& index,
-                 T1 t1,
-                 T2 t2,
-                 T3 t3,
-                 T4 t4)
-      {
-        const Tuple tuple(t1, t2, t3, t4);
-        TupleOperationsWrapper<ReadOnlyOperationsT4, Tuple> wrapper(*this, tuple);
-        index.Apply(wrapper);
-      }
-    };
-
-
-    template <typename T1,
-              typename T2,
-              typename T3,
-              typename T4,
-              typename T5>
-    class ReadOnlyOperationsT5 : public boost::noncopyable
-    {
-    public:
-      typedef typename boost::tuple<T1, T2, T3, T4, T5>  Tuple;
-      
-      virtual ~ReadOnlyOperationsT5()
-      {
-      }
-
-      virtual void ApplyTuple(ServerIndex::ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) = 0;
-
-      void Apply(ServerIndex& index,
-                 T1 t1,
-                 T2 t2,
-                 T3 t3,
-                 T4 t4,
-                 T5 t5)
-      {
-        const Tuple tuple(t1, t2, t3, t4, t5);
-        TupleOperationsWrapper<ReadOnlyOperationsT5, Tuple> wrapper(*this, tuple);
-        index.Apply(wrapper);
-      }
-    };
-
-
-    template <typename T1,
-              typename T2,
-              typename T3,
-              typename T4,
-              typename T5,
-              typename T6>
-    class ReadOnlyOperationsT6 : public boost::noncopyable
-    {
-    public:
-      typedef typename boost::tuple<T1, T2, T3, T4, T5, T6>  Tuple;
-      
-      virtual ~ReadOnlyOperationsT6()
-      {
-      }
-
-      virtual void ApplyTuple(ServerIndex::ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) = 0;
-
-      void Apply(ServerIndex& index,
-                 T1 t1,
-                 T2 t2,
-                 T3 t3,
-                 T4 t4,
-                 T5 t5,
-                 T6 t6)
-      {
-        const Tuple tuple(t1, t2, t3, t4, t5, t6);
-        TupleOperationsWrapper<ReadOnlyOperationsT6, Tuple> wrapper(*this, tuple);
-        index.Apply(wrapper);
-      }
-    };
-  }
-  
-
-  void ServerIndex::ApplyInternal(IReadOnlyOperations* readOperations,
-                                  IReadWriteOperations* writeOperations)
-  {
-    if ((readOperations == NULL && writeOperations == NULL) ||
-        (readOperations != NULL && writeOperations != NULL))
-    {
-      throw OrthancException(ErrorCode_InternalError);
-    }
-
-    if (factory_.get() == NULL)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls, "No transaction context was provided");     
-    }
-    
-    unsigned int count = 0;
-
-    for (;;)
-    {
-      try
-      {
-        std::unique_ptr<ITransactionContext>  context(factory_->Create());
-
-        if (context.get() == NULL)
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-        
-        boost::mutex::scoped_lock lock(databaseMutex_);  // TODO - REMOVE
-
-        if (readOperations != NULL)
-        {
-          /**
-           * IMPORTANT: In Orthanc <= 1.9.1, there was no transaction
-           * in this case. This was OK because of the presence of the
-           * global mutex protecting the database.
-           **/
-          
-          Transaction transaction(*this, TransactionType_ReadOnly);  // TODO - Only if not "TransactionType_Implicit"
-          {
-            ReadOnlyTransaction t(db_, *context);
-            readOperations->Apply(t);
-          }
-          transaction.Commit();
-        }
-        else
-        {
-          assert(writeOperations != NULL);
-          
-          Transaction transaction(*this, TransactionType_ReadWrite);
-          {
-            ReadWriteTransaction t(db_, *context);
-            writeOperations->Apply(t);
-          }
-          transaction.Commit();
-        }
-        
-        return;  // Success
-      }
-      catch (OrthancException& e)
-      {
-        if (e.GetErrorCode() == ErrorCode_DatabaseCannotSerialize)
-        {
-          if (count == maxRetries_)
-          {
-            throw;
-          }
-          else
-          {
-            count++;
-            boost::this_thread::sleep(boost::posix_time::milliseconds(100 * count));
-          }          
-        }
-        else if (e.GetErrorCode() == ErrorCode_DatabaseUnavailable)
-        {
-          if (count == maxRetries_)
-          {
-            throw;
-          }
-          else
-          {
-            count++;
-            boost::this_thread::sleep(boost::posix_time::milliseconds(1000));
-          }
-        }
-        else
-        {
-          throw;
-        }
-      }
-    }
-  }
-
-  
-  void ServerIndex::SetTransactionContextFactory(ITransactionContextFactory* factory)
-  {
-    if (factory == NULL)
-    {
-      throw OrthancException(ErrorCode_NullPointer);
-    }
-    else if (factory_.get() != NULL)
-    {
-      throw OrthancException(ErrorCode_BadSequenceOfCalls);
-    }
-    else
-    {
-      factory_.reset(factory);
-    }
-  }
-    
-
-  void ServerIndex::Apply(IReadOnlyOperations& operations)
-  {
-    ApplyInternal(&operations, NULL);
-  }
-  
-
-  void ServerIndex::Apply(IReadWriteOperations& operations)
-  {
-    ApplyInternal(NULL, &operations);
-  }
-  
-
-  bool ServerIndex::ExpandResource(Json::Value& target,
-                                   const std::string& publicId,
-                                   ResourceType level)
-  {    
-    class Operations : public ReadOnlyOperationsT4<bool&, Json::Value&, const std::string&, ResourceType>
-    {
-    private:
-      static bool LookupStringMetadata(std::string& result,
-                                       const std::map<MetadataType, std::string>& metadata,
-                                       MetadataType type)
-      {
-        std::map<MetadataType, std::string>::const_iterator found = metadata.find(type);
-
-        if (found == metadata.end())
-        {
-          return false;
-        }
-        else
-        {
-          result = found->second;
-          return true;
-        }
-      }
-
-
-      static bool LookupIntegerMetadata(int64_t& result,
-                                        const std::map<MetadataType, std::string>& metadata,
-                                        MetadataType type)
-      {
-        std::string s;
-        if (!LookupStringMetadata(s, metadata, type))
-        {
-          return false;
-        }
-
-        try
-        {
-          result = boost::lexical_cast<int64_t>(s);
-          return true;
-        }
-        catch (boost::bad_lexical_cast&)
-        {
-          return false;
-        }
-      }
-
-
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        // Lookup for the requested resource
-        int64_t internalId;  // unused
-        ResourceType type;
-        std::string parent;
-        if (!transaction.LookupResourceAndParent(internalId, type, parent, tuple.get<2>()) ||
-            type != tuple.get<3>())
-        {
-          tuple.get<0>() = false;
-        }
-        else
-        {
-          Json::Value& target = tuple.get<1>();
-          target = Json::objectValue;
-        
-          // Set information about the parent resource (if it exists)
-          if (type == ResourceType_Patient)
-          {
-            if (!parent.empty())
-            {
-              throw OrthancException(ErrorCode_DatabasePlugin);
-            }
-          }
-          else
-          {
-            if (parent.empty())
-            {
-              throw OrthancException(ErrorCode_DatabasePlugin);
-            }
-
-            switch (type)
-            {
-              case ResourceType_Study:
-                target["ParentPatient"] = parent;
-                break;
-
-              case ResourceType_Series:
-                target["ParentStudy"] = parent;
-                break;
-
-              case ResourceType_Instance:
-                target["ParentSeries"] = parent;
-                break;
-
-              default:
-                throw OrthancException(ErrorCode_InternalError);
-            }
-          }
-
-          // List the children resources
-          std::list<std::string> children;
-          transaction.GetChildrenPublicId(children, internalId);
-
-          if (type != ResourceType_Instance)
-          {
-            Json::Value c = Json::arrayValue;
-
-            for (std::list<std::string>::const_iterator
-                   it = children.begin(); it != children.end(); ++it)
-            {
-              c.append(*it);
-            }
-
-            switch (type)
-            {
-              case ResourceType_Patient:
-                target["Studies"] = c;
-                break;
-
-              case ResourceType_Study:
-                target["Series"] = c;
-                break;
-
-              case ResourceType_Series:
-                target["Instances"] = c;
-                break;
-
-              default:
-                throw OrthancException(ErrorCode_InternalError);
-            }
-          }
-
-          // Extract the metadata
-          std::map<MetadataType, std::string> metadata;
-          transaction.GetAllMetadata(metadata, internalId);
-
-          // Set the resource type
-          switch (type)
-          {
-            case ResourceType_Patient:
-              target["Type"] = "Patient";
-              break;
-
-            case ResourceType_Study:
-              target["Type"] = "Study";
-              break;
-
-            case ResourceType_Series:
-            {
-              target["Type"] = "Series";
-
-              int64_t i;
-              if (LookupIntegerMetadata(i, metadata, MetadataType_Series_ExpectedNumberOfInstances))
-              {
-                target["ExpectedNumberOfInstances"] = static_cast<int>(i);
-                target["Status"] = EnumerationToString(transaction.GetSeriesStatus(internalId, i));
-              }
-              else
-              {
-                target["ExpectedNumberOfInstances"] = Json::nullValue;
-                target["Status"] = EnumerationToString(SeriesStatus_Unknown);
-              }
-
-              break;
-            }
-
-            case ResourceType_Instance:
-            {
-              target["Type"] = "Instance";
-
-              FileInfo attachment;
-              if (!transaction.LookupAttachment(attachment, internalId, FileContentType_Dicom))
-              {
-                throw OrthancException(ErrorCode_InternalError);
-              }
-
-              target["FileSize"] = static_cast<unsigned int>(attachment.GetUncompressedSize());
-              target["FileUuid"] = attachment.GetUuid();
-
-              int64_t i;
-              if (LookupIntegerMetadata(i, metadata, MetadataType_Instance_IndexInSeries))
-              {
-                target["IndexInSeries"] = static_cast<int>(i);
-              }
-              else
-              {
-                target["IndexInSeries"] = Json::nullValue;
-              }
-
-              break;
-            }
-
-            default:
-              throw OrthancException(ErrorCode_InternalError);
-          }
-
-          // Record the remaining information
-          target["ID"] = tuple.get<2>();
-          transaction.MainDicomTagsToJson(target, internalId, type);
-
-          std::string tmp;
-
-          if (LookupStringMetadata(tmp, metadata, MetadataType_AnonymizedFrom))
-          {
-            target["AnonymizedFrom"] = tmp;
-          }
-
-          if (LookupStringMetadata(tmp, metadata, MetadataType_ModifiedFrom))
-          {
-            target["ModifiedFrom"] = tmp;
-          }
-
-          if (type == ResourceType_Patient ||
-              type == ResourceType_Study ||
-              type == ResourceType_Series)
-          {
-            target["IsStable"] = !transaction.GetTransactionContext().IsUnstableResource(internalId);
-
-            if (LookupStringMetadata(tmp, metadata, MetadataType_LastUpdate))
-            {
-              target["LastUpdate"] = tmp;
-            }
-          }
-
-          tuple.get<0>() = true;
-        }
-      }
-    };
-
-    bool found;
-    Operations operations;
-    operations.Apply(*this, found, target, publicId, level);
-    return found;
-  }
-
-
-  void ServerIndex::GetAllMetadata(std::map<MetadataType, std::string>& target,
-                                   const std::string& publicId,
-                                   ResourceType level)
-  {
-    class Operations : public ReadOnlyOperationsT3<std::map<MetadataType, std::string>&, const std::string&, ResourceType>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        ResourceType type;
-        int64_t id;
-        if (!transaction.LookupResource(id, type, tuple.get<1>()) ||
-            tuple.get<2>() != type)
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-        else
-        {
-          transaction.GetAllMetadata(tuple.get<0>(), id);
-        }
-      }
-    };
-
-    Operations operations;
-    operations.Apply(*this, target, publicId, level);
-  }
-
-
-  bool ServerIndex::LookupAttachment(FileInfo& attachment,
-                                     const std::string& instancePublicId,
-                                     FileContentType contentType)
-  {
-    class Operations : public ReadOnlyOperationsT4<bool&, FileInfo&, const std::string&, FileContentType>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        int64_t internalId;
-        ResourceType type;
-        if (!transaction.LookupResource(internalId, type, tuple.get<2>()))
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-        else if (transaction.LookupAttachment(tuple.get<1>(), internalId, tuple.get<3>()))
-        {
-          assert(tuple.get<1>().GetContentType() == tuple.get<3>());
-          tuple.get<0>() = true;
-        }
-        else
-        {
-          tuple.get<0>() = false;
-        }
-      }
-    };
-
-    bool found;
-    Operations operations;
-    operations.Apply(*this, found, attachment, instancePublicId, contentType);
-    return found;
-  }
-
-
-  void ServerIndex::GetAllUuids(std::list<std::string>& target,
-                                ResourceType resourceType)
-  {
-    class Operations : public ReadOnlyOperationsT2<std::list<std::string>&, ResourceType>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        // TODO - CANDIDATE FOR "TransactionType_Implicit"
-        transaction.GetAllPublicIds(tuple.get<0>(), tuple.get<1>());
-      }
-    };
-
-    Operations operations;
-    operations.Apply(*this, target, resourceType);
-  }
-
-
-  void ServerIndex::GetAllUuids(std::list<std::string>& target,
-                                ResourceType resourceType,
-                                size_t since,
-                                size_t limit)
-  {
-    if (limit == 0)
-    {
-      target.clear();
-    }
-    else
-    {
-      class Operations : public ReadOnlyOperationsT4<std::list<std::string>&, ResourceType, size_t, size_t>
-      {
-      public:
-        virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                                const Tuple& tuple) ORTHANC_OVERRIDE
-        {
-          // TODO - CANDIDATE FOR "TransactionType_Implicit"
-          transaction.GetAllPublicIds(tuple.get<0>(), tuple.get<1>(), tuple.get<2>(), tuple.get<3>());
-        }
-      };
-
-      Operations operations;
-      operations.Apply(*this, target, resourceType, since, limit);
-    }
-  }
-
-
-  void ServerIndex::GetGlobalStatistics(/* out */ uint64_t& diskSize,
-                                        /* out */ uint64_t& uncompressedSize,
-                                        /* out */ uint64_t& countPatients, 
-                                        /* out */ uint64_t& countStudies, 
-                                        /* out */ uint64_t& countSeries, 
-                                        /* out */ uint64_t& countInstances)
-  {
-    class Operations : public ReadOnlyOperationsT6<uint64_t&, uint64_t&, uint64_t&, uint64_t&, uint64_t&, uint64_t&>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        tuple.get<0>() = transaction.GetTotalCompressedSize();
-        tuple.get<1>() = transaction.GetTotalUncompressedSize();
-        tuple.get<2>() = transaction.GetResourceCount(ResourceType_Patient);
-        tuple.get<3>() = transaction.GetResourceCount(ResourceType_Study);
-        tuple.get<4>() = transaction.GetResourceCount(ResourceType_Series);
-        tuple.get<5>() = transaction.GetResourceCount(ResourceType_Instance);
-      }
-    };
-    
-    Operations operations;
-    operations.Apply(*this, diskSize, uncompressedSize, countPatients,
-                     countStudies, countSeries, countInstances);
-  }
-
-
-  void ServerIndex::GetChanges(Json::Value& target,
-                               int64_t since,                               
-                               unsigned int maxResults)
-  {
-    class Operations : public ReadOnlyOperationsT3<Json::Value&, int64_t, unsigned int>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        // NB: In Orthanc <= 1.3.2, a transaction was missing, as
-        // "GetLastChange()" involves calls to "GetPublicId()"
-
-        std::list<ServerIndexChange> changes;
-        bool done;
-        bool hasLast = false;
-        int64_t last = 0;
-
-        transaction.GetChanges(changes, done, tuple.get<1>(), tuple.get<2>());
-        if (changes.empty())
-        {
-          last = transaction.GetLastChangeIndex();
-          hasLast = true;
-        }
-
-        FormatLog(tuple.get<0>(), changes, "Changes", done, tuple.get<1>(), hasLast, last);
-      }
-    };
-    
-    Operations operations;
-    operations.Apply(*this, target, since, maxResults);
-  }
-
-
-  void ServerIndex::GetLastChange(Json::Value& target)
-  {
-    class Operations : public ReadOnlyOperationsT1<Json::Value&>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        // NB: In Orthanc <= 1.3.2, a transaction was missing, as
-        // "GetLastChange()" involves calls to "GetPublicId()"
-
-        std::list<ServerIndexChange> changes;
-        bool hasLast = false;
-        int64_t last = 0;
-
-        transaction.GetLastChange(changes);
-        if (changes.empty())
-        {
-          last = transaction.GetLastChangeIndex();
-          hasLast = true;
-        }
-
-        FormatLog(tuple.get<0>(), changes, "Changes", true, 0, hasLast, last);
-      }
-    };
-    
-    Operations operations;
-    operations.Apply(*this, target);
-  }
-
-
-  void ServerIndex::GetExportedResources(Json::Value& target,
-                                         int64_t since,
-                                         unsigned int maxResults)
-  {
-    class Operations : public ReadOnlyOperationsT3<Json::Value&, int64_t, unsigned int>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        // TODO - CANDIDATE FOR "TransactionType_Implicit"
-
-        std::list<ExportedResource> exported;
-        bool done;
-        transaction.GetExportedResources(exported, done, tuple.get<1>(), tuple.get<2>());
-        FormatLog(tuple.get<0>(), exported, "Exports", done, tuple.get<1>(), false, -1);
-      }
-    };
-    
-    Operations operations;
-    operations.Apply(*this, target, since, maxResults);
-  }
-
-
-  void ServerIndex::GetLastExportedResource(Json::Value& target)
-  {
-    class Operations : public ReadOnlyOperationsT1<Json::Value&>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        // TODO - CANDIDATE FOR "TransactionType_Implicit"
-
-        std::list<ExportedResource> exported;
-        transaction.GetLastExportedResource(exported);
-        FormatLog(tuple.get<0>(), exported, "Exports", true, 0, false, -1);
-      }
-    };
-    
-    Operations operations;
-    operations.Apply(*this, target);
-  }
-
-
-  bool ServerIndex::IsProtectedPatient(const std::string& publicId)
-  {
-    class Operations : public ReadOnlyOperationsT2<bool&, const std::string&>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        // Lookup for the requested resource
-        int64_t id;
-        ResourceType type;
-        if (!transaction.LookupResource(id, type, tuple.get<1>()) ||
-            type != ResourceType_Patient)
-        {
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-        }
-        else
-        {
-          tuple.get<0>() = transaction.IsProtectedPatient(id);
-        }
-      }
-    };
-
-    bool isProtected;
-    Operations operations;
-    operations.Apply(*this, isProtected, publicId);
-    return isProtected;
-  }
-
-
-  void ServerIndex::GetChildren(std::list<std::string>& result,
-                                const std::string& publicId)
-  {
-    class Operations : public ReadOnlyOperationsT2<std::list<std::string>&, const std::string&>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        ResourceType type;
-        int64_t resource;
-        if (!transaction.LookupResource(resource, type, tuple.get<1>()))
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-        else if (type == ResourceType_Instance)
-        {
-          // An instance cannot have a child
-          throw OrthancException(ErrorCode_BadParameterType);
-        }
-        else
-        {
-          std::list<int64_t> tmp;
-          transaction.GetChildrenInternalId(tmp, resource);
-
-          tuple.get<0>().clear();
-
-          for (std::list<int64_t>::const_iterator 
-                 it = tmp.begin(); it != tmp.end(); ++it)
-          {
-            tuple.get<0>().push_back(transaction.GetPublicId(*it));
-          }
-        }
-      }
-    };
-    
-    Operations operations;
-    operations.Apply(*this, result, publicId);
-  }
-
-
-  void ServerIndex::GetChildInstances(std::list<std::string>& result,
-                                      const std::string& publicId)
-  {
-    class Operations : public ReadOnlyOperationsT2<std::list<std::string>&, const std::string&>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        tuple.get<0>().clear();
-        
-        ResourceType type;
-        int64_t top;
-        if (!transaction.LookupResource(top, type, tuple.get<1>()))
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-        else if (type == ResourceType_Instance)
-        {
-          // The resource is already an instance: Do not go down the hierarchy
-          tuple.get<0>().push_back(tuple.get<1>());
-        }
-        else
-        {
-          std::stack<int64_t> toExplore;
-          toExplore.push(top);
-
-          std::list<int64_t> tmp;
-          while (!toExplore.empty())
-          {
-            // Get the internal ID of the current resource
-            int64_t resource = toExplore.top();
-            toExplore.pop();
-
-            // TODO - This could be optimized by seeing how many
-            // levels "type == transaction.GetResourceType(top)" is
-            // above the "instances level"
-            if (transaction.GetResourceType(resource) == ResourceType_Instance)
-            {
-              tuple.get<0>().push_back(transaction.GetPublicId(resource));
-            }
-            else
-            {
-              // Tag all the children of this resource as to be explored
-              transaction.GetChildrenInternalId(tmp, resource);
-              for (std::list<int64_t>::const_iterator 
-                     it = tmp.begin(); it != tmp.end(); ++it)
-              {
-                toExplore.push(*it);
-              }
-            }
-          }
-        }
-      }
-    };
-    
-    Operations operations;
-    operations.Apply(*this, result, publicId);
-  }
-
-
-  bool ServerIndex::LookupMetadata(std::string& target,
-                                   const std::string& publicId,
-                                   ResourceType expectedType,
-                                   MetadataType type)
-  {
-    class Operations : public ReadOnlyOperationsT5<bool&, std::string&, const std::string&, ResourceType, MetadataType>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        ResourceType rtype;
-        int64_t id;
-        if (!transaction.LookupResource(id, rtype, tuple.get<2>()) ||
-            rtype != tuple.get<3>())
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-        else
-        {
-          tuple.get<0>() = transaction.LookupMetadata(tuple.get<1>(), id, tuple.get<4>());
-        }
-      }
-    };
-
-    bool found;
-    Operations operations;
-    operations.Apply(*this, found, target, publicId, expectedType, type);
-    return found;
-  }
-
-
-  void ServerIndex::ListAvailableAttachments(std::set<FileContentType>& target,
-                                             const std::string& publicId,
-                                             ResourceType expectedType)
-  {
-    class Operations : public ReadOnlyOperationsT3<std::set<FileContentType>&, const std::string&, ResourceType>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        ResourceType type;
-        int64_t id;
-        if (!transaction.LookupResource(id, type, tuple.get<1>()) ||
-            tuple.get<2>() != type)
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-        else
-        {
-          transaction.ListAvailableAttachments(tuple.get<0>(), id);
-        }
-      }
-    };
-    
-    Operations operations;
-    operations.Apply(*this, target, publicId, expectedType);
-  }
-
-
-  bool ServerIndex::LookupParent(std::string& target,
-                                 const std::string& publicId)
-  {
-    class Operations : public ReadOnlyOperationsT3<bool&, std::string&, const std::string&>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        ResourceType type;
-        int64_t id;
-        if (!transaction.LookupResource(id, type, tuple.get<2>()))
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-        else
-        {
-          int64_t parentId;
-          if (transaction.LookupParent(parentId, id))
-          {
-            tuple.get<1>() = transaction.GetPublicId(parentId);
-            tuple.get<0>() = true;
-          }
-          else
-          {
-            tuple.get<0>() = false;
-          }
-        }
-      }
-    };
-
-    bool found;
-    Operations operations;
-    operations.Apply(*this, found, target, publicId);
-    return found;
-  }
-
-
-  void ServerIndex::GetResourceStatistics(/* out */ ResourceType& type,
-                                          /* out */ uint64_t& diskSize, 
-                                          /* out */ uint64_t& uncompressedSize, 
-                                          /* out */ unsigned int& countStudies, 
-                                          /* out */ unsigned int& countSeries, 
-                                          /* out */ unsigned int& countInstances, 
-                                          /* out */ uint64_t& dicomDiskSize, 
-                                          /* out */ uint64_t& dicomUncompressedSize, 
-                                          const std::string& publicId)
-  {
-    class Operations : public IReadOnlyOperations
-    {
-    private:
-      ResourceType&      type_;
-      uint64_t&          diskSize_; 
-      uint64_t&          uncompressedSize_; 
-      unsigned int&      countStudies_; 
-      unsigned int&      countSeries_; 
-      unsigned int&      countInstances_; 
-      uint64_t&          dicomDiskSize_; 
-      uint64_t&          dicomUncompressedSize_; 
-      const std::string& publicId_;
-        
-    public:
-      explicit Operations(ResourceType& type,
-                          uint64_t& diskSize, 
-                          uint64_t& uncompressedSize, 
-                          unsigned int& countStudies, 
-                          unsigned int& countSeries, 
-                          unsigned int& countInstances, 
-                          uint64_t& dicomDiskSize, 
-                          uint64_t& dicomUncompressedSize, 
-                          const std::string& publicId) :
-        type_(type),
-        diskSize_(diskSize),
-        uncompressedSize_(uncompressedSize),
-        countStudies_(countStudies),
-        countSeries_(countSeries),
-        countInstances_(countInstances),
-        dicomDiskSize_(dicomDiskSize),
-        dicomUncompressedSize_(dicomUncompressedSize),
-        publicId_(publicId)
-      {
-      }
-      
-      virtual void Apply(ReadOnlyTransaction& transaction) ORTHANC_OVERRIDE
-      {
-        int64_t top;
-        if (!transaction.LookupResource(top, type_, publicId_))
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-        else
-        {
-          countInstances_ = 0;
-          countSeries_ = 0;
-          countStudies_ = 0;
-          diskSize_ = 0;
-          uncompressedSize_ = 0;
-          dicomDiskSize_ = 0;
-          dicomUncompressedSize_ = 0;
-
-          std::stack<int64_t> toExplore;
-          toExplore.push(top);
-
-          while (!toExplore.empty())
-          {
-            // Get the internal ID of the current resource
-            int64_t resource = toExplore.top();
-            toExplore.pop();
-
-            ResourceType thisType = transaction.GetResourceType(resource);
-
-            std::set<FileContentType> f;
-            transaction.ListAvailableAttachments(f, resource);
-
-            for (std::set<FileContentType>::const_iterator
-                   it = f.begin(); it != f.end(); ++it)
-            {
-              FileInfo attachment;
-              if (transaction.LookupAttachment(attachment, resource, *it))
-              {
-                if (attachment.GetContentType() == FileContentType_Dicom)
-                {
-                  dicomDiskSize_ += attachment.GetCompressedSize();
-                  dicomUncompressedSize_ += attachment.GetUncompressedSize();
-                }
-          
-                diskSize_ += attachment.GetCompressedSize();
-                uncompressedSize_ += attachment.GetUncompressedSize();
-              }
-            }
-
-            if (thisType == ResourceType_Instance)
-            {
-              countInstances_++;
-            }
-            else
-            {
-              switch (thisType)
-              {
-                case ResourceType_Study:
-                  countStudies_++;
-                  break;
-
-                case ResourceType_Series:
-                  countSeries_++;
-                  break;
-
-                default:
-                  break;
-              }
-
-              // Tag all the children of this resource as to be explored
-              std::list<int64_t> tmp;
-              transaction.GetChildrenInternalId(tmp, resource);
-              for (std::list<int64_t>::const_iterator 
-                     it = tmp.begin(); it != tmp.end(); ++it)
-              {
-                toExplore.push(*it);
-              }
-            }
-          }
-
-          if (countStudies_ == 0)
-          {
-            countStudies_ = 1;
-          }
-
-          if (countSeries_ == 0)
-          {
-            countSeries_ = 1;
-          }
-        }
-      }
-    };
-
-    Operations operations(type, diskSize, uncompressedSize, countStudies, countSeries,
-                          countInstances, dicomDiskSize, dicomUncompressedSize, publicId);
-    Apply(operations);
-  }
-
-
-  void ServerIndex::LookupIdentifierExact(std::vector<std::string>& result,
-                                          ResourceType level,
-                                          const DicomTag& tag,
-                                          const std::string& value)
-  {
-    assert((level == ResourceType_Patient && tag == DICOM_TAG_PATIENT_ID) ||
-           (level == ResourceType_Study && tag == DICOM_TAG_STUDY_INSTANCE_UID) ||
-           (level == ResourceType_Study && tag == DICOM_TAG_ACCESSION_NUMBER) ||
-           (level == ResourceType_Series && tag == DICOM_TAG_SERIES_INSTANCE_UID) ||
-           (level == ResourceType_Instance && tag == DICOM_TAG_SOP_INSTANCE_UID));
-    
-    result.clear();
-
-    DicomTagConstraint c(tag, ConstraintType_Equal, value, true, true);
-
-    std::vector<DatabaseConstraint> query;
-    query.push_back(c.ConvertToDatabaseConstraint(level, DicomTagType_Identifier));
-
-
-    class Operations : public IReadOnlyOperations
-    {
-    private:
-      std::vector<std::string>&               result_;
-      const std::vector<DatabaseConstraint>&  query_;
-      ResourceType                            level_;
-      
-    public:
-      Operations(std::vector<std::string>& result,
-                 const std::vector<DatabaseConstraint>& query,
-                 ResourceType level) :
-        result_(result),
-        query_(query),
-        level_(level)
-      {
-      }
-
-      virtual void Apply(ReadOnlyTransaction& transaction) ORTHANC_OVERRIDE
-      {
-        // TODO - CANDIDATE FOR "TransactionType_Implicit"
-        std::list<std::string> tmp;
-        transaction.ApplyLookupResources(tmp, NULL, query_, level_, 0);
-        CopyListToVector(result_, tmp);
-      }
-    };
-
-    Operations operations(result, query, level);
-    Apply(operations);
-  }
-
-
-  bool ServerIndex::LookupGlobalProperty(std::string& value,
-                                         GlobalProperty property)
-  {
-    class Operations : public ReadOnlyOperationsT3<bool&, std::string&, GlobalProperty>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        // TODO - CANDIDATE FOR "TransactionType_Implicit"
-        tuple.get<0>() = transaction.LookupGlobalProperty(tuple.get<1>(), tuple.get<2>());
-      }
-    };
-
-    bool found;
-    Operations operations;
-    operations.Apply(*this, found, value, property);
-    return found;
-  }
-  
-
-  std::string ServerIndex::GetGlobalProperty(GlobalProperty property,
-                                             const std::string& defaultValue)
-  {
-    std::string s;
-    if (LookupGlobalProperty(s, property))
-    {
-      return s;
-    }
-    else
-    {
-      return defaultValue;
-    }
-  }
-
-
-  bool ServerIndex::GetMainDicomTags(DicomMap& result,
-                                     const std::string& publicId,
-                                     ResourceType expectedType,
-                                     ResourceType levelOfInterest)
-  {
-    // Yes, the following test could be shortened, but we wish to make it as clear as possible
-    if (!(expectedType == ResourceType_Patient  && levelOfInterest == ResourceType_Patient) &&
-        !(expectedType == ResourceType_Study    && levelOfInterest == ResourceType_Patient) &&
-        !(expectedType == ResourceType_Study    && levelOfInterest == ResourceType_Study)   &&
-        !(expectedType == ResourceType_Series   && levelOfInterest == ResourceType_Series)  &&
-        !(expectedType == ResourceType_Instance && levelOfInterest == ResourceType_Instance))
-    {
-      throw OrthancException(ErrorCode_ParameterOutOfRange);
-    }
-
-
-    class Operations : public ReadOnlyOperationsT5<bool&, DicomMap&, const std::string&, ResourceType, ResourceType>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        // Lookup for the requested resource
-        int64_t id;
-        ResourceType type;
-        if (!transaction.LookupResource(id, type, tuple.get<2>()) ||
-            type != tuple.get<3>())
-        {
-          tuple.get<0>() = false;
-        }
-        else if (type == ResourceType_Study)
-        {
-          DicomMap tmp;
-          transaction.GetMainDicomTags(tmp, id);
-
-          switch (tuple.get<4>())
-          {
-            case ResourceType_Patient:
-              tmp.ExtractPatientInformation(tuple.get<1>());
-              tuple.get<0>() = true;
-              break;
-
-            case ResourceType_Study:
-              tmp.ExtractStudyInformation(tuple.get<1>());
-              tuple.get<0>() = true;
-              break;
-
-            default:
-              throw OrthancException(ErrorCode_InternalError);
-          }
-        }
-        else
-        {
-          transaction.GetMainDicomTags(tuple.get<1>(), id);
-          tuple.get<0>() = true;
-        }    
-      }
-    };
-
-    result.Clear();
-
-    bool found;
-    Operations operations;
-    operations.Apply(*this, found, result, publicId, expectedType, levelOfInterest);
-    return found;
-  }
-
-
-  bool ServerIndex::GetAllMainDicomTags(DicomMap& result,
-                                        const std::string& instancePublicId)
-  {
-    class Operations : public ReadOnlyOperationsT3<bool&, DicomMap&, const std::string&>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        // Lookup for the requested resource
-        int64_t instance;
-        ResourceType type;
-        if (!transaction.LookupResource(instance, type, tuple.get<2>()) ||
-            type != ResourceType_Instance)
-        {
-          tuple.get<0>() =  false;
-        }
-        else
-        {
-          DicomMap tmp;
-
-          transaction.GetMainDicomTags(tmp, instance);
-          tuple.get<1>().Merge(tmp);
-
-          int64_t series;
-          if (!transaction.LookupParent(series, instance))
-          {
-            throw OrthancException(ErrorCode_InternalError);
-          }
-
-          tmp.Clear();
-          transaction.GetMainDicomTags(tmp, series);
-          tuple.get<1>().Merge(tmp);
-
-          int64_t study;
-          if (!transaction.LookupParent(study, series))
-          {
-            throw OrthancException(ErrorCode_InternalError);
-          }
-
-          tmp.Clear();
-          transaction.GetMainDicomTags(tmp, study);
-          tuple.get<1>().Merge(tmp);
-
-#ifndef NDEBUG
-          {
-            // Sanity test to check that all the main DICOM tags from the
-            // patient level are copied at the study level
-        
-            int64_t patient;
-            if (!transaction.LookupParent(patient, study))
-            {
-              throw OrthancException(ErrorCode_InternalError);
-            }
-
-            tmp.Clear();
-            transaction.GetMainDicomTags(tmp, study);
-
-            std::set<DicomTag> patientTags;
-            tmp.GetTags(patientTags);
-
-            for (std::set<DicomTag>::const_iterator
-                   it = patientTags.begin(); it != patientTags.end(); ++it)
-            {
-              assert(tuple.get<1>().HasTag(*it));
-            }
-          }
-#endif
-      
-          tuple.get<0>() =  true;
-        }
-      }
-    };
-
-    result.Clear();
-    
-    bool found;
-    Operations operations;
-    operations.Apply(*this, found, result, instancePublicId);
-    return found;
-  }
-
-
-  bool ServerIndex::LookupResourceType(ResourceType& type,
-                                       const std::string& publicId)
-  {
-    class Operations : public ReadOnlyOperationsT3<bool&, ResourceType&, const std::string&>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        // TODO - CANDIDATE FOR "TransactionType_Implicit"
-        int64_t id;
-        tuple.get<0>() = transaction.LookupResource(id, tuple.get<1>(), tuple.get<2>());
-      }
-    };
-
-    bool found;
-    Operations operations;
-    operations.Apply(*this, found, type, publicId);
-    return found;
-  }
-
-
-  bool ServerIndex::LookupParent(std::string& target,
-                                 const std::string& publicId,
-                                 ResourceType parentType)
-  {
-    class Operations : public ReadOnlyOperationsT4<bool&, std::string&, const std::string&, ResourceType>
-    {
-    public:
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        ResourceType type;
-        int64_t id;
-        if (!transaction.LookupResource(id, type, tuple.get<2>()))
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-
-        while (type != tuple.get<3>())
-        {
-          int64_t parentId;
-
-          if (type == ResourceType_Patient ||    // Cannot further go up in hierarchy
-              !transaction.LookupParent(parentId, id))
-          {
-            tuple.get<0>() = false;
-            return;
-          }
-
-          id = parentId;
-          type = GetParentResourceType(type);
-        }
-
-        tuple.get<0>() = true;
-        tuple.get<1>() = transaction.GetPublicId(id);
-      }
-    };
-
-    bool found;
-    Operations operations;
-    operations.Apply(*this, found, target, publicId, parentType);
-    return found;
-  }
-
-
-  void ServerIndex::ApplyLookupResources(std::vector<std::string>& resourcesId,
-                                         std::vector<std::string>* instancesId,
-                                         const DatabaseLookup& lookup,
-                                         ResourceType queryLevel,
-                                         size_t limit)
-  {
-    class Operations : public ReadOnlyOperationsT4<bool, const std::vector<DatabaseConstraint>&, ResourceType, size_t>
-    {
-    private:
-      std::list<std::string>  resourcesList_;
-      std::list<std::string>  instancesList_;
-      
-    public:
-      const std::list<std::string>& GetResourcesList() const
-      {
-        return resourcesList_;
-      }
-
-      const std::list<std::string>& GetInstancesList() const
-      {
-        return instancesList_;
-      }
-
-      virtual void ApplyTuple(ReadOnlyTransaction& transaction,
-                              const Tuple& tuple) ORTHANC_OVERRIDE
-      {
-        // TODO - CANDIDATE FOR "TransactionType_Implicit"
-        if (tuple.get<0>())
-        {
-          transaction.ApplyLookupResources(resourcesList_, &instancesList_, tuple.get<1>(), tuple.get<2>(), tuple.get<3>());
-        }
-        else
-        {
-          transaction.ApplyLookupResources(resourcesList_, NULL, tuple.get<1>(), tuple.get<2>(), tuple.get<3>());
-        }
-      }
-    };
-
-
-    std::vector<DatabaseConstraint> normalized;
-    NormalizeLookup(normalized, lookup, queryLevel);
-
-    Operations operations;
-    operations.Apply(*this, (instancesId != NULL), normalized, queryLevel, limit);
-    
-    CopyListToVector(resourcesId, operations.GetResourcesList());
-
-    if (instancesId != NULL)
-    { 
-      CopyListToVector(*instancesId, operations.GetInstancesList());
-    }
-  }
-
-
-  bool ServerIndex::DeleteResource(Json::Value& target,
-                                   const std::string& uuid,
-                                   ResourceType expectedType)
-  {
-    class Operations : public IReadWriteOperations
-    {
-    private:
-      bool                found_;
-      Json::Value&        target_;
-      const std::string&  uuid_;
-      ResourceType        expectedType_;
-      
-    public:
-      Operations(Json::Value& target,
-                 const std::string& uuid,
-                 ResourceType expectedType) :
-        found_(false),
-        target_(target),
-        uuid_(uuid),
-        expectedType_(expectedType)
-      {
-      }
-
-      bool IsFound() const
-      {
-        return found_;
-      }
-
-      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
-      {
-        int64_t id;
-        ResourceType type;
-        if (!transaction.LookupResource(id, type, uuid_) ||
-            expectedType_ != type)
-        {
-          found_ = false;
-        }
-        else
-        {
-          found_ = true;
-          transaction.DeleteResource(id);
-
-          std::string remainingPublicId;
-          ResourceType remainingLevel;
-          if (transaction.GetTransactionContext().LookupRemainingLevel(remainingPublicId, remainingLevel))
-          {
-            target_["RemainingAncestor"] = Json::Value(Json::objectValue);
-            target_["RemainingAncestor"]["Path"] = GetBasePath(remainingLevel, remainingPublicId);
-            target_["RemainingAncestor"]["Type"] = EnumerationToString(remainingLevel);
-            target_["RemainingAncestor"]["ID"] = remainingPublicId;
-          }
-          else
-          {
-            target_["RemainingAncestor"] = Json::nullValue;
-          }
-        }
-      }
-    };
-
-    Operations operations(target, uuid, expectedType);
-    Apply(operations);
-    return operations.IsFound();
-  }
-
-
-  void ServerIndex::LogExportedResource(const std::string& publicId,
-                                        const std::string& remoteModality)
-  {
-    class Operations : public IReadWriteOperations
-    {
-    private:
-      const std::string&  publicId_;
-      const std::string&  remoteModality_;
-
-    public:
-      Operations(const std::string& publicId,
-                 const std::string& remoteModality) :
-        publicId_(publicId),
-        remoteModality_(remoteModality)
-      {
-      }
-      
-      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
-      {
-        int64_t id;
-        ResourceType type;
-        if (!transaction.LookupResource(id, type, publicId_))
-        {
-          throw OrthancException(ErrorCode_InexistentItem);
-        }
-
-        std::string patientId;
-        std::string studyInstanceUid;
-        std::string seriesInstanceUid;
-        std::string sopInstanceUid;
-
-        int64_t currentId = id;
-        ResourceType currentType = type;
-
-        // Iteratively go up inside the patient/study/series/instance hierarchy
-        bool done = false;
-        while (!done)
-        {
-          DicomMap map;
-          transaction.GetMainDicomTags(map, currentId);
-
-          switch (currentType)
-          {
-            case ResourceType_Patient:
-              if (map.HasTag(DICOM_TAG_PATIENT_ID))
-              {
-                patientId = map.GetValue(DICOM_TAG_PATIENT_ID).GetContent();
-              }
-              done = true;
-              break;
-
-            case ResourceType_Study:
-              if (map.HasTag(DICOM_TAG_STUDY_INSTANCE_UID))
-              {
-                studyInstanceUid = map.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).GetContent();
-              }
-              currentType = ResourceType_Patient;
-              break;
-
-            case ResourceType_Series:
-              if (map.HasTag(DICOM_TAG_SERIES_INSTANCE_UID))
-              {
-                seriesInstanceUid = map.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).GetContent();
-              }
-              currentType = ResourceType_Study;
-              break;
-
-            case ResourceType_Instance:
-              if (map.HasTag(DICOM_TAG_SOP_INSTANCE_UID))
-              {
-                sopInstanceUid = map.GetValue(DICOM_TAG_SOP_INSTANCE_UID).GetContent();
-              }
-              currentType = ResourceType_Series;
-              break;
-
-            default:
-              throw OrthancException(ErrorCode_InternalError);
-          }
-
-          // If we have not reached the Patient level, find the parent of
-          // the current resource
-          if (!done)
-          {
-            bool ok = transaction.LookupParent(currentId, currentId);
-            (void) ok;  // Remove warning about unused variable in release builds
-            assert(ok);
-          }
-        }
-
-        ExportedResource resource(-1, 
-                                  type,
-                                  publicId_,
-                                  remoteModality_,
-                                  SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */),
-                                  patientId,
-                                  studyInstanceUid,
-                                  seriesInstanceUid,
-                                  sopInstanceUid);
-
-        transaction.LogExportedResource(resource);
-      }
-    };
-
-    Operations operations(publicId, remoteModality);
-    Apply(operations);
-  }
-
-
-  void ServerIndex::SetProtectedPatient(const std::string& publicId,
-                                        bool isProtected)
-  {
-    class Operations : public IReadWriteOperations
-    {
-    private:
-      const std::string&  publicId_;
-      bool                isProtected_;
-
-    public:
-      Operations(const std::string& publicId,
-                 bool isProtected) :
-        publicId_(publicId),
-        isProtected_(isProtected)
-      {
-      }
-
-      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
-      {
-        // Lookup for the requested resource
-        int64_t id;
-        ResourceType type;
-        if (!transaction.LookupResource(id, type, publicId_) ||
-            type != ResourceType_Patient)
-        {
-          throw OrthancException(ErrorCode_ParameterOutOfRange);
-        }
-        else
-        {
-          transaction.SetProtectedPatient(id, isProtected_);
-        }
-      }
-    };
-
-    Operations operations(publicId, isProtected);
-    Apply(operations);
-
-    if (isProtected)
-    {
-      LOG(INFO) << "Patient " << publicId << " has been protected";
-    }
-    else
-    {
-      LOG(INFO) << "Patient " << publicId << " has been unprotected";
-    }
-  }
-
-
-  void ServerIndex::SetMetadata(const std::string& publicId,
-                                MetadataType type,
-                                const std::string& value)
-  {
-    class Operations : public IReadWriteOperations
-    {
-    private:
-      const std::string&  publicId_;
-      MetadataType        type_;
-      const std::string&  value_;
-
-    public:
-      Operations(const std::string& publicId,
-                 MetadataType type,
-                 const std::string& value) :
-        publicId_(publicId),
-        type_(type),
-        value_(value)
-      {
-      }
-
-      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
-      {
-        ResourceType rtype;
-        int64_t id;
-        if (!transaction.LookupResource(id, rtype, publicId_))
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-        else
-        {
-          transaction.SetMetadata(id, type_, value_);
-
-          if (IsUserMetadata(type_))
-          {
-            transaction.LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId_);
-          }
-        }
-      }
-    };
-
-    Operations operations(publicId, type, value);
-    Apply(operations);
-  }
-
-
-  void ServerIndex::DeleteMetadata(const std::string& publicId,
-                                   MetadataType type)
-  {
-    class Operations : public IReadWriteOperations
-    {
-    private:
-      const std::string&  publicId_;
-      MetadataType        type_;
-
-    public:
-      Operations(const std::string& publicId,
-                 MetadataType type) :
-        publicId_(publicId),
-        type_(type)
-      {
-      }
-
-      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
-      {
-        ResourceType rtype;
-        int64_t id;
-        if (!transaction.LookupResource(id, rtype, publicId_))
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-        else
-        {
-          transaction.DeleteMetadata(id, type_);
-
-          if (IsUserMetadata(type_))
-          {
-            transaction.LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId_);
-          }
-        }
-      }
-    };
-
-    Operations operations(publicId, type);
-    Apply(operations);
-  }
-
-
-  uint64_t ServerIndex::IncrementGlobalSequence(GlobalProperty sequence)
-  {
-    class Operations : public IReadWriteOperations
-    {
-    private:
-      uint64_t       newValue_;
-      GlobalProperty sequence_;
-
-    public:
-      explicit Operations(GlobalProperty sequence) :
-        newValue_(0),  // Dummy initialization
-        sequence_(sequence)
-      {
-      }
-
-      uint64_t GetNewValue() const
-      {
-        return newValue_;
-      }
-
-      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
-      {
-        std::string oldString;
-
-        if (transaction.LookupGlobalProperty(oldString, sequence_))
-        {
-          uint64_t oldValue;
-      
-          try
-          {
-            oldValue = boost::lexical_cast<uint64_t>(oldString);
-          }
-          catch (boost::bad_lexical_cast&)
-          {
-            LOG(ERROR) << "Cannot read the global sequence "
-                       << boost::lexical_cast<std::string>(sequence_) << ", resetting it";
-            oldValue = 0;
-          }
-
-          newValue_ = oldValue + 1;
-        }
-        else
-        {
-          // Initialize the sequence at "1"
-          newValue_ = 1;
-        }
-
-        transaction.SetGlobalProperty(sequence_, boost::lexical_cast<std::string>(newValue_));
-      }
-    };
-
-    Operations operations(sequence);
-    Apply(operations);
-    assert(operations.GetNewValue() != 0);
-    return operations.GetNewValue();
-  }
-
-
-  void ServerIndex::DeleteChanges()
-  {
-    class Operations : public IReadWriteOperations
-    {
-    public:
-      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
-      {
-        transaction.ClearChanges();
-      }
-    };
-
-    Operations operations;
-    Apply(operations);
-  }
-
-  
-  void ServerIndex::DeleteExportedResources()
-  {
-    class Operations : public IReadWriteOperations
-    {
-    public:
-      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
-      {
-        transaction.ClearExportedResources();
-      }
-    };
-
-    Operations operations;
-    Apply(operations);
-  }
-
-
-  void ServerIndex::SetGlobalProperty(GlobalProperty property,
-                                      const std::string& value)
-  {
-    class Operations : public IReadWriteOperations
-    {
-    private:
-      GlobalProperty      property_;
-      const std::string&  value_;
-      
-    public:
-      Operations(GlobalProperty property,
-                 const std::string& value) :
-        property_(property),
-        value_(value)
-      {
-      }
-        
-      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
-      {
-        transaction.SetGlobalProperty(property_, value_);
-      }
-    };
-
-    Operations operations(property, value);
-    Apply(operations);
-  }
-
-
-  void ServerIndex::DeleteAttachment(const std::string& publicId,
-                                     FileContentType type)
-  {
-    class Operations : public IReadWriteOperations
-    {
-    private:
-      const std::string&  publicId_;
-      FileContentType     type_;
-      
-    public:
-      Operations(const std::string& publicId,
-                 FileContentType type) :
-        publicId_(publicId),
-        type_(type)
-      {
-      }
-        
-      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
-      {
-        ResourceType rtype;
-        int64_t id;
-        if (!transaction.LookupResource(id, rtype, publicId_))
-        {
-          throw OrthancException(ErrorCode_UnknownResource);
-        }
-        else
-        {
-          transaction.DeleteAttachment(id, type_);
-          
-          if (IsUserContentType(type_))
-          {
-            transaction.LogChange(id, ChangeType_UpdatedAttachment, rtype, publicId_);
-          }
-        }
-      }
-    };
-
-    Operations operations(publicId, type);
-    Apply(operations);
-  }
-
-
-  void ServerIndex::LogChange(ChangeType changeType,
-                              const std::string& publicId,
-                              ResourceType level)
-  {
-    class Operations : public IReadWriteOperations
-    {
-    private:
-      ChangeType          changeType_;
-      const std::string&  publicId_;
-      ResourceType        level_;
-      
-    public:
-      Operations(ChangeType changeType,
-                 const std::string& publicId,
-                 ResourceType level) :
-        changeType_(changeType),
-        publicId_(publicId),
-        level_(level)
-      {
-      }
-        
-      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
-      {
-        int64_t id;
-        ResourceType type;
-        if (transaction.LookupResource(id, type, publicId_))
-        {
-          // Make sure that the resource is still existing. Ignore if
-          // the resource has been deleted, because this function
-          // might e.g. be called from
-          // "ServerIndex::UnstableResourcesMonitorThread()" (for
-          // which a deleted resource not an error case)
-          if (type == level_)
-          {
-            transaction.LogChange(id, changeType_, type, publicId_);
-          }
-          else
-          {
-            // Consistency check
-            throw OrthancException(ErrorCode_UnknownResource);
-          }
-        }
-      }
-    };
-
-    Operations operations(changeType, publicId, level);
-    Apply(operations);
-  }
-
-
-  void ServerIndex::ReconstructInstance(const ParsedDicomFile& dicom)
-  {
-    class Operations : public IReadWriteOperations
-    {
-    private:
-      DicomMap                              summary_;
-      std::unique_ptr<DicomInstanceHasher>  hasher_;
-      bool                                  hasTransferSyntax_;
-      DicomTransferSyntax                   transferSyntax_;
-      
-    public:
-      Operations(const ParsedDicomFile& dicom)
-      {
-        OrthancConfiguration::DefaultExtractDicomSummary(summary_, dicom);
-        hasher_.reset(new DicomInstanceHasher(summary_));
-        hasTransferSyntax_ = dicom.LookupTransferSyntax(transferSyntax_);
-      }
-        
-      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
-      {
-        int64_t patient = -1, study = -1, series = -1, instance = -1;
-
-        ResourceType type1, type2, type3, type4;      
-        if (!transaction.LookupResource(patient, type1, hasher_->HashPatient()) ||
-            !transaction.LookupResource(study, type2, hasher_->HashStudy()) ||
-            !transaction.LookupResource(series, type3, hasher_->HashSeries()) ||
-            !transaction.LookupResource(instance, type4, hasher_->HashInstance()) ||
-            type1 != ResourceType_Patient ||
-            type2 != ResourceType_Study ||
-            type3 != ResourceType_Series ||
-            type4 != ResourceType_Instance ||
-            patient == -1 ||
-            study == -1 ||
-            series == -1 ||
-            instance == -1)
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-
-        transaction.ClearMainDicomTags(patient);
-        transaction.ClearMainDicomTags(study);
-        transaction.ClearMainDicomTags(series);
-        transaction.ClearMainDicomTags(instance);
-
-        {
-          ResourcesContent content;
-          content.AddResource(patient, ResourceType_Patient, summary_);
-          content.AddResource(study, ResourceType_Study, summary_);
-          content.AddResource(series, ResourceType_Series, summary_);
-          content.AddResource(instance, ResourceType_Instance, summary_);
-          transaction.SetResourcesContent(content);
-        }
-
-        if (hasTransferSyntax_)
-        {
-          transaction.SetMetadata(instance, MetadataType_Instance_TransferSyntax, GetTransferSyntaxUid(transferSyntax_));
-        }
-
-        const DicomValue* value;
-        if ((value = summary_.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
-            !value->IsNull() &&
-            !value->IsBinary())
-        {
-          transaction.SetMetadata(instance, MetadataType_Instance_SopClassUid, value->GetContent());
-        }
-      }
-    };
-
-    Operations operations(dicom);
-    Apply(operations);
-  }
-
-
-  static bool IsRecyclingNeeded(IDatabaseWrapper& db,
-                                uint64_t maximumStorageSize,
-                                unsigned int maximumPatients,
-                                uint64_t addedInstanceSize)
-  {
-    if (maximumStorageSize != 0)
-    {
-      if (maximumStorageSize < addedInstanceSize)
-      {
-        throw OrthancException(ErrorCode_FullStorage, "Cannot store an instance of size " +
-                               boost::lexical_cast<std::string>(addedInstanceSize) +
-                               " bytes in a storage area limited to " +
-                               boost::lexical_cast<std::string>(maximumStorageSize));
-      }
-      
-      if (db.IsDiskSizeAbove(maximumStorageSize - addedInstanceSize))
-      {
-        return true;
-      }
-    }
-
-    if (maximumPatients != 0)
-    {
-      uint64_t patientCount = db.GetResourceCount(ResourceType_Patient);
-      if (patientCount > maximumPatients)
-      {
-        return true;
-      }
-    }
-
-    return false;
-  }
-  
-
-  void ServerIndex::ReadWriteTransaction::Recycle(uint64_t maximumStorageSize,
-                                                  unsigned int maximumPatients,
-                                                  uint64_t addedInstanceSize,
-                                                  const std::string& newPatientId)
-  {
-    // TODO - Performance: Avoid calls to "IsRecyclingNeeded()"
-    
-    if (IsRecyclingNeeded(db_, maximumStorageSize, maximumPatients, addedInstanceSize))
-    {
-      // Check whether other DICOM instances from this patient are
-      // already stored
-      int64_t patientToAvoid;
-      bool hasPatientToAvoid;
-
-      if (newPatientId.empty())
-      {
-        hasPatientToAvoid = false;
-      }
-      else
-      {
-        ResourceType type;
-        hasPatientToAvoid = db_.LookupResource(patientToAvoid, type, newPatientId);
-        if (type != ResourceType_Patient)
-        {
-          throw OrthancException(ErrorCode_InternalError);
-        }
-      }
-
-      // Iteratively select patient to remove until there is enough
-      // space in the DICOM store
-      int64_t patientToRecycle;
-      while (true)
-      {
-        // If other instances of this patient are already in the store,
-        // we must avoid to recycle them
-        bool ok = (hasPatientToAvoid ?
-                   db_.SelectPatientToRecycle(patientToRecycle, patientToAvoid) :
-                   db_.SelectPatientToRecycle(patientToRecycle));
-        
-        if (!ok)
-        {
-          throw OrthancException(ErrorCode_FullStorage);
-        }
-      
-        LOG(TRACE) << "Recycling one patient";
-        db_.DeleteResource(patientToRecycle);
-
-        if (!IsRecyclingNeeded(db_, maximumStorageSize, maximumPatients, addedInstanceSize))
-        {
-          // OK, we're done
-          return;
-        }
-      }
-    }
-  }
-
-
-  void ServerIndex::StandaloneRecycling(uint64_t maximumStorageSize,
-                                        unsigned int maximumPatientCount)
-  {
-    class Operations : public IReadWriteOperations
-    {
-    private:
-      uint64_t      maximumStorageSize_;
-      unsigned int  maximumPatientCount_;
-      
-    public:
-      Operations(uint64_t maximumStorageSize,
-                 unsigned int maximumPatientCount) :
-        maximumStorageSize_(maximumStorageSize),
-        maximumPatientCount_(maximumPatientCount)
-      {
-      }
-        
-      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
-      {
-        transaction.Recycle(maximumStorageSize_, maximumPatientCount_, 0, "");
-      }
-    };
-
-    Operations operations(maximumStorageSize, maximumPatientCount);
-    Apply(operations);
-  }
-
-
   StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata,
                                  const DicomMap& dicomSummary,
-                                 const Attachments& attachments,
-                                 const MetadataMap& metadata,
+                                 const ServerIndex::Attachments& attachments,
+                                 const ServerIndex::MetadataMap& metadata,
                                  const DicomInstanceOrigin& origin,
                                  bool overwrite,
                                  bool hasTransferSyntax,
@@ -3201,404 +735,6 @@
                                  bool hasPixelDataOffset,
                                  uint64_t pixelDataOffset)
   {
-    class Operations : public IReadWriteOperations
-    {
-    private:
-      StoreStatus                          storeStatus_;
-      std::map<MetadataType, std::string>& instanceMetadata_;
-      const DicomMap&                      dicomSummary_;
-      const Attachments&                   attachments_;
-      const MetadataMap&                   metadata_;
-      const DicomInstanceOrigin&           origin_;
-      bool                                 overwrite_;
-      bool                                 hasTransferSyntax_;
-      DicomTransferSyntax                  transferSyntax_;
-      bool                                 hasPixelDataOffset_;
-      uint64_t                             pixelDataOffset_;
-      uint64_t                             maximumStorageSize_;
-      unsigned int                         maximumPatientCount_;
-
-      // Auto-computed fields
-      bool          hasExpectedInstances_;
-      int64_t       expectedInstances_;
-      std::string   hashPatient_;
-      std::string   hashStudy_;
-      std::string   hashSeries_;
-      std::string   hashInstance_;
-
-      
-      static void SetInstanceMetadata(ResourcesContent& content,
-                                      std::map<MetadataType, std::string>& instanceMetadata,
-                                      int64_t instance,
-                                      MetadataType metadata,
-                                      const std::string& value)
-      {
-        content.AddMetadata(instance, metadata, value);
-        instanceMetadata[metadata] = value;
-      }
-
-      
-      static bool ComputeExpectedNumberOfInstances(int64_t& target,
-                                                   const DicomMap& dicomSummary)
-      {
-        try
-        {
-          const DicomValue* value;
-          const DicomValue* value2;
-          
-          if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_IMAGES_IN_ACQUISITION)) != NULL &&
-              !value->IsNull() &&
-              !value->IsBinary() &&
-              (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS)) != NULL &&
-              !value2->IsNull() &&
-              !value2->IsBinary())
-          {
-            // Patch for series with temporal positions thanks to Will Ryder
-            int64_t imagesInAcquisition = boost::lexical_cast<int64_t>(value->GetContent());
-            int64_t countTemporalPositions = boost::lexical_cast<int64_t>(value2->GetContent());
-            target = imagesInAcquisition * countTemporalPositions;
-            return (target > 0);
-          }
-
-          else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_SLICES)) != NULL &&
-                   !value->IsNull() &&
-                   !value->IsBinary() &&
-                   (value2 = dicomSummary.TestAndGetValue(DICOM_TAG_NUMBER_OF_TIME_SLICES)) != NULL &&
-                   !value2->IsBinary() &&
-                   !value2->IsNull())
-          {
-            // Support of Cardio-PET images
-            int64_t numberOfSlices = boost::lexical_cast<int64_t>(value->GetContent());
-            int64_t numberOfTimeSlices = boost::lexical_cast<int64_t>(value2->GetContent());
-            target = numberOfSlices * numberOfTimeSlices;
-            return (target > 0);
-          }
-
-          else if ((value = dicomSummary.TestAndGetValue(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES)) != NULL &&
-                   !value->IsNull() &&
-                   !value->IsBinary())
-          {
-            target = boost::lexical_cast<int64_t>(value->GetContent());
-            return (target > 0);
-          }
-        }
-        catch (OrthancException&)
-        {
-        }
-        catch (boost::bad_lexical_cast&)
-        {
-        }
-
-        return false;
-      }
-
-    public:
-      Operations(std::map<MetadataType, std::string>& instanceMetadata,
-                 const DicomMap& dicomSummary,
-                 const Attachments& attachments,
-                 const MetadataMap& metadata,
-                 const DicomInstanceOrigin& origin,
-                 bool overwrite,
-                 bool hasTransferSyntax,
-                 DicomTransferSyntax transferSyntax,
-                 bool hasPixelDataOffset,
-                 uint64_t pixelDataOffset,
-                 uint64_t maximumStorageSize,
-                 unsigned int maximumPatientCount) :
-        storeStatus_(StoreStatus_Failure),
-        instanceMetadata_(instanceMetadata),
-        dicomSummary_(dicomSummary),
-        attachments_(attachments),
-        metadata_(metadata),
-        origin_(origin),
-        overwrite_(overwrite),
-        hasTransferSyntax_(hasTransferSyntax),
-        transferSyntax_(transferSyntax),
-        hasPixelDataOffset_(hasPixelDataOffset),
-        pixelDataOffset_(pixelDataOffset),
-        maximumStorageSize_(maximumStorageSize),
-        maximumPatientCount_(maximumPatientCount)
-      {
-        hasExpectedInstances_ = ComputeExpectedNumberOfInstances(expectedInstances_, dicomSummary);
-    
-        instanceMetadata_.clear();
-
-        DicomInstanceHasher hasher(dicomSummary);
-        hashPatient_ = hasher.HashPatient();
-        hashStudy_ = hasher.HashStudy();
-        hashSeries_ = hasher.HashSeries();
-        hashInstance_ = hasher.HashInstance();
-      }
-
-      StoreStatus GetStoreStatus() const
-      {
-        return storeStatus_;
-      }
-        
-      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
-      {
-        try
-        {
-          IDatabaseWrapper::CreateInstanceResult status;
-          int64_t instanceId;
-
-          // Check whether this instance is already stored
-          if (!transaction.CreateInstance(status, instanceId, hashPatient_,
-                                          hashStudy_, hashSeries_, hashInstance_))
-          {
-            // The instance already exists
-        
-            if (overwrite_)
-            {
-              // Overwrite the old instance
-              LOG(INFO) << "Overwriting instance: " << hashInstance_;
-              transaction.DeleteResource(instanceId);
-
-              // Re-create the instance, now that the old one is removed
-              if (!transaction.CreateInstance(status, instanceId, hashPatient_,
-                                              hashStudy_, hashSeries_, hashInstance_))
-              {
-                throw OrthancException(ErrorCode_InternalError);
-              }
-            }
-            else
-            {
-              // Do nothing if the instance already exists and overwriting is disabled
-              transaction.GetAllMetadata(instanceMetadata_, instanceId);
-              storeStatus_ = StoreStatus_AlreadyStored;
-              return;
-            }
-          }
-
-
-          // Warn about the creation of new resources. The order must be
-          // from instance to patient.
-
-          // NB: In theory, could be sped up by grouping the underlying
-          // calls to "transaction.LogChange()". However, this would only have an
-          // impact when new patient/study/series get created, which
-          // occurs far less often that creating new instances. The
-          // positive impact looks marginal in practice.
-          transaction.LogChange(instanceId, ChangeType_NewInstance, ResourceType_Instance, hashInstance_);
-
-          if (status.isNewSeries_)
-          {
-            transaction.LogChange(status.seriesId_, ChangeType_NewSeries, ResourceType_Series, hashSeries_);
-          }
-      
-          if (status.isNewStudy_)
-          {
-            transaction.LogChange(status.studyId_, ChangeType_NewStudy, ResourceType_Study, hashStudy_);
-          }
-      
-          if (status.isNewPatient_)
-          {
-            transaction.LogChange(status.patientId_, ChangeType_NewPatient, ResourceType_Patient, hashPatient_);
-          }
-      
-      
-          // Ensure there is enough room in the storage for the new instance
-          uint64_t instanceSize = 0;
-          for (Attachments::const_iterator it = attachments_.begin();
-               it != attachments_.end(); ++it)
-          {
-            instanceSize += it->GetCompressedSize();
-          }
-
-          transaction.Recycle(maximumStorageSize_, maximumPatientCount_,
-                              instanceSize, hashPatient_ /* don't consider the current patient for recycling */);
-      
-     
-          // Attach the files to the newly created instance
-          for (Attachments::const_iterator it = attachments_.begin();
-               it != attachments_.end(); ++it)
-          {
-            transaction.AddAttachment(instanceId, *it);
-          }
-
-      
-          {
-            ResourcesContent content;
-      
-            // Populate the tags of the newly-created resources
-
-            content.AddResource(instanceId, ResourceType_Instance, dicomSummary_);
-
-            if (status.isNewSeries_)
-            {
-              content.AddResource(status.seriesId_, ResourceType_Series, dicomSummary_);
-            }
-
-            if (status.isNewStudy_)
-            {
-              content.AddResource(status.studyId_, ResourceType_Study, dicomSummary_);
-            }
-
-            if (status.isNewPatient_)
-            {
-              content.AddResource(status.patientId_, ResourceType_Patient, dicomSummary_);
-            }
-
-
-            // Attach the user-specified metadata
-
-            for (MetadataMap::const_iterator 
-                   it = metadata_.begin(); it != metadata_.end(); ++it)
-            {
-              switch (it->first.first)
-              {
-                case ResourceType_Patient:
-                  content.AddMetadata(status.patientId_, it->first.second, it->second);
-                  break;
-
-                case ResourceType_Study:
-                  content.AddMetadata(status.studyId_, it->first.second, it->second);
-                  break;
-
-                case ResourceType_Series:
-                  content.AddMetadata(status.seriesId_, it->first.second, it->second);
-                  break;
-
-                case ResourceType_Instance:
-                  SetInstanceMetadata(content, instanceMetadata_, instanceId,
-                                      it->first.second, it->second);
-                  break;
-
-                default:
-                  throw OrthancException(ErrorCode_ParameterOutOfRange);
-              }
-            }
-
-        
-            // Attach the auto-computed metadata for the patient/study/series levels
-            std::string now = SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */);
-            content.AddMetadata(status.seriesId_, MetadataType_LastUpdate, now);
-            content.AddMetadata(status.studyId_, MetadataType_LastUpdate, now);
-            content.AddMetadata(status.patientId_, MetadataType_LastUpdate, now);
-
-            if (status.isNewSeries_)
-            {
-              if (hasExpectedInstances_)
-              {
-                content.AddMetadata(status.seriesId_, MetadataType_Series_ExpectedNumberOfInstances,
-                                    boost::lexical_cast<std::string>(expectedInstances_));
-              }
-
-              // New in Orthanc 1.9.0
-              content.AddMetadata(status.seriesId_, MetadataType_RemoteAet,
-                                  origin_.GetRemoteAetC());
-            }
-
-        
-            // Attach the auto-computed metadata for the instance level,
-            // reflecting these additions into the input metadata map
-            SetInstanceMetadata(content, instanceMetadata_, instanceId,
-                                MetadataType_Instance_ReceptionDate, now);
-            SetInstanceMetadata(content, instanceMetadata_, instanceId, MetadataType_RemoteAet,
-                                origin_.GetRemoteAetC());
-            SetInstanceMetadata(content, instanceMetadata_, instanceId, MetadataType_Instance_Origin, 
-                                EnumerationToString(origin_.GetRequestOrigin()));
-
-
-            if (hasTransferSyntax_)
-            {
-              // New in Orthanc 1.2.0
-              SetInstanceMetadata(content, instanceMetadata_, instanceId,
-                                  MetadataType_Instance_TransferSyntax,
-                                  GetTransferSyntaxUid(transferSyntax_));
-            }
-
-            {
-              std::string s;
-
-              if (origin_.LookupRemoteIp(s))
-              {
-                // New in Orthanc 1.4.0
-                SetInstanceMetadata(content, instanceMetadata_, instanceId,
-                                    MetadataType_Instance_RemoteIp, s);
-              }
-
-              if (origin_.LookupCalledAet(s))
-              {
-                // New in Orthanc 1.4.0
-                SetInstanceMetadata(content, instanceMetadata_, instanceId,
-                                    MetadataType_Instance_CalledAet, s);
-              }
-
-              if (origin_.LookupHttpUsername(s))
-              {
-                // New in Orthanc 1.4.0
-                SetInstanceMetadata(content, instanceMetadata_, instanceId,
-                                    MetadataType_Instance_HttpUsername, s);
-              }
-            }
-
-            if (hasPixelDataOffset_)
-            {
-              // New in Orthanc 1.9.1
-              SetInstanceMetadata(content, instanceMetadata_, instanceId,
-                                  MetadataType_Instance_PixelDataOffset,
-                                  boost::lexical_cast<std::string>(pixelDataOffset_));
-            }
-        
-            const DicomValue* value;
-            if ((value = dicomSummary_.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
-                !value->IsNull() &&
-                !value->IsBinary())
-            {
-              SetInstanceMetadata(content, instanceMetadata_, instanceId,
-                                  MetadataType_Instance_SopClassUid, value->GetContent());
-            }
-
-
-            if ((value = dicomSummary_.TestAndGetValue(DICOM_TAG_INSTANCE_NUMBER)) != NULL ||
-                (value = dicomSummary_.TestAndGetValue(DICOM_TAG_IMAGE_INDEX)) != NULL)
-            {
-              if (!value->IsNull() && 
-                  !value->IsBinary())
-              {
-                SetInstanceMetadata(content, instanceMetadata_, instanceId,
-                                    MetadataType_Instance_IndexInSeries, Toolbox::StripSpaces(value->GetContent()));
-              }
-            }
-
-        
-            transaction.SetResourcesContent(content);
-          }
-
-  
-          // Check whether the series of this new instance is now completed
-          int64_t expectedNumberOfInstances;
-          if (ComputeExpectedNumberOfInstances(expectedNumberOfInstances, dicomSummary_))
-          {
-            SeriesStatus seriesStatus = transaction.GetSeriesStatus(status.seriesId_, expectedNumberOfInstances);
-            if (seriesStatus == SeriesStatus_Complete)
-            {
-              transaction.LogChange(status.seriesId_, ChangeType_CompletedSeries, ResourceType_Series, hashSeries_);
-            }
-          }
-          
-          transaction.LogChange(status.seriesId_, ChangeType_NewChildInstance, ResourceType_Series, hashSeries_);
-          transaction.LogChange(status.studyId_, ChangeType_NewChildInstance, ResourceType_Study, hashStudy_);
-          transaction.LogChange(status.patientId_, ChangeType_NewChildInstance, ResourceType_Patient, hashPatient_);
-          
-          // Mark the parent resources of this instance as unstable
-          transaction.GetTransactionContext().MarkAsUnstable(status.seriesId_, ResourceType_Series, hashSeries_);
-          transaction.GetTransactionContext().MarkAsUnstable(status.studyId_, ResourceType_Study, hashStudy_);
-          transaction.GetTransactionContext().MarkAsUnstable(status.patientId_, ResourceType_Patient, hashPatient_);
-          transaction.GetTransactionContext().SignalAttachmentsAdded(instanceSize);
-
-          storeStatus_ = StoreStatus_Success;          
-        }
-        catch (OrthancException& e)
-        {
-          LOG(ERROR) << "EXCEPTION [" << e.What() << "]";
-          storeStatus_ = StoreStatus_Failure;
-        }
-      }
-    };
-
-
     uint64_t maximumStorageSize;
     unsigned int maximumPatients;
     
@@ -3608,94 +744,15 @@
       maximumPatients = maximumPatients_;
     }
 
-    Operations operations(instanceMetadata, dicomSummary, attachments, metadata, origin,
-                          overwrite, hasTransferSyntax, transferSyntax, hasPixelDataOffset,
-                          pixelDataOffset, maximumStorageSize, maximumPatients);
-    Apply(operations);
-    return operations.GetStoreStatus();
+    return StatelessDatabaseOperations::Store(
+      instanceMetadata, dicomSummary, attachments, metadata, origin, overwrite, hasTransferSyntax,
+      transferSyntax, hasPixelDataOffset, pixelDataOffset, maximumStorageSize, maximumPatients);
   }
 
-
+  
   StoreStatus ServerIndex::AddAttachment(const FileInfo& attachment,
                                          const std::string& publicId)
   {
-    class Operations : public IReadWriteOperations
-    {
-    private:
-      StoreStatus         status_;
-      const FileInfo&     attachment_;
-      const std::string&  publicId_;
-      uint64_t            maximumStorageSize_;
-      unsigned int        maximumPatientCount_;
-      
-    public:
-      Operations(const FileInfo& attachment,
-                 const std::string& publicId,
-                 uint64_t maximumStorageSize,
-                 unsigned int maximumPatientCount) :
-        status_(StoreStatus_Failure),
-        attachment_(attachment),
-        publicId_(publicId),
-        maximumStorageSize_(maximumStorageSize),
-        maximumPatientCount_(maximumPatientCount)
-      {
-      }
-
-      StoreStatus GetStatus() const
-      {
-        return status_;
-      }
-        
-      virtual void Apply(ReadWriteTransaction& transaction) ORTHANC_OVERRIDE
-      {
-        ResourceType resourceType;
-        int64_t resourceId;
-        if (!transaction.LookupResource(resourceId, resourceType, publicId_))
-        {
-          status_ = StoreStatus_Failure;  // Inexistent resource
-        }
-        else
-        {
-          // Remove possible previous attachment
-          transaction.DeleteAttachment(resourceId, attachment_.GetContentType());
-
-          // Locate the patient of the target resource
-          int64_t patientId = resourceId;
-          for (;;)
-          {
-            int64_t parent;
-            if (transaction.LookupParent(parent, patientId))
-            {
-              // We have not reached the patient level yet
-              patientId = parent;
-            }
-            else
-            {
-              // We have reached the patient level
-              break;
-            }
-          }
-
-          // Possibly apply the recycling mechanism while preserving this patient
-          assert(transaction.GetResourceType(patientId) == ResourceType_Patient);
-          transaction.Recycle(maximumStorageSize_, maximumPatientCount_,
-                              attachment_.GetCompressedSize(), transaction.GetPublicId(patientId));
-
-          transaction.AddAttachment(resourceId, attachment_);
-
-          if (IsUserContentType(attachment_.GetContentType()))
-          {
-            transaction.LogChange(resourceId, ChangeType_UpdatedAttachment, resourceType, publicId_);
-          }
-
-          transaction.GetTransactionContext().SignalAttachmentsAdded(attachment_.GetCompressedSize());
-
-          status_ = StoreStatus_Success;
-        }
-      }
-    };
-
-
     uint64_t maximumStorageSize;
     unsigned int maximumPatients;
     
@@ -3705,8 +762,7 @@
       maximumPatients = maximumPatients_;
     }
 
-    Operations operations(attachment, publicId, maximumStorageSize, maximumPatients);
-    Apply(operations);
-    return operations.GetStatus();
+    return StatelessDatabaseOperations::AddAttachment(
+      attachment, publicId, maximumStorageSize, maximumPatients);
   }
 }
--- a/OrthancServer/Sources/ServerIndex.h	Tue Mar 09 18:24:59 2021 +0100
+++ b/OrthancServer/Sources/ServerIndex.h	Wed Mar 10 15:59:03 2021 +0100
@@ -33,47 +33,32 @@
 
 #pragma once
 
+#include "Database/StatelessDatabaseOperations.h"
 #include "../../OrthancFramework/Sources/Cache/LeastRecentlyUsedIndex.h"
-#include "../../OrthancFramework/Sources/DicomFormat/DicomMap.h"
-
-#include "Database/IDatabaseWrapper.h"
-#include "DicomInstanceOrigin.h"
 
 #include <boost/thread.hpp>
-#include <boost/noncopyable.hpp>
 
 namespace Orthanc
 {
-  class DatabaseLookup;
-  class ParsedDicomFile;
   class ServerContext;
-
-  class ServerIndex : public boost::noncopyable
+  
+  class ServerIndex : public StatelessDatabaseOperations
   {
-  public:
-    typedef std::list<FileInfo> Attachments;
-    typedef std::map<std::pair<ResourceType, MetadataType>, std::string>  MetadataMap;
-
   private:
     class TransactionContextFactory;
     class Listener;
-    class Transaction;
     class UnstableResourcePayload;
-    class MainDicomTagsRegistry;
 
     bool done_;
     boost::mutex monitoringMutex_;
-    boost::mutex databaseMutex_;  // TODO - REMOVE
     boost::thread flushThread_;
     boost::thread unstableResourcesMonitorThread_;
 
     std::unique_ptr<Listener> listener_;
-    IDatabaseWrapper& db_;
     LeastRecentlyUsedIndex<int64_t, UnstableResourcePayload>  unstableResources_;
 
     uint64_t     maximumStorageSize_;
     unsigned int maximumPatients_;
-    std::unique_ptr<MainDicomTagsRegistry>  mainDicomTagsRegistry_;
 
     static void FlushThread(ServerIndex* that,
                             unsigned int threadSleep);
@@ -81,17 +66,10 @@
     static void UnstableResourcesMonitorThread(ServerIndex* that,
                                                unsigned int threadSleep);
 
-    void StandaloneRecycling(uint64_t maximumStorageSize,
-                             unsigned int maximumPatientCount);
-
     void MarkAsUnstable(int64_t id,
                         Orthanc::ResourceType type,
                         const std::string& publicId);
 
-    void NormalizeLookup(std::vector<DatabaseConstraint>& target,
-                         const DatabaseLookup& source,
-                         ResourceType level) const;
-
     bool IsUnstableResource(int64_t id);
 
   public:
@@ -109,516 +87,6 @@
     // "count == 0" means no limit on the number of patients
     void SetMaximumPatientCount(unsigned int count);
 
-    // It is assumed that "GetDatabaseVersion()" can run out of a
-    // database transaction
-    unsigned int GetDatabaseVersion()
-    {
-      return db_.GetDatabaseVersion();
-    }
-
-
-    
-    /***
-     ** PROTOTYPING FOR DB REFACTORING BELOW
-     ***/
-    
-  public:
-    class ITransactionContext : public boost::noncopyable
-    {
-    public:
-      virtual ~ITransactionContext()
-      {
-      }
-
-      virtual bool IsUnstableResource(int64_t id) = 0;
-
-      virtual bool LookupRemainingLevel(std::string& remainingPublicId /* out */,
-                                        ResourceType& remainingLevel   /* out */) = 0;
-
-      virtual void MarkAsUnstable(int64_t id,
-                                  Orthanc::ResourceType type,
-                                  const std::string& publicId) = 0;
-
-      virtual void SignalAttachmentsAdded(uint64_t compressedSize) = 0;
-
-      virtual void SignalChange(const ServerIndexChange& change) = 0;
-    };
-
-    
-    class ITransactionContextFactory : public boost::noncopyable
-    {
-    public:
-      virtual ~ITransactionContextFactory()
-      {
-      }
-
-      virtual ITransactionContext* Create() = 0;
-    };
-
-
-    class ReadOnlyTransaction : public boost::noncopyable
-    {
-    private:
-      ITransactionContext&  context_;
-      
-    protected:
-      IDatabaseWrapper&  db_;
-      
-    public:
-      explicit ReadOnlyTransaction(IDatabaseWrapper& db,
-                                   ITransactionContext& context) :
-        context_(context),
-        db_(db)
-      {
-      }
-
-      ITransactionContext& GetTransactionContext()
-      {
-        return context_;
-      }
-
-      /**
-       * Higher-level constructions
-       **/
-
-      SeriesStatus GetSeriesStatus(int64_t id,
-                                   int64_t expectedNumberOfInstances);
-
-      void MainDicomTagsToJson(Json::Value& result,
-                               int64_t resourceId,
-                               ResourceType resourceType);
-
-      /**
-       * Read-only methods from "IDatabaseWrapper"
-       **/
-
-      void ApplyLookupResources(std::list<std::string>& resourcesId,
-                                std::list<std::string>* instancesId, // Can be NULL if not needed
-                                const std::vector<DatabaseConstraint>& lookup,
-                                ResourceType queryLevel,
-                                size_t limit)
-      {
-        return db_.ApplyLookupResources(resourcesId, instancesId, lookup, queryLevel, limit);
-      }
-
-      void GetAllMetadata(std::map<MetadataType, std::string>& target,
-                          int64_t id)
-      {
-        db_.GetAllMetadata(target, id);
-      }
-
-      void GetAllPublicIds(std::list<std::string>& target,
-                           ResourceType resourceType)
-      {
-        return db_.GetAllPublicIds(target, resourceType);
-      }
-
-      void GetAllPublicIds(std::list<std::string>& target,
-                           ResourceType resourceType,
-                           size_t since,
-                           size_t limit)
-      {
-        return db_.GetAllPublicIds(target, resourceType, since, limit);
-      }  
-
-      void GetChanges(std::list<ServerIndexChange>& target /*out*/,
-                      bool& done /*out*/,
-                      int64_t since,
-                      uint32_t maxResults)
-      {
-        db_.GetChanges(target, done, since, maxResults);
-      }
-
-      void GetChildrenInternalId(std::list<int64_t>& target,
-                                 int64_t id)
-      {
-        db_.GetChildrenInternalId(target, id);
-      }
-
-      void GetChildrenPublicId(std::list<std::string>& target,
-                               int64_t id)
-      {
-        db_.GetChildrenPublicId(target, id);
-      }
-
-      void GetExportedResources(std::list<ExportedResource>& target /*out*/,
-                                bool& done /*out*/,
-                                int64_t since,
-                                uint32_t maxResults)
-      {
-        return db_.GetExportedResources(target, done, since, maxResults);
-      }
-
-      void GetLastChange(std::list<ServerIndexChange>& target /*out*/)
-      {
-        db_.GetLastChange(target);
-      }
-
-      void GetLastExportedResource(std::list<ExportedResource>& target /*out*/)
-      {
-        return db_.GetLastExportedResource(target);
-      }
-
-      int64_t GetLastChangeIndex()
-      {
-        return db_.GetLastChangeIndex();
-      }
-
-      void GetMainDicomTags(DicomMap& map,
-                            int64_t id)
-      {
-        db_.GetMainDicomTags(map, id);
-      }
-
-      std::string GetPublicId(int64_t resourceId)
-      {
-        return db_.GetPublicId(resourceId);
-      }
-      
-      uint64_t GetResourceCount(ResourceType resourceType)
-      {
-        return db_.GetResourceCount(resourceType);
-      }
-      
-      ResourceType GetResourceType(int64_t resourceId)
-      {
-        return db_.GetResourceType(resourceId);
-      }
-
-      uint64_t GetTotalCompressedSize()
-      {
-        return db_.GetTotalCompressedSize();
-      }
-    
-      uint64_t GetTotalUncompressedSize()
-      {
-        return db_.GetTotalUncompressedSize();
-      }
-      
-      bool IsProtectedPatient(int64_t internalId)
-      {
-        return db_.IsProtectedPatient(internalId);
-      }
-
-      void ListAvailableAttachments(std::set<FileContentType>& target,
-                                    int64_t id)
-      {
-        db_.ListAvailableAttachments(target, id);
-      }
-
-      bool LookupAttachment(FileInfo& attachment,
-                            int64_t id,
-                            FileContentType contentType)
-      {
-        return db_.LookupAttachment(attachment, id, contentType);
-      }
-      
-      bool LookupGlobalProperty(std::string& target,
-                                GlobalProperty property)
-      {
-        return db_.LookupGlobalProperty(target, property);
-      }
-
-      bool LookupMetadata(std::string& target,
-                          int64_t id,
-                          MetadataType type)
-      {
-        return db_.LookupMetadata(target, id, type);
-      }
-
-      bool LookupParent(int64_t& parentId,
-                        int64_t resourceId)
-      {
-        return db_.LookupParent(parentId, resourceId);
-      }
-        
-      bool LookupResource(int64_t& id,
-                          ResourceType& type,
-                          const std::string& publicId)
-      {
-        return db_.LookupResource(id, type, publicId);
-      }
-      
-      bool LookupResourceAndParent(int64_t& id,
-                                   ResourceType& type,
-                                   std::string& parentPublicId,
-                                   const std::string& publicId)
-      {
-        return db_.LookupResourceAndParent(id, type, parentPublicId, publicId);
-      }
-    };
-
-
-    class ReadWriteTransaction : public ReadOnlyTransaction
-    {
-    public:
-      ReadWriteTransaction(IDatabaseWrapper& db,
-                           ITransactionContext& context) :
-        ReadOnlyTransaction(db, context)
-      {
-      }
-
-      void AddAttachment(int64_t id,
-                         const FileInfo& attachment)
-      {
-        db_.AddAttachment(id, attachment);
-      }
-      
-      void ClearChanges()
-      {
-        db_.ClearChanges();
-      }
-
-      void ClearExportedResources()
-      {
-        db_.ClearExportedResources();
-      }
-
-      void ClearMainDicomTags(int64_t id)
-      {
-        return db_.ClearMainDicomTags(id);
-      }
-
-      bool CreateInstance(IDatabaseWrapper::CreateInstanceResult& result, /* out */
-                          int64_t& instanceId,          /* out */
-                          const std::string& patient,
-                          const std::string& study,
-                          const std::string& series,
-                          const std::string& instance)
-      {
-        return db_.CreateInstance(result, instanceId, patient, study, series, instance);
-      }
-
-      void DeleteAttachment(int64_t id,
-                            FileContentType attachment)
-      {
-        return db_.DeleteAttachment(id, attachment);
-      }
-      
-      void DeleteMetadata(int64_t id,
-                          MetadataType type)
-      {
-        db_.DeleteMetadata(id, type);
-      }
-
-      void DeleteResource(int64_t id)
-      {
-        db_.DeleteResource(id);
-      }
-
-      void LogChange(int64_t internalId,
-                     ChangeType changeType,
-                     ResourceType resourceType,
-                     const std::string& publicId);
-
-      void LogExportedResource(const ExportedResource& resource)
-      {
-        db_.LogExportedResource(resource);
-      }
-
-      void SetGlobalProperty(GlobalProperty property,
-                             const std::string& value)
-      {
-        db_.SetGlobalProperty(property, value);
-      }
-
-      void SetMetadata(int64_t id,
-                       MetadataType type,
-                       const std::string& value)
-      {
-        return db_.SetMetadata(id, type, value);
-      }
-
-      void SetProtectedPatient(int64_t internalId, 
-                               bool isProtected)
-      {
-        db_.SetProtectedPatient(internalId, isProtected);
-      }
-
-      void SetResourcesContent(const ResourcesContent& content)
-      {
-        db_.SetResourcesContent(content);
-      }
-
-      void Recycle(uint64_t maximumStorageSize,
-                   unsigned int maximumPatients,
-                   uint64_t addedInstanceSize,
-                   const std::string& newPatientId);
-    };
-
-
-    class IReadOnlyOperations : public boost::noncopyable
-    {
-    public:
-      virtual ~IReadOnlyOperations()
-      {
-      }
-
-      virtual void Apply(ReadOnlyTransaction& transaction) = 0;
-    };
-
-
-    class IReadWriteOperations : public boost::noncopyable
-    {
-    public:
-      virtual ~IReadWriteOperations()
-      {
-      }
-
-      virtual void Apply(ReadWriteTransaction& transaction) = 0;
-    };
-    
-  private:
-    void ApplyInternal(IReadOnlyOperations* readOperations,
-                       IReadWriteOperations* writeOperations);
-
-    std::unique_ptr<ITransactionContextFactory>  factory_;
-    unsigned int maxRetries_;
-
-  public:
-    void SetTransactionContextFactory(ITransactionContextFactory* factory /* takes ownership */);
-    
-    void Apply(IReadOnlyOperations& operations);
-  
-    void Apply(IReadWriteOperations& operations);
-
-    bool ExpandResource(Json::Value& target,
-                        const std::string& publicId,
-                        ResourceType level);
-
-    void GetAllMetadata(std::map<MetadataType, std::string>& target,
-                        const std::string& publicId,
-                        ResourceType level);
-
-    void GetAllUuids(std::list<std::string>& target,
-                     ResourceType resourceType);
-
-    void GetAllUuids(std::list<std::string>& target,
-                     ResourceType resourceType,
-                     size_t since,
-                     size_t limit);
-
-    void GetGlobalStatistics(/* out */ uint64_t& diskSize,
-                             /* out */ uint64_t& uncompressedSize,
-                             /* out */ uint64_t& countPatients, 
-                             /* out */ uint64_t& countStudies, 
-                             /* out */ uint64_t& countSeries, 
-                             /* out */ uint64_t& countInstances);
-
-    bool LookupAttachment(FileInfo& attachment,
-                          const std::string& instancePublicId,
-                          FileContentType contentType);
-
-    void GetChanges(Json::Value& target,
-                    int64_t since,
-                    unsigned int maxResults);
-
-    void GetLastChange(Json::Value& target);
-
-    void GetExportedResources(Json::Value& target,
-                              int64_t since,
-                              unsigned int maxResults);
-
-    void GetLastExportedResource(Json::Value& target);
-
-    bool IsProtectedPatient(const std::string& publicId);
-
-    void GetChildren(std::list<std::string>& result,
-                     const std::string& publicId);
-
-    void GetChildInstances(std::list<std::string>& result,
-                           const std::string& publicId);
-
-    bool LookupMetadata(std::string& target,
-                        const std::string& publicId,
-                        ResourceType expectedType,
-                        MetadataType type);
-
-    void ListAvailableAttachments(std::set<FileContentType>& target,
-                                  const std::string& publicId,
-                                  ResourceType expectedType);
-
-    bool LookupParent(std::string& target,
-                      const std::string& publicId);
-
-    void GetResourceStatistics(/* out */ ResourceType& type,
-                               /* out */ uint64_t& diskSize, 
-                               /* out */ uint64_t& uncompressedSize, 
-                               /* out */ unsigned int& countStudies, 
-                               /* out */ unsigned int& countSeries, 
-                               /* out */ unsigned int& countInstances, 
-                               /* out */ uint64_t& dicomDiskSize, 
-                               /* out */ uint64_t& dicomUncompressedSize, 
-                               const std::string& publicId);
-
-    void LookupIdentifierExact(std::vector<std::string>& result,
-                               ResourceType level,
-                               const DicomTag& tag,
-                               const std::string& value);
-
-    bool LookupGlobalProperty(std::string& value,
-                              GlobalProperty property);
-
-    std::string GetGlobalProperty(GlobalProperty property,
-                                  const std::string& defaultValue);
-
-    bool GetMainDicomTags(DicomMap& result,
-                          const std::string& publicId,
-                          ResourceType expectedType,
-                          ResourceType levelOfInterest);
-
-    // Only applicable at the instance level
-    bool GetAllMainDicomTags(DicomMap& result,
-                             const std::string& instancePublicId);
-
-    bool LookupResourceType(ResourceType& type,
-                            const std::string& publicId);
-
-    bool LookupParent(std::string& target,
-                      const std::string& publicId,
-                      ResourceType parentType);
-
-    void ApplyLookupResources(std::vector<std::string>& resourcesId,
-                              std::vector<std::string>* instancesId,  // Can be NULL if not needed
-                              const DatabaseLookup& lookup,
-                              ResourceType queryLevel,
-                              size_t limit);
-
-    bool DeleteResource(Json::Value& target /* out */,
-                        const std::string& uuid,
-                        ResourceType expectedType);
-
-    void LogExportedResource(const std::string& publicId,
-                             const std::string& remoteModality);
-
-    void SetProtectedPatient(const std::string& publicId,
-                             bool isProtected);
-
-    void SetMetadata(const std::string& publicId,
-                     MetadataType type,
-                     const std::string& value);
-
-    void DeleteMetadata(const std::string& publicId,
-                        MetadataType type);
-
-    uint64_t IncrementGlobalSequence(GlobalProperty sequence);
-
-    void DeleteChanges();
-
-    void DeleteExportedResources();
-
-    void SetGlobalProperty(GlobalProperty property,
-                           const std::string& value);
-
-    void DeleteAttachment(const std::string& publicId,
-                          FileContentType type);
-
-    void LogChange(ChangeType changeType,
-                   const std::string& publicId,
-                   ResourceType level);
-
-    void ReconstructInstance(const ParsedDicomFile& dicom);
-
     StoreStatus Store(std::map<MetadataType, std::string>& instanceMetadata,
                       const DicomMap& dicomSummary,
                       const Attachments& attachments,