diff OrthancServer/Sources/ServerIndex.cpp @ 4044:d25f4c0fa160 framework

splitting code into OrthancFramework and OrthancServer
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 10 Jun 2020 20:30:34 +0200
parents OrthancServer/ServerIndex.cpp@058b5ade8acd
children 05b8fd21089c
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/Sources/ServerIndex.cpp	Wed Jun 10 20:30:34 2020 +0200
@@ -0,0 +1,2597 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ * Copyright (C) 2017-2020 Osimis S.A., Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "PrecompiledHeadersServer.h"
+#include "ServerIndex.h"
+
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif
+
+#include "../Core/DicomFormat/DicomArray.h"
+#include "../Core/DicomParsing/FromDcmtkBridge.h"
+#include "../Core/DicomParsing/ParsedDicomFile.h"
+#include "../Core/Logging.h"
+#include "../Core/Toolbox.h"
+
+#include "Database/ResourcesContent.h"
+#include "DicomInstanceToStore.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 <stdio.h>
+
+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
+  {
+  private:
+    struct FileToRemove
+    {
+    private:
+      std::string  uuid_;
+      FileContentType  type_;
+
+    public:
+      FileToRemove(const FileInfo& info) : uuid_(info.GetUuid()), 
+                                           type_(info.GetContentType())
+      {
+      }
+
+      const std::string& GetUuid() const
+      {
+        return uuid_;
+      }
+
+      FileContentType GetContentType() const 
+      {
+        return type_;
+      }
+    };
+
+    ServerContext& context_;
+    bool hasRemainingLevel_;
+    ResourceType remainingType_;
+    std::string remainingPublicId_;
+    std::list<FileToRemove> pendingFilesToRemove_;
+    std::list<ServerIndexChange> pendingChanges_;
+    uint64_t sizeOfFilesToRemove_;
+    bool insideTransaction_;
+
+    void Reset()
+    {
+      sizeOfFilesToRemove_ = 0;
+      hasRemainingLevel_ = false;
+      pendingFilesToRemove_.clear();
+      pendingChanges_.clear();
+    }
+
+  public:
+    Listener(ServerContext& context) : context_(context),
+                                       insideTransaction_(false)      
+    {
+      Reset();
+      assert(ResourceType_Patient < ResourceType_Study &&
+             ResourceType_Study < ResourceType_Series &&
+             ResourceType_Series < ResourceType_Instance);
+    }
+
+    void StartTransaction()
+    {
+      Reset();
+      insideTransaction_ = true;
+    }
+
+    void EndTransaction()
+    {
+      insideTransaction_ = false;
+    }
+
+    uint64_t GetSizeOfFilesToRemove()
+    {
+      return sizeOfFilesToRemove_;
+    }
+
+    void CommitFilesToRemove()
+    {
+      for (std::list<FileToRemove>::const_iterator 
+             it = pendingFilesToRemove_.begin();
+           it != pendingFilesToRemove_.end(); ++it)
+      {
+        try
+        {
+          context_.RemoveFile(it->GetUuid(), it->GetContentType());
+        }
+        catch (OrthancException& e)
+        {
+          LOG(ERROR) << "Unable to remove an attachment from the storage area: "
+                     << it->GetUuid() << " (type: " << EnumerationToString(it->GetContentType()) << ")";
+        }
+      }
+    }
+
+    void CommitChanges()
+    {
+      for (std::list<ServerIndexChange>::const_iterator 
+             it = pendingChanges_.begin(); 
+           it != pendingChanges_.end(); ++it)
+      {
+        context_.SignalChange(*it);
+      }
+    }
+
+    virtual void SignalRemainingAncestor(ResourceType parentType,
+                                         const std::string& publicId)
+    {
+      VLOG(1) << "Remaining ancestor \"" << publicId << "\" (" << parentType << ")";
+
+      if (hasRemainingLevel_)
+      {
+        if (parentType < remainingType_)
+        {
+          remainingType_ = parentType;
+          remainingPublicId_ = publicId;
+        }
+      }
+      else
+      {
+        hasRemainingLevel_ = true;
+        remainingType_ = parentType;
+        remainingPublicId_ = publicId;
+      }        
+    }
+
+    virtual void SignalFileDeleted(const FileInfo& info)
+    {
+      assert(Toolbox::IsUuid(info.GetUuid()));
+      pendingFilesToRemove_.push_back(FileToRemove(info));
+      sizeOfFilesToRemove_ += info.GetCompressedSize();
+    }
+
+    virtual void SignalChange(const ServerIndexChange& change)
+    {
+      VLOG(1) << "Change related to resource " << change.GetPublicId() << " of type " 
+              << EnumerationToString(change.GetResourceType()) << ": " 
+              << EnumerationToString(change.GetChangeType());
+
+      if (insideTransaction_)
+      {
+        pendingChanges_.push_back(change);
+      }
+      else
+      {
+        context_.SignalChange(change);
+      }
+    }
+
+    bool HasRemainingLevel() const
+    {
+      return hasRemainingLevel_;
+    }
+
+    ResourceType GetRemainingType() const
+    {
+      assert(HasRemainingLevel());
+      return remainingType_;
+    }
+
+    const std::string& GetRemainingPublicId() const
+    {
+      assert(HasRemainingLevel());
+      return remainingPublicId_;
+    }                                 
+  };
+
+
+  class ServerIndex::Transaction
+  {
+  private:
+    ServerIndex& index_;
+    std::unique_ptr<IDatabaseWrapper::ITransaction> transaction_;
+    bool isCommitted_;
+
+  public:
+    Transaction(ServerIndex& index) : 
+      index_(index),
+      isCommitted_(false)
+    {
+      transaction_.reset(index_.db_.StartTransaction());
+      transaction_->Begin();
+
+      index_.listener_->StartTransaction();
+    }
+
+    ~Transaction()
+    {
+      index_.listener_->EndTransaction();
+
+      if (!isCommitted_)
+      {
+        transaction_->Rollback();
+      }
+    }
+
+    void Commit(uint64_t sizeOfAddedFiles)
+    {
+      if (!isCommitted_)
+      {
+        int64_t delta = (static_cast<int64_t>(sizeOfAddedFiles) -
+                         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:
+    ResourceType type_;
+    std::string publicId_;
+    boost::posix_time::ptime time_;
+
+  public:
+    UnstableResourcePayload() : type_(ResourceType_Instance)
+    {
+    }
+
+    UnstableResourcePayload(Orthanc::ResourceType type,
+                            const std::string& publicId) : 
+      type_(type),
+      publicId_(publicId)
+    {
+      time_ = boost::posix_time::second_clock::local_time();
+    }
+
+    unsigned int GetAge() const
+    {
+      return (boost::posix_time::second_clock::local_time() - time_).total_seconds();
+    }
+
+    ResourceType GetResourceType() const
+    {
+      return type_;
+    }
+    
+    const std::string& GetPublicId() const
+    {
+      return publicId_;
+    }
+  };
+
+
+  class ServerIndex::MainDicomTagsRegistry : public boost::noncopyable
+  {
+  private:
+    class TagInfo
+    {
+    private:
+      ResourceType  level_;
+      DicomTagType  type_;
+
+    public:
+      TagInfo()
+      {
+      }
+
+      TagInfo(ResourceType level,
+              DicomTagType type) :
+        level_(level),
+        type_(type)
+      {
+      }
+
+      ResourceType GetLevel() const
+      {
+        return level_;
+      }
+
+      DicomTagType GetType() const
+      {
+        return type_;
+      }
+    };
+      
+    typedef std::map<DicomTag, TagInfo>   Registry;
+
+
+    Registry  registry_;
+      
+    void LoadTags(ResourceType level)
+    {
+      {
+        const DicomTag* tags = NULL;
+        size_t size;
+  
+        ServerToolbox::LoadIdentifiers(tags, size, level);
+  
+        for (size_t i = 0; i < size; i++)
+        {
+          if (registry_.find(tags[i]) == registry_.end())
+          {
+            registry_[tags[i]] = TagInfo(level, DicomTagType_Identifier);
+          }
+          else
+          {
+            // These patient-level tags are copied in the study level
+            assert(level == ResourceType_Study &&
+                   (tags[i] == DICOM_TAG_PATIENT_ID ||
+                    tags[i] == DICOM_TAG_PATIENT_NAME ||
+                    tags[i] == DICOM_TAG_PATIENT_BIRTH_DATE));
+          }
+        }
+      }
+
+      {
+        std::set<DicomTag> tags;
+        DicomMap::GetMainDicomTags(tags, level);
+
+        for (std::set<DicomTag>::const_iterator
+               tag = tags.begin(); tag != tags.end(); ++tag)
+        {
+          if (registry_.find(*tag) == registry_.end())
+          {
+            registry_[*tag] = TagInfo(level, DicomTagType_Main);
+          }
+        }
+      }
+    }
+
+  public:
+    MainDicomTagsRegistry()
+    {
+      LoadTags(ResourceType_Patient);
+      LoadTags(ResourceType_Study);
+      LoadTags(ResourceType_Series);
+      LoadTags(ResourceType_Instance); 
+    }
+
+    void LookupTag(ResourceType& level,
+                   DicomTagType& type,
+                   const DicomTag& tag) const
+    {
+      Registry::const_iterator it = registry_.find(tag);
+
+      if (it == registry_.end())
+      {
+        // Default values
+        level = ResourceType_Instance;
+        type = DicomTagType_Generic;
+      }
+      else
+      {
+        level = it->second.GetLevel();
+        type = it->second.GetType();
+      }
+    }
+  };
+
+
+  bool ServerIndex::DeleteResource(Json::Value& target,
+                                   const std::string& uuid,
+                                   ResourceType expectedType)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Transaction t(*this);
+
+    int64_t id;
+    ResourceType type;
+    if (!db_.LookupResource(id, type, uuid) ||
+        expectedType != type)
+    {
+      return false;
+    }
+      
+    db_.DeleteResource(id);
+
+    if (listener_->HasRemainingLevel())
+    {
+      ResourceType type = listener_->GetRemainingType();
+      const std::string& uuid = listener_->GetRemainingPublicId();
+
+      target["RemainingAncestor"] = Json::Value(Json::objectValue);
+      target["RemainingAncestor"]["Path"] = GetBasePath(type, uuid);
+      target["RemainingAncestor"]["Type"] = EnumerationToString(type);
+      target["RemainingAncestor"]["ID"] = uuid;
+    }
+    else
+    {
+      target["RemainingAncestor"] = Json::nullValue;
+    }
+
+    t.Commit(0);
+
+    return true;
+  }
+
+
+  void ServerIndex::FlushThread(ServerIndex* that,
+                                unsigned int threadSleep)
+  {
+    // By default, wait for 10 seconds before flushing
+    unsigned int sleep = 10;
+
+    try
+    {
+      boost::mutex::scoped_lock lock(that->mutex_);
+      std::string sleepString;
+
+      if (that->db_.LookupGlobalProperty(sleepString, GlobalProperty_FlushSleep) &&
+          Toolbox::IsInteger(sleepString))
+      {
+        sleep = boost::lexical_cast<unsigned int>(sleepString);
+      }
+    }
+    catch (boost::bad_lexical_cast&)
+    {
+    }
+
+    LOG(INFO) << "Starting the database flushing thread (sleep = " << sleep << ")";
+
+    unsigned int count = 0;
+
+    while (!that->done_)
+    {
+      boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep));
+      count++;
+      if (count < sleep)
+      {
+        continue;
+      }
+
+      Logging::Flush();
+
+      boost::mutex::scoped_lock lock(that->mutex_);
+
+      try
+      {
+        that->db_.FlushToDisk();
+      }
+      catch (OrthancException&)
+      {
+        LOG(ERROR) << "Cannot flush the SQLite database to the disk (is your filesystem full?)";
+      }
+          
+      count = 0;
+    }
+
+    LOG(INFO) << "Stopping the database flushing thread";
+  }
+
+
+  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;
+  }
+
+
+
+
+  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;
+    }
+  }
+
+
+  void ServerIndex::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);
+    }
+
+    assert(listener_.get() != NULL);
+    listener_->SignalChange(change);
+  }
+
+
+  uint64_t ServerIndex::IncrementGlobalSequenceInternal(GlobalProperty property)
+  {
+    std::string oldValue;
+
+    if (db_.LookupGlobalProperty(oldValue, property))
+    {
+      uint64_t oldNumber;
+
+      try
+      {
+        oldNumber = boost::lexical_cast<uint64_t>(oldValue);
+        db_.SetGlobalProperty(property, boost::lexical_cast<std::string>(oldNumber + 1));
+        return oldNumber + 1;
+      }
+      catch (boost::bad_lexical_cast&)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+    else
+    {
+      // Initialize the sequence at "1"
+      db_.SetGlobalProperty(property, "1");
+      return 1;
+    }
+  }
+
+
+
+  ServerIndex::ServerIndex(ServerContext& context,
+                           IDatabaseWrapper& db,
+                           unsigned int threadSleep) : 
+    done_(false),
+    db_(db),
+    maximumStorageSize_(0),
+    maximumPatients_(0),
+    mainDicomTagsRegistry_(new MainDicomTagsRegistry)
+  {
+    listener_.reset(new Listener(context));
+    db_.SetListener(*listener_);
+
+    // Initial recycling if the parameters have changed since the last
+    // execution of Orthanc
+    StandaloneRecycling();
+
+    if (db.HasFlushToDisk())
+    {
+      flushThread_ = boost::thread(FlushThread, this, threadSleep);
+    }
+
+    unstableResourcesMonitorThread_ = boost::thread
+      (UnstableResourcesMonitorThread, this, threadSleep);
+  }
+
+
+
+  ServerIndex::~ServerIndex()
+  {
+    if (!done_)
+    {
+      LOG(ERROR) << "INTERNAL ERROR: ServerIndex::Stop() should be invoked manually to avoid mess in the destruction order!";
+      Stop();
+    }
+  }
+
+
+
+  void ServerIndex::Stop()
+  {
+    if (!done_)
+    {
+      done_ = true;
+
+      if (db_.HasFlushToDisk() &&
+          flushThread_.joinable())
+      {
+        flushThread_.join();
+      }
+
+      if (unstableResourcesMonitorThread_.joinable())
+      {
+        unstableResourcesMonitorThread_.join();
+      }
+    }
+  }
+
+
+  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;
+  }
+
+
+  void ServerIndex::SignalNewResource(ChangeType changeType,
+                                      ResourceType level,
+                                      const std::string& publicId,
+                                      int64_t internalId)
+  {
+    ServerIndexChange change(changeType, level, publicId);
+    db_.LogChange(internalId, change);
+    
+    assert(listener_.get() != NULL);
+    listener_->SignalChange(change);
+  }
+
+  
+  StoreStatus ServerIndex::Store(std::map<MetadataType, std::string>& instanceMetadata,
+                                 DicomInstanceToStore& instanceToStore,
+                                 const Attachments& attachments,
+                                 bool overwrite)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    const DicomMap& dicomSummary = instanceToStore.GetSummary();
+    const ServerIndex::MetadataMap& metadata = instanceToStore.GetMetadata();
+
+    int64_t expectedInstances;
+    const bool hasExpectedInstances =
+      ComputeExpectedNumberOfInstances(expectedInstances, dicomSummary);
+    
+    instanceMetadata.clear();
+
+    const std::string hashPatient = instanceToStore.GetHasher().HashPatient();
+    const std::string hashStudy = instanceToStore.GetHasher().HashStudy();
+    const std::string hashSeries = instanceToStore.GetHasher().HashSeries();
+    const std::string hashInstance = instanceToStore.GetHasher().HashInstance();
+
+    try
+    {
+      Transaction t(*this);
+
+      IDatabaseWrapper::CreateInstanceResult status;
+      int64_t instanceId;
+
+      // Check whether this instance is already stored
+      if (!db_.CreateInstance(status, instanceId, hashPatient,
+                              hashStudy, hashSeries, hashInstance))
+      {
+        // The instance already exists
+        
+        if (overwrite)
+        {
+          // Overwrite the old instance
+          LOG(INFO) << "Overwriting instance: " << hashInstance;
+          db_.DeleteResource(instanceId);
+
+          // Re-create the instance, now that the old one is removed
+          if (!db_.CreateInstance(status, instanceId, hashPatient,
+                                  hashStudy, hashSeries, hashInstance))
+          {
+            throw OrthancException(ErrorCode_InternalError);
+          }
+        }
+        else
+        {
+          // Do nothing if the instance already exists and overwriting is disabled
+          db_.GetAllMetadata(instanceMetadata, instanceId);
+          return StoreStatus_AlreadyStored;
+        }
+      }
+
+
+      // 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 "db_.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.
+      SignalNewResource(ChangeType_NewInstance, ResourceType_Instance, hashInstance, instanceId);
+
+      if (status.isNewSeries_)
+      {
+        SignalNewResource(ChangeType_NewSeries, ResourceType_Series, hashSeries, status.seriesId_);
+      }
+      
+      if (status.isNewStudy_)
+      {
+        SignalNewResource(ChangeType_NewStudy, ResourceType_Study, hashStudy, status.studyId_);
+      }
+      
+      if (status.isNewPatient_)
+      {
+        SignalNewResource(ChangeType_NewPatient, ResourceType_Patient, hashPatient, status.patientId_);
+      }
+      
+      
+      // 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();
+      }
+
+      Recycle(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)
+      {
+        db_.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_ &&
+            hasExpectedInstances)
+        {
+          content.AddMetadata(status.seriesId_, MetadataType_Series_ExpectedNumberOfInstances,
+                              boost::lexical_cast<std::string>(expectedInstances));
+        }
+
+        
+        // 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_Instance_RemoteAet,
+                            instanceToStore.GetOrigin().GetRemoteAetC());
+        SetInstanceMetadata(content, instanceMetadata, instanceId, MetadataType_Instance_Origin, 
+                            EnumerationToString(instanceToStore.GetOrigin().GetRequestOrigin()));
+
+
+        {
+          std::string s;
+
+          if (instanceToStore.LookupTransferSyntax(s))
+          {
+            // New in Orthanc 1.2.0
+            SetInstanceMetadata(content, instanceMetadata, instanceId,
+                                MetadataType_Instance_TransferSyntax, s);
+          }
+
+          if (instanceToStore.GetOrigin().LookupRemoteIp(s))
+          {
+            // New in Orthanc 1.4.0
+            SetInstanceMetadata(content, instanceMetadata, instanceId,
+                                MetadataType_Instance_RemoteIp, s);
+          }
+
+          if (instanceToStore.GetOrigin().LookupCalledAet(s))
+          {
+            // New in Orthanc 1.4.0
+            SetInstanceMetadata(content, instanceMetadata, instanceId,
+                                MetadataType_Instance_CalledAet, s);
+          }
+
+          if (instanceToStore.GetOrigin().LookupHttpUsername(s))
+          {
+            // New in Orthanc 1.4.0
+            SetInstanceMetadata(content, instanceMetadata, instanceId,
+                                MetadataType_Instance_HttpUsername, s);
+          }
+        }
+
+        
+        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, value->GetContent());
+          }
+        }
+
+        
+        db_.SetResourcesContent(content);
+      }
+
+  
+      // Check whether the series of this new instance is now completed
+      int64_t expectedNumberOfInstances;
+      if (ComputeExpectedNumberOfInstances(expectedNumberOfInstances, dicomSummary))
+      {
+        SeriesStatus seriesStatus = GetSeriesStatus(status.seriesId_, expectedNumberOfInstances);
+        if (seriesStatus == SeriesStatus_Complete)
+        {
+          LogChange(status.seriesId_, ChangeType_CompletedSeries, ResourceType_Series, hashSeries);
+        }
+      }
+      
+
+      // Mark the parent resources of this instance as unstable
+      MarkAsUnstable(status.seriesId_, ResourceType_Series, hashSeries);
+      MarkAsUnstable(status.studyId_, ResourceType_Study, hashStudy);
+      MarkAsUnstable(status.patientId_, ResourceType_Patient, hashPatient);
+
+      t.Commit(instanceSize);
+
+      return StoreStatus_Success;
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "EXCEPTION [" << e.What() << "]";
+    }
+
+    return StoreStatus_Failure;
+  }
+
+
+  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)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    diskSize = db_.GetTotalCompressedSize();
+    uncompressedSize = db_.GetTotalUncompressedSize();
+    countPatients = db_.GetResourceCount(ResourceType_Patient);
+    countStudies = db_.GetResourceCount(ResourceType_Study);
+    countSeries = db_.GetResourceCount(ResourceType_Series);
+    countInstances = db_.GetResourceCount(ResourceType_Instance);
+  }
+
+  
+  SeriesStatus ServerIndex::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::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);
+    }
+  }
+
+  
+  bool ServerIndex::LookupResource(Json::Value& result,
+                                   const std::string& publicId,
+                                   ResourceType expectedType)
+  {
+    result = Json::objectValue;
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    // Lookup for the requested resource
+    int64_t id;
+    ResourceType type;
+    std::string parent;
+    if (!db_.LookupResourceAndParent(id, type, parent, publicId) ||
+        type != expectedType)
+    {
+      return false;
+    }
+
+    // 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:
+          result["ParentPatient"] = parent;
+          break;
+
+        case ResourceType_Series:
+          result["ParentStudy"] = parent;
+          break;
+
+        case ResourceType_Instance:
+          result["ParentSeries"] = parent;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    // List the children resources
+    std::list<std::string> children;
+    db_.GetChildrenPublicId(children, id);
+
+    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:
+          result["Studies"] = c;
+          break;
+
+        case ResourceType_Study:
+          result["Series"] = c;
+          break;
+
+        case ResourceType_Series:
+          result["Instances"] = c;
+          break;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+
+    // Extract the metadata
+    std::map<MetadataType, std::string> metadata;
+    db_.GetAllMetadata(metadata, id);
+
+    // Set the resource type
+    switch (type)
+    {
+      case ResourceType_Patient:
+        result["Type"] = "Patient";
+        break;
+
+      case ResourceType_Study:
+        result["Type"] = "Study";
+        break;
+
+      case ResourceType_Series:
+      {
+        result["Type"] = "Series";
+
+        int64_t i;
+        if (LookupIntegerMetadata(i, metadata, MetadataType_Series_ExpectedNumberOfInstances))
+        {
+          result["ExpectedNumberOfInstances"] = static_cast<int>(i);
+          result["Status"] = EnumerationToString(GetSeriesStatus(id, i));
+        }
+        else
+        {
+          result["ExpectedNumberOfInstances"] = Json::nullValue;
+          result["Status"] = EnumerationToString(SeriesStatus_Unknown);
+        }
+
+        break;
+      }
+
+      case ResourceType_Instance:
+      {
+        result["Type"] = "Instance";
+
+        FileInfo attachment;
+        if (!db_.LookupAttachment(attachment, id, FileContentType_Dicom))
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        result["FileSize"] = static_cast<unsigned int>(attachment.GetUncompressedSize());
+        result["FileUuid"] = attachment.GetUuid();
+
+        int64_t i;
+        if (LookupIntegerMetadata(i, metadata, MetadataType_Instance_IndexInSeries))
+        {
+          result["IndexInSeries"] = static_cast<int>(i);
+        }
+        else
+        {
+          result["IndexInSeries"] = Json::nullValue;
+        }
+
+        break;
+      }
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    // Record the remaining information
+    result["ID"] = publicId;
+    MainDicomTagsToJson(result, id, type);
+
+    std::string tmp;
+
+    if (LookupStringMetadata(tmp, metadata, MetadataType_AnonymizedFrom))
+    {
+      result["AnonymizedFrom"] = tmp;
+    }
+
+    if (LookupStringMetadata(tmp, metadata, MetadataType_ModifiedFrom))
+    {
+      result["ModifiedFrom"] = tmp;
+    }
+
+    if (type == ResourceType_Patient ||
+        type == ResourceType_Study ||
+        type == ResourceType_Series)
+    {
+      result["IsStable"] = !unstableResources_.Contains(id);
+
+      if (LookupStringMetadata(tmp, metadata, MetadataType_LastUpdate))
+      {
+        result["LastUpdate"] = tmp;
+      }
+    }
+
+    return true;
+  }
+
+
+  bool ServerIndex::LookupAttachment(FileInfo& attachment,
+                                     const std::string& instanceUuid,
+                                     FileContentType contentType)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    int64_t id;
+    ResourceType type;
+    if (!db_.LookupResource(id, type, instanceUuid))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    if (db_.LookupAttachment(attachment, id, contentType))
+    {
+      assert(attachment.GetContentType() == contentType);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+
+  void ServerIndex::GetAllUuids(std::list<std::string>& target,
+                                ResourceType resourceType)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    db_.GetAllPublicIds(target, resourceType);
+  }
+
+
+  void ServerIndex::GetAllUuids(std::list<std::string>& target,
+                                ResourceType resourceType,
+                                size_t since,
+                                size_t limit)
+  {
+    if (limit == 0)
+    {
+      target.clear();
+      return;
+    }
+
+    boost::mutex::scoped_lock lock(mutex_);
+    db_.GetAllPublicIds(target, resourceType, since, limit);
+  }
+
+
+  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::GetChanges(Json::Value& target,
+                               int64_t since,                               
+                               unsigned int maxResults)
+  {
+    std::list<ServerIndexChange> changes;
+    bool done;
+    bool hasLast = false;
+    int64_t last = 0;
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      // Fix wrt. Orthanc <= 1.3.2: A transaction was missing, as
+      // "GetLastChange()" involves calls to "GetPublicId()"
+      Transaction transaction(*this);
+
+      db_.GetChanges(changes, done, since, maxResults);
+      if (changes.empty())
+      {
+        last = db_.GetLastChangeIndex();
+        hasLast = true;
+      }
+      
+      transaction.Commit(0);
+    }
+
+    FormatLog(target, changes, "Changes", done, since, hasLast, last);
+  }
+
+
+  void ServerIndex::GetLastChange(Json::Value& target)
+  {
+    std::list<ServerIndexChange> changes;
+    bool hasLast = false;
+    int64_t last = 0;
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      // Fix wrt. Orthanc <= 1.3.2: A transaction was missing, as
+      // "GetLastChange()" involves calls to "GetPublicId()"
+      Transaction transaction(*this);
+
+      db_.GetLastChange(changes);
+      if (changes.empty())
+      {
+        last = db_.GetLastChangeIndex();
+        hasLast = true;
+      }
+
+      transaction.Commit(0);
+    }
+
+    FormatLog(target, changes, "Changes", true, 0, hasLast, last);
+  }
+
+
+  void ServerIndex::LogExportedResource(const std::string& publicId,
+                                        const std::string& remoteModality)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    Transaction transaction(*this);
+
+    int64_t id;
+    ResourceType type;
+    if (!db_.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;
+      db_.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 = db_.LookupParent(currentId, currentId);
+        assert(ok);
+      }
+    }
+
+    ExportedResource resource(-1, 
+                              type,
+                              publicId,
+                              remoteModality,
+                              SystemToolbox::GetNowIsoString(true /* use UTC time (not local time) */),
+                              patientId,
+                              studyInstanceUid,
+                              seriesInstanceUid,
+                              sopInstanceUid);
+
+    db_.LogExportedResource(resource);
+    transaction.Commit(0);
+  }
+
+
+  void ServerIndex::GetExportedResources(Json::Value& target,
+                                         int64_t since,
+                                         unsigned int maxResults)
+  {
+    std::list<ExportedResource> exported;
+    bool done;
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      db_.GetExportedResources(exported, done, since, maxResults);
+    }
+
+    FormatLog(target, exported, "Exports", done, since, false, -1);
+  }
+
+
+  void ServerIndex::GetLastExportedResource(Json::Value& target)
+  {
+    std::list<ExportedResource> exported;
+
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      db_.GetLastExportedResource(exported);
+    }
+
+    FormatLog(target, exported, "Exports", true, 0, false, -1);
+  }
+
+
+  bool ServerIndex::IsRecyclingNeeded(uint64_t instanceSize)
+  {
+    if (maximumStorageSize_ != 0)
+    {
+      assert(maximumStorageSize_ >= instanceSize);
+      
+      if (db_.IsDiskSizeAbove(maximumStorageSize_ - instanceSize))
+      {
+        return true;
+      }
+    }
+
+    if (maximumPatients_ != 0)
+    {
+      uint64_t patientCount = db_.GetResourceCount(ResourceType_Patient);
+      if (patientCount > maximumPatients_)
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  
+  void ServerIndex::Recycle(uint64_t instanceSize,
+                            const std::string& newPatientId)
+  {
+    if (!IsRecyclingNeeded(instanceSize))
+    {
+      return;
+    }
+
+    // Check whether other DICOM instances from this patient are
+    // already stored
+    int64_t patientToAvoid;
+    ResourceType type;
+    bool hasPatientToAvoid = db_.LookupResource(patientToAvoid, type, newPatientId);
+
+    if (hasPatientToAvoid && 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);
+      }
+      
+      VLOG(1) << "Recycling one patient";
+      db_.DeleteResource(patientToRecycle);
+
+      if (!IsRecyclingNeeded(instanceSize))
+      {
+        // OK, we're done
+        break;
+      }
+    }
+  }  
+
+  void ServerIndex::SetMaximumPatientCount(unsigned int count) 
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    maximumPatients_ = count;
+
+    if (count == 0)
+    {
+      LOG(WARNING) << "No limit on the number of stored patients";
+    }
+    else
+    {
+      LOG(WARNING) << "At most " << count << " patients will be stored";
+    }
+
+    StandaloneRecycling();
+  }
+
+  void ServerIndex::SetMaximumStorageSize(uint64_t size) 
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    maximumStorageSize_ = size;
+
+    if (size == 0)
+    {
+      LOG(WARNING) << "No limit on the size of the storage area";
+    }
+    else
+    {
+      LOG(WARNING) << "At most " << (size / MEGA_BYTES) << "MB will be used for the storage area";
+    }
+
+    StandaloneRecycling();
+  }
+
+
+  void ServerIndex::StandaloneRecycling()
+  {
+    // WARNING: No mutex here, do not include this as a public method
+    Transaction t(*this);
+    Recycle(0, "");
+    t.Commit(0);
+  }
+
+
+  bool ServerIndex::IsProtectedPatient(const std::string& publicId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    // Lookup for the requested resource
+    int64_t id;
+    ResourceType type;
+    if (!db_.LookupResource(id, type, publicId) ||
+        type != ResourceType_Patient)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    return db_.IsProtectedPatient(id);
+  }
+     
+
+  void ServerIndex::SetProtectedPatient(const std::string& publicId,
+                                        bool isProtected)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    Transaction transaction(*this);
+
+    // Lookup for the requested resource
+    int64_t id;
+    ResourceType type;
+    if (!db_.LookupResource(id, type, publicId) ||
+        type != ResourceType_Patient)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    db_.SetProtectedPatient(id, isProtected);
+    transaction.Commit(0);
+
+    if (isProtected)
+      LOG(INFO) << "Patient " << publicId << " has been protected";
+    else
+      LOG(INFO) << "Patient " << publicId << " has been unprotected";
+  }
+
+
+  void ServerIndex::GetChildren(std::list<std::string>& result,
+                                const std::string& publicId)
+  {
+    result.clear();
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    ResourceType type;
+    int64_t resource;
+    if (!db_.LookupResource(resource, type, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    if (type == ResourceType_Instance)
+    {
+      // An instance cannot have a child
+      throw OrthancException(ErrorCode_BadParameterType);
+    }
+
+    std::list<int64_t> tmp;
+    db_.GetChildrenInternalId(tmp, resource);
+
+    for (std::list<int64_t>::const_iterator 
+           it = tmp.begin(); it != tmp.end(); ++it)
+    {
+      result.push_back(db_.GetPublicId(*it));
+    }
+  }
+
+
+  void ServerIndex::GetChildInstances(std::list<std::string>& result,
+                                      const std::string& publicId)
+  {
+    result.clear();
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    ResourceType type;
+    int64_t top;
+    if (!db_.LookupResource(top, type, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    if (type == ResourceType_Instance)
+    {
+      // The resource is already an instance: Do not go down the hierarchy
+      result.push_back(publicId);
+      return;
+    }
+
+    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();
+
+      if (db_.GetResourceType(resource) == ResourceType_Instance)
+      {
+        result.push_back(db_.GetPublicId(resource));
+      }
+      else
+      {
+        // Tag all the children of this resource as to be explored
+        db_.GetChildrenInternalId(tmp, resource);
+        for (std::list<int64_t>::const_iterator 
+               it = tmp.begin(); it != tmp.end(); ++it)
+        {
+          toExplore.push(*it);
+        }
+      }
+    }
+  }
+
+
+  void ServerIndex::SetMetadata(const std::string& publicId,
+                                MetadataType type,
+                                const std::string& value)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    Transaction t(*this);
+
+    ResourceType rtype;
+    int64_t id;
+    if (!db_.LookupResource(id, rtype, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    db_.SetMetadata(id, type, value);
+
+    if (IsUserMetadata(type))
+    {
+      LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId);
+    }
+
+    t.Commit(0);
+  }
+
+
+  void ServerIndex::DeleteMetadata(const std::string& publicId,
+                                   MetadataType type)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    Transaction t(*this);
+
+    ResourceType rtype;
+    int64_t id;
+    if (!db_.LookupResource(id, rtype, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    db_.DeleteMetadata(id, type);
+
+    if (IsUserMetadata(type))
+    {
+      LogChange(id, ChangeType_UpdatedMetadata, rtype, publicId);
+    }
+
+    t.Commit(0);
+  }
+
+
+  bool ServerIndex::LookupMetadata(std::string& target,
+                                   const std::string& publicId,
+                                   MetadataType type)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    ResourceType rtype;
+    int64_t id;
+    if (!db_.LookupResource(id, rtype, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    return db_.LookupMetadata(target, id, type);
+  }
+
+
+  void ServerIndex::GetAllMetadata(std::map<MetadataType, std::string>& target,
+                                   const std::string& publicId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    ResourceType type;
+    int64_t id;
+    if (!db_.LookupResource(id, type, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    return db_.GetAllMetadata(target, id);
+  }
+
+
+  void ServerIndex::ListAvailableAttachments(std::list<FileContentType>& target,
+                                             const std::string& publicId,
+                                             ResourceType expectedType)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    ResourceType type;
+    int64_t id;
+    if (!db_.LookupResource(id, type, publicId) ||
+        expectedType != type)
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    db_.ListAvailableAttachments(target, id);
+  }
+
+
+  bool ServerIndex::LookupParent(std::string& target,
+                                 const std::string& publicId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    ResourceType type;
+    int64_t id;
+    if (!db_.LookupResource(id, type, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    int64_t parentId;
+    if (db_.LookupParent(parentId, id))
+    {
+      target = db_.GetPublicId(parentId);
+      return true;
+    }
+    else
+    {
+      return false;
+    }
+  }
+
+
+  uint64_t ServerIndex::IncrementGlobalSequence(GlobalProperty sequence)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    Transaction transaction(*this);
+
+    uint64_t seq = IncrementGlobalSequenceInternal(sequence);
+    transaction.Commit(0);
+
+    return seq;
+  }
+
+
+
+  void ServerIndex::LogChange(ChangeType changeType,
+                              const std::string& publicId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    Transaction transaction(*this);
+
+    int64_t id;
+    ResourceType type;
+    if (!db_.LookupResource(id, type, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    LogChange(id, changeType, type, publicId);
+    transaction.Commit(0);
+  }
+
+
+  void ServerIndex::DeleteChanges()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Transaction transaction(*this);
+    db_.ClearChanges();
+    transaction.Commit(0);
+  }
+
+  void ServerIndex::DeleteExportedResources()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Transaction transaction(*this);
+    db_.ClearExportedResources();
+    transaction.Commit(0);
+  }
+
+
+  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)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    int64_t top;
+    if (!db_.LookupResource(top, type, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    std::stack<int64_t> toExplore;
+    toExplore.push(top);
+
+    countInstances = 0;
+    countSeries = 0;
+    countStudies = 0;
+    diskSize = 0;
+    uncompressedSize = 0;
+    dicomDiskSize = 0;
+    dicomUncompressedSize = 0;
+
+    while (!toExplore.empty())
+    {
+      // Get the internal ID of the current resource
+      int64_t resource = toExplore.top();
+      toExplore.pop();
+
+      ResourceType thisType = db_.GetResourceType(resource);
+
+      std::list<FileContentType> f;
+      db_.ListAvailableAttachments(f, resource);
+
+      for (std::list<FileContentType>::const_iterator
+             it = f.begin(); it != f.end(); ++it)
+      {
+        FileInfo attachment;
+        if (db_.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;
+        db_.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;
+    }
+  }
+
+
+  void ServerIndex::UnstableResourcesMonitorThread(ServerIndex* that,
+                                                   unsigned int threadSleep)
+  {
+    int stableAge;
+    
+    {
+      OrthancConfiguration::ReaderLock lock;
+      stableAge = lock.GetConfiguration().GetUnsignedIntegerParameter("StableAge", 60);
+    }
+
+    if (stableAge <= 0)
+    {
+      stableAge = 60;
+    }
+
+    LOG(INFO) << "Starting the monitor for stable resources (stable age = " << stableAge << ")";
+
+    while (!that->done_)
+    {
+      // Check for stable resources each few seconds
+      boost::this_thread::sleep(boost::posix_time::milliseconds(threadSleep));
+
+      boost::mutex::scoped_lock lock(that->mutex_);
+
+      while (!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.
+          
+        UnstableResourcePayload payload;
+        int64_t id = that->unstableResources_.RemoveOldest(payload);
+
+        // Ensure that the resource is still existing before logging the change
+        if (that->db_.IsExistingResource(id))
+        {
+          switch (payload.GetResourceType())
+          {
+            case ResourceType_Patient:
+              that->LogChange(id, ChangeType_StablePatient, ResourceType_Patient, payload.GetPublicId());
+              break;
+
+            case ResourceType_Study:
+              that->LogChange(id, ChangeType_StableStudy, ResourceType_Study, payload.GetPublicId());
+              break;
+
+            case ResourceType_Series:
+              that->LogChange(id, ChangeType_StableSeries, ResourceType_Series, payload.GetPublicId());
+              break;
+
+            default:
+              throw OrthancException(ErrorCode_InternalError);
+          }
+
+          //LOG(INFO) << "Stable resource: " << EnumerationToString(payload.type_) << " " << id;
+        }
+      }
+    }
+
+    LOG(INFO) << "Closing the monitor thread for stable resources";
+  }
+  
+
+  void ServerIndex::MarkAsUnstable(int64_t id,
+                                   Orthanc::ResourceType type,
+                                   const std::string& publicId)
+  {
+    // WARNING: Before calling this method, "mutex_" must be locked.
+
+    assert(type == Orthanc::ResourceType_Patient ||
+           type == Orthanc::ResourceType_Study ||
+           type == Orthanc::ResourceType_Series);
+
+    UnstableResourcePayload payload(type, publicId);
+    unstableResources_.AddOrMakeMostRecent(id, payload);
+    //LOG(INFO) << "Unstable resource: " << EnumerationToString(type) << " " << id;
+
+    LogChange(id, ChangeType_NewChildInstance, type, publicId);
+  }
+
+
+
+  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));
+
+    std::list<std::string> tmp;
+    
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+      db_.ApplyLookupResources(tmp, NULL, query, level, 0);
+    }
+
+    CopyListToVector(result, tmp);
+  }
+
+
+  StoreStatus ServerIndex::AddAttachment(const FileInfo& attachment,
+                                         const std::string& publicId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Transaction t(*this);
+
+    ResourceType resourceType;
+    int64_t resourceId;
+    if (!db_.LookupResource(resourceId, resourceType, publicId))
+    {
+      return StoreStatus_Failure;  // Inexistent resource
+    }
+
+    // Remove possible previous attachment
+    db_.DeleteAttachment(resourceId, attachment.GetContentType());
+
+    // Locate the patient of the target resource
+    int64_t patientId = resourceId;
+    for (;;)
+    {
+      int64_t parent;
+      if (db_.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(db_.GetResourceType(patientId) == ResourceType_Patient);
+    Recycle(attachment.GetCompressedSize(), db_.GetPublicId(patientId));
+
+    db_.AddAttachment(resourceId, attachment);
+
+    if (IsUserContentType(attachment.GetContentType()))
+    {
+      LogChange(resourceId, ChangeType_UpdatedAttachment, resourceType, publicId);
+    }
+
+    t.Commit(attachment.GetCompressedSize());
+
+    return StoreStatus_Success;
+  }
+
+
+  void ServerIndex::DeleteAttachment(const std::string& publicId,
+                                     FileContentType type)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    Transaction t(*this);
+
+    ResourceType rtype;
+    int64_t id;
+    if (!db_.LookupResource(id, rtype, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    db_.DeleteAttachment(id, type);
+
+    if (IsUserContentType(type))
+    {
+      LogChange(id, ChangeType_UpdatedAttachment, rtype, publicId);
+    }
+
+    t.Commit(0);
+  }
+
+
+  void ServerIndex::SetGlobalProperty(GlobalProperty property,
+                                      const std::string& value)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    Transaction transaction(*this);
+    db_.SetGlobalProperty(property, value);
+    transaction.Commit(0);
+  }
+
+
+  bool ServerIndex::LookupGlobalProperty(std::string& value,
+                                         GlobalProperty property)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    return db_.LookupGlobalProperty(value, property);
+  }
+  
+
+  std::string ServerIndex::GetGlobalProperty(GlobalProperty property,
+                                             const std::string& defaultValue)
+  {
+    std::string value;
+
+    if (LookupGlobalProperty(value, property))
+    {
+      return value;
+    }
+    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);
+    }
+
+    result.Clear();
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    // Lookup for the requested resource
+    int64_t id;
+    ResourceType type;
+    if (!db_.LookupResource(id, type, publicId) ||
+        type != expectedType)
+    {
+      return false;
+    }
+
+    if (type == ResourceType_Study)
+    {
+      DicomMap tmp;
+      db_.GetMainDicomTags(tmp, id);
+
+      switch (levelOfInterest)
+      {
+        case ResourceType_Patient:
+          tmp.ExtractPatientInformation(result);
+          return true;
+
+        case ResourceType_Study:
+          tmp.ExtractStudyInformation(result);
+          return true;
+
+        default:
+          throw OrthancException(ErrorCode_InternalError);
+      }
+    }
+    else
+    {
+      db_.GetMainDicomTags(result, id);
+      return true;
+    }    
+  }
+
+
+  bool ServerIndex::GetAllMainDicomTags(DicomMap& result,
+                                        const std::string& instancePublicId)
+  {
+    result.Clear();
+    
+    boost::mutex::scoped_lock lock(mutex_);
+
+    // Lookup for the requested resource
+    int64_t instance;
+    ResourceType type;
+    if (!db_.LookupResource(instance, type, instancePublicId) ||
+        type != ResourceType_Instance)
+    {
+      return false;
+    }
+    else
+    {
+      DicomMap tmp;
+
+      db_.GetMainDicomTags(tmp, instance);
+      result.Merge(tmp);
+
+      int64_t series;
+      if (!db_.LookupParent(series, instance))
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      tmp.Clear();
+      db_.GetMainDicomTags(tmp, series);
+      result.Merge(tmp);
+
+      int64_t study;
+      if (!db_.LookupParent(study, series))
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      tmp.Clear();
+      db_.GetMainDicomTags(tmp, study);
+      result.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 (!db_.LookupParent(patient, study))
+        {
+          throw OrthancException(ErrorCode_InternalError);
+        }
+
+        tmp.Clear();
+        db_.GetMainDicomTags(tmp, study);
+
+        std::set<DicomTag> patientTags;
+        tmp.GetTags(patientTags);
+
+        for (std::set<DicomTag>::const_iterator
+               it = patientTags.begin(); it != patientTags.end(); ++it)
+        {
+          assert(result.HasTag(*it));
+        }
+      }
+#endif
+      
+      return true;
+    }
+  }
+
+
+  bool ServerIndex::LookupResourceType(ResourceType& type,
+                                       const std::string& publicId)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    int64_t id;
+    return db_.LookupResource(id, type, publicId);
+  }
+
+
+  unsigned int ServerIndex::GetDatabaseVersion()
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    return db_.GetDatabaseVersion();
+  }
+
+
+  bool ServerIndex::LookupParent(std::string& target,
+                                 const std::string& publicId,
+                                 ResourceType parentType)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    ResourceType type;
+    int64_t id;
+    if (!db_.LookupResource(id, type, publicId))
+    {
+      throw OrthancException(ErrorCode_UnknownResource);
+    }
+
+    while (type != parentType)
+    {
+      int64_t parentId;
+
+      if (type == ResourceType_Patient ||    // Cannot further go up in hierarchy
+          !db_.LookupParent(parentId, id))
+      {
+        return false;
+      }
+
+      id = parentId;
+      type = GetParentResourceType(type);
+    }
+
+    target = db_.GetPublicId(id);
+    return true;
+  }
+
+
+  void ServerIndex::ReconstructInstance(ParsedDicomFile& dicom)
+  {
+    DicomMap summary;
+    dicom.ExtractDicomSummary(summary);
+
+    DicomInstanceHasher hasher(summary);
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    try
+    {
+      Transaction t(*this);
+
+      int64_t patient = -1, study = -1, series = -1, instance = -1;
+
+      ResourceType dummy;      
+      if (!db_.LookupResource(patient, dummy, hasher.HashPatient()) ||
+          !db_.LookupResource(study, dummy, hasher.HashStudy()) ||
+          !db_.LookupResource(series, dummy, hasher.HashSeries()) ||
+          !db_.LookupResource(instance, dummy, hasher.HashInstance()) ||
+          patient == -1 ||
+          study == -1 ||
+          series == -1 ||
+          instance == -1)
+      {
+        throw OrthancException(ErrorCode_InternalError);
+      }
+
+      db_.ClearMainDicomTags(patient);
+      db_.ClearMainDicomTags(study);
+      db_.ClearMainDicomTags(series);
+      db_.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);
+        db_.SetResourcesContent(content);
+      }
+
+      {
+        std::string s;
+        if (dicom.LookupTransferSyntax(s))
+        {
+          db_.SetMetadata(instance, MetadataType_Instance_TransferSyntax, s);
+        }
+      }
+
+      const DicomValue* value;
+      if ((value = summary.TestAndGetValue(DICOM_TAG_SOP_CLASS_UID)) != NULL &&
+          !value->IsNull() &&
+          !value->IsBinary())
+      {
+        db_.SetMetadata(instance, MetadataType_Instance_SopClassUid, value->GetContent());
+      }
+
+      t.Commit(0);  // No change in the DB size
+    }
+    catch (OrthancException& e)
+    {
+      LOG(ERROR) << "EXCEPTION [" << e.What() << "]";
+    }
+  }
+
+
+  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));
+      }
+    }
+  }
+
+
+  void ServerIndex::ApplyLookupResources(std::vector<std::string>& resourcesId,
+                                         std::vector<std::string>* instancesId,
+                                         const DatabaseLookup& lookup,
+                                         ResourceType queryLevel,
+                                         size_t limit)
+  {
+    std::vector<DatabaseConstraint> normalized;
+    NormalizeLookup(normalized, lookup, queryLevel);
+
+    std::list<std::string> resourcesList, instancesList;
+    
+    {
+      boost::mutex::scoped_lock lock(mutex_);
+
+      if (instancesId == NULL)
+      {
+        db_.ApplyLookupResources(resourcesList, NULL, normalized, queryLevel, limit);
+      }
+      else
+      {
+        db_.ApplyLookupResources(resourcesList, &instancesList, normalized, queryLevel, limit);
+      }
+    }
+
+    CopyListToVector(resourcesId, resourcesList);
+
+    if (instancesId != NULL)
+    { 
+      CopyListToVector(*instancesId, instancesList);
+    }
+  }
+}