changeset 1370:7b6f5115607f query-retrieve

integration mainline->query-retrieve
author Sebastien Jodogne <s.jodogne@gmail.com>
date Wed, 27 May 2015 12:32:43 +0200
parents b22ba8c5edbe (diff) 4460e2622016 (current diff)
children f528849ee9f7
files
diffstat 28 files changed, 1178 insertions(+), 240 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Wed May 27 10:50:59 2015 +0200
+++ b/CMakeLists.txt	Wed May 27 12:32:43 2015 +0200
@@ -70,6 +70,7 @@
 
 set(ORTHANC_CORE_SOURCES
   Core/Cache/MemoryCache.cpp
+  Core/Cache/SharedArchive.cpp
   Core/ChunkedBuffer.cpp
   Core/Compression/BufferCompressor.cpp
   Core/Compression/ZlibCompressor.cpp
@@ -172,6 +173,7 @@
   OrthancServer/ExportedResource.cpp
   OrthancServer/ResourceFinder.cpp
   OrthancServer/DicomFindQuery.cpp
+  OrthancServer/QueryRetrieveHandler.cpp
 
   # From "lua-scripting" branch
   OrthancServer/DicomInstanceToStore.cpp
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Cache/SharedArchive.cpp	Wed May 27 12:32:43 2015 +0200
@@ -0,0 +1,134 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "../PrecompiledHeaders.h"
+#include "SharedArchive.h"
+
+
+#include "../Uuid.h"
+
+
+namespace Orthanc
+{
+  void SharedArchive::RemoveInternal(const std::string& id)
+  {
+    Archive::iterator it = archive_.find(id);
+
+    if (it != archive_.end())
+    {
+      delete it->second;
+      archive_.erase(it);
+    }
+  }
+
+
+  SharedArchive::Accessor::Accessor(SharedArchive& that,
+                                    const std::string& id) :
+    lock_(that.mutex_)
+  {
+    Archive::iterator it = that.archive_.find(id);
+
+    if (it == that.archive_.end())
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+    else
+    {
+      that.lru_.MakeMostRecent(id);
+      item_ = it->second;
+    }
+  }
+
+
+  SharedArchive::SharedArchive(size_t maxSize) : 
+    maxSize_(maxSize)
+  {
+    if (maxSize == 0)
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+  }
+
+
+  SharedArchive::~SharedArchive()
+  {
+    for (Archive::iterator it = archive_.begin();
+         it != archive_.end(); it++)
+    {
+      delete it->second;
+    }
+  }
+
+
+  std::string SharedArchive::Add(IDynamicObject* obj)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+
+    if (archive_.size() == maxSize_)
+    {
+      // The quota has been reached, remove the oldest element
+      std::string oldest = lru_.RemoveOldest();
+      RemoveInternal(oldest);
+    }
+
+    std::string id = Toolbox::GenerateUuid();
+    RemoveInternal(id);  // Should never be useful because of UUID
+    archive_[id] = obj;
+    lru_.Add(id);
+
+    return id;
+  }
+
+
+  void SharedArchive::Remove(const std::string& id)
+  {
+    boost::mutex::scoped_lock lock(mutex_);
+    RemoveInternal(id);      
+    lru_.Invalidate(id);
+  }
+
+
+  void SharedArchive::List(std::list<std::string>& items)
+  {
+    items.clear();
+
+    boost::mutex::scoped_lock lock(mutex_);
+
+    for (Archive::const_iterator it = archive_.begin();
+         it != archive_.end(); it++)
+    {
+      items.push_back(it->first);
+    }
+  }
+}
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/Cache/SharedArchive.h	Wed May 27 12:32:43 2015 +0200
@@ -0,0 +1,85 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "LeastRecentlyUsedIndex.h"
+#include "../IDynamicObject.h"
+
+#include <map>
+#include <boost/thread.hpp>
+
+namespace Orthanc
+{
+  class SharedArchive : public boost::noncopyable
+  {
+  private:
+    typedef std::map<std::string, IDynamicObject*>  Archive;
+
+    size_t         maxSize_;
+    boost::mutex   mutex_;
+    Archive        archive_;
+    Orthanc::LeastRecentlyUsedIndex<std::string> lru_;
+
+    void RemoveInternal(const std::string& id);
+
+  public:
+    class Accessor : public boost::noncopyable
+    {
+    private:
+      boost::mutex::scoped_lock  lock_;
+      IDynamicObject*            item_;
+
+    public:
+      Accessor(SharedArchive& that,
+               const std::string& id);
+
+      IDynamicObject& GetItem() const
+      {
+        return *item_;
+      }      
+    };
+
+
+    SharedArchive(size_t maxSize);
+
+    ~SharedArchive();
+
+    std::string Add(IDynamicObject* obj);  // Takes the ownership
+
+    void Remove(const std::string& id);
+
+    void List(std::list<std::string>& items);
+  };
+}
+
+
--- a/Core/DicomFormat/DicomTag.cpp	Wed May 27 10:50:59 2015 +0200
+++ b/Core/DicomFormat/DicomTag.cpp	Wed May 27 12:32:43 2015 +0200
@@ -118,11 +118,10 @@
   }
 
 
-  void DicomTag::GetTagsForModule(std::set<DicomTag>& target,
+  void DicomTag::AddTagsForModule(std::set<DicomTag>& target,
                                   DicomModule module)
   {
     // REFERENCE: 11_03pu.pdf, DICOM PS 3.3 2011 - Information Object Definitions
-    target.clear();
 
     switch (module)
     {
--- a/Core/DicomFormat/DicomTag.h	Wed May 27 10:50:59 2015 +0200
+++ b/Core/DicomFormat/DicomTag.h	Wed May 27 12:32:43 2015 +0200
@@ -84,7 +84,7 @@
 
     friend std::ostream& operator<< (std::ostream& o, const DicomTag& tag);
 
-    static void GetTagsForModule(std::set<DicomTag>& target,
+    static void AddTagsForModule(std::set<DicomTag>& target,
                                  DicomModule module);
 
     bool IsIdentifier() const;
--- a/Core/RestApi/RestApi.cpp	Wed May 27 10:50:59 2015 +0200
+++ b/Core/RestApi/RestApi.cpp	Wed May 27 12:32:43 2015 +0200
@@ -163,7 +163,7 @@
                        const GetArguments& getArguments,
                        const std::string& postData)
   {
-    RestApiOutput wrappedOutput(output);
+    RestApiOutput wrappedOutput(output, method);
 
 #if ORTHANC_PUGIXML_ENABLED == 1
     // Look if the user wishes XML answers instead of JSON
--- a/Core/RestApi/RestApiCall.cpp	Wed May 27 10:50:59 2015 +0200
+++ b/Core/RestApi/RestApiCall.cpp	Wed May 27 12:32:43 2015 +0200
@@ -41,4 +41,17 @@
     Json::Reader reader;
     return reader.parse(request, result);
   }
+
+
+  std::string RestApiCall::FlattenUri() const
+  {
+    std::string s = "/";
+
+    for (size_t i = 0; i < fullUri_.size(); i++)
+    {
+      s += fullUri_[i] + "/";
+    }
+
+    return s;
+  }
 }
--- a/Core/RestApi/RestApiCall.h	Wed May 27 10:50:59 2015 +0200
+++ b/Core/RestApi/RestApiCall.h	Wed May 27 12:32:43 2015 +0200
@@ -114,6 +114,8 @@
       HttpHandler::ParseCookies(result, httpHeaders_);
     }
 
+    std::string FlattenUri() const;
+
     virtual bool ParseJsonRequest(Json::Value& result) const = 0;
   };
 }
--- a/Core/RestApi/RestApiOutput.cpp	Wed May 27 10:50:59 2015 +0200
+++ b/Core/RestApi/RestApiOutput.cpp	Wed May 27 12:32:43 2015 +0200
@@ -40,8 +40,10 @@
 
 namespace Orthanc
 {
-  RestApiOutput::RestApiOutput(HttpOutput& output) : 
+  RestApiOutput::RestApiOutput(HttpOutput& output,
+                               HttpMethod method) : 
     output_(output),
+    method_(method),
     convertJsonToXml_(false)
   {
     alreadySent_ = false;
@@ -55,7 +57,14 @@
   {
     if (!alreadySent_)
     {
-      output_.SendStatus(HttpStatus_404_NotFound);
+      if (method_ == HttpMethod_Post)
+      {
+        output_.SendStatus(HttpStatus_400_BadRequest);
+      }
+      else
+      {
+        output_.SendStatus(HttpStatus_404_NotFound);
+      }
     }
   }
   
--- a/Core/RestApi/RestApiOutput.h	Wed May 27 10:50:59 2015 +0200
+++ b/Core/RestApi/RestApiOutput.h	Wed May 27 12:32:43 2015 +0200
@@ -43,13 +43,15 @@
   {
   private:
     HttpOutput& output_;
-    bool alreadySent_;
-    bool convertJsonToXml_;
+    HttpMethod  method_;
+    bool        alreadySent_;
+    bool        convertJsonToXml_;
 
     void CheckStatus();
 
   public:
-    RestApiOutput(HttpOutput& output);
+    RestApiOutput(HttpOutput& output,
+                  HttpMethod method);
 
     ~RestApiOutput();
 
--- a/OrthancServer/DicomProtocol/DicomFindAnswers.cpp	Wed May 27 10:50:59 2015 +0200
+++ b/OrthancServer/DicomProtocol/DicomFindAnswers.cpp	Wed May 27 12:32:43 2015 +0200
@@ -53,14 +53,15 @@
     }
   }
 
-  void DicomFindAnswers::ToJson(Json::Value& target) const
+  void DicomFindAnswers::ToJson(Json::Value& target,
+                                bool simplify) const
   {
     target = Json::arrayValue;
 
     for (size_t i = 0; i < GetSize(); i++)
     {
       Json::Value answer(Json::objectValue);
-      FromDcmtkBridge::ToJson(answer, GetAnswer(i));
+      FromDcmtkBridge::ToJson(answer, GetAnswer(i), simplify);
       target.append(answer);
     }
   }
--- a/OrthancServer/DicomProtocol/DicomFindAnswers.h	Wed May 27 10:50:59 2015 +0200
+++ b/OrthancServer/DicomProtocol/DicomFindAnswers.h	Wed May 27 12:32:43 2015 +0200
@@ -69,6 +69,7 @@
       return *items_.at(index);
     }
 
-    void ToJson(Json::Value& target) const;
+    void ToJson(Json::Value& target,
+                bool simplify) const;
   };
 }
--- a/OrthancServer/DicomProtocol/DicomUserConnection.cpp	Wed May 27 10:50:59 2015 +0200
+++ b/OrthancServer/DicomProtocol/DicomUserConnection.cpp	Wed May 27 12:32:43 2015 +0200
@@ -84,6 +84,7 @@
 #include "../../Core/OrthancException.h"
 #include "../ToDcmtkBridge.h"
 #include "../FromDcmtkBridge.h"
+#include "../../Core/DicomFormat/DicomArray.h"
 
 #include <dcmtk/dcmdata/dcistrmb.h>
 #include <dcmtk/dcmdata/dcistrmf.h>
@@ -337,6 +338,16 @@
   }
 
 
+  namespace
+  {
+    struct FindPayload
+    {
+      DicomFindAnswers* answers;
+      std::string       level;
+    };
+  }
+
+
   static void FindCallback(
     /* in */
     void *callbackData,
@@ -346,73 +357,96 @@
     DcmDataset *responseIdentifiers /* pending response identifiers */
     )
   {
-    DicomFindAnswers& answers = *reinterpret_cast<DicomFindAnswers*>(callbackData);
+    FindPayload& payload = *reinterpret_cast<FindPayload*>(callbackData);
 
     if (responseIdentifiers != NULL)
     {
       DicomMap m;
       FromDcmtkBridge::Convert(m, *responseIdentifiers);
-      answers.Add(m);
+
+      if (!m.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
+      {
+        m.SetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL, payload.level);
+      }
+
+      payload.answers->Add(m);
     }
   }
 
+
+  static void CheckFindQuery(ResourceType level,
+                             const DicomMap& fields)
+  {
+    std::set<DicomTag> allowedTags;
+
+    // WARNING: Do not add "break" or reorder items in this switch-case!
+    switch (level)
+    {
+      case ResourceType_Instance:
+        DicomTag::AddTagsForModule(allowedTags, DicomModule_Instance);
+
+      case ResourceType_Series:
+        DicomTag::AddTagsForModule(allowedTags, DicomModule_Series);
+
+      case ResourceType_Study:
+        DicomTag::AddTagsForModule(allowedTags, DicomModule_Study);
+
+      case ResourceType_Patient:
+        DicomTag::AddTagsForModule(allowedTags, DicomModule_Patient);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    DicomArray query(fields);
+    for (size_t i = 0; i < query.GetSize(); i++)
+    {
+      const DicomTag& tag = query.GetElement(i).GetTag();
+      if (allowedTags.find(tag) == allowedTags.end())
+      {
+        LOG(ERROR) << "Tag not allowed for this C-Find level: " << tag;
+        throw OrthancException(ErrorCode_BadRequest);
+      }
+    }
+  }
+
+
   void DicomUserConnection::Find(DicomFindAnswers& result,
-                                 FindRootModel model,
+                                 ResourceType level,
                                  const DicomMap& fields)
   {
+    CheckFindQuery(level, fields);
+
     CheckIsOpen();
 
+    FindPayload payload;
+    payload.answers = &result;
+
     const char* sopClass;
     std::auto_ptr<DcmDataset> dataset(ToDcmtkBridge::Convert(fields));
-    switch (model)
+    switch (level)
     {
-      case FindRootModel_Patient:
+      case ResourceType_Patient:
+        payload.level = "PATIENT";
         DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "PATIENT");
         sopClass = UID_FINDPatientRootQueryRetrieveInformationModel;
-      
-        // Accession number
-        if (!fields.HasTag(0x0008, 0x0050))
-          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0050), "");
-
-        // Patient ID
-        if (!fields.HasTag(0x0010, 0x0020))
-          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0010, 0x0020), "");
-
         break;
 
-      case FindRootModel_Study:
+      case ResourceType_Study:
+        payload.level = "STUDY";
         DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "STUDY");
         sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
-
-        // Accession number
-        if (!fields.HasTag(0x0008, 0x0050))
-          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0050), "");
-
-        // Study instance UID
-        if (!fields.HasTag(0x0020, 0x000d))
-          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0020, 0x000d), "");
-
         break;
 
-      case FindRootModel_Series:
+      case ResourceType_Series:
+        payload.level = "SERIES";
         DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "SERIES");
         sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
-
-        // Accession number
-        if (!fields.HasTag(0x0008, 0x0050))
-          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0050), "");
-
-        // Study instance UID
-        if (!fields.HasTag(0x0020, 0x000d))
-          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0020, 0x000d), "");
-
-        // Series instance UID
-        if (!fields.HasTag(0x0020, 0x000e))
-          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0020, 0x000e), "");
-
         break;
 
-      case FindRootModel_Instance:
+      case ResourceType_Instance:
+        payload.level = "INSTANCE";
         if (manufacturer_ == ModalityManufacturer_ClearCanvas ||
             manufacturer_ == ModalityManufacturer_Dcm4Chee)
         {
@@ -427,7 +461,27 @@
         }
 
         sopClass = UID_FINDStudyRootQueryRetrieveInformationModel;
+        break;
 
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    // Add the expected tags for this query level.
+    // WARNING: Do not reorder or add "break" in this switch-case!
+    switch (level)
+    {
+      case ResourceType_Instance:
+        // SOP Instance UID
+        if (!fields.HasTag(0x0008, 0x0018))
+          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0018), "");
+
+      case ResourceType_Series:
+        // Series instance UID
+        if (!fields.HasTag(0x0020, 0x000e))
+          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0020, 0x000e), "");
+
+      case ResourceType_Study:
         // Accession number
         if (!fields.HasTag(0x0008, 0x0050))
           DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0050), "");
@@ -436,13 +490,10 @@
         if (!fields.HasTag(0x0020, 0x000d))
           DU_putStringDOElement(dataset.get(), DcmTagKey(0x0020, 0x000d), "");
 
-        // Series instance UID
-        if (!fields.HasTag(0x0020, 0x000e))
-          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0020, 0x000e), "");
-
-        // SOP Instance UID
-        if (!fields.HasTag(0x0008, 0x0018))
-          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0018), "");
+      case ResourceType_Patient:
+        // Patient ID
+        if (!fields.HasTag(0x0010, 0x0020))
+          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0010, 0x0020), "");
 
         break;
 
@@ -467,7 +518,7 @@
     T_DIMSE_C_FindRSP response;
     DcmDataset* statusDetail = NULL;
     OFCondition cond = DIMSE_findUser(pimpl_->assoc_, presID, &request, dataset.get(),
-                                      FindCallback, &result,
+                                      FindCallback, &payload,
                                       /*opt_blockMode*/ DIMSE_BLOCKING, 
                                       /*opt_dimse_timeout*/ pimpl_->dimseTimeout_,
                                       &response, &statusDetail);
@@ -481,62 +532,9 @@
   }
 
 
-  void DicomUserConnection::FindPatient(DicomFindAnswers& result,
-                                        const DicomMap& fields)
-  {
-    // Only keep the filters from "fields" that are related to the patient
-    DicomMap s;
-    fields.ExtractPatientInformation(s);
-    Find(result, FindRootModel_Patient, s);
-  }
-
-  void DicomUserConnection::FindStudy(DicomFindAnswers& result,
-                                      const DicomMap& fields)
-  {
-    // Only keep the filters from "fields" that are related to the study
-    DicomMap s;
-    fields.ExtractStudyInformation(s);
-
-    s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID);
-    s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER);
-    s.CopyTagIfExists(fields, DICOM_TAG_MODALITIES_IN_STUDY);
-
-    Find(result, FindRootModel_Study, s);
-  }
-
-  void DicomUserConnection::FindSeries(DicomFindAnswers& result,
-                                       const DicomMap& fields)
-  {
-    // Only keep the filters from "fields" that are related to the series
-    DicomMap s;
-    fields.ExtractSeriesInformation(s);
-
-    s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID);
-    s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER);
-    s.CopyTagIfExists(fields, DICOM_TAG_STUDY_INSTANCE_UID);
-
-    Find(result, FindRootModel_Series, s);
-  }
-
-  void DicomUserConnection::FindInstance(DicomFindAnswers& result,
+  void DicomUserConnection::MoveInternal(const std::string& targetAet,
                                          const DicomMap& fields)
   {
-    // Only keep the filters from "fields" that are related to the instance
-    DicomMap s;
-    fields.ExtractInstanceInformation(s);
-
-    s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID);
-    s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER);
-    s.CopyTagIfExists(fields, DICOM_TAG_STUDY_INSTANCE_UID);
-    s.CopyTagIfExists(fields, DICOM_TAG_SERIES_INSTANCE_UID);
-
-    Find(result, FindRootModel_Instance, s);
-  }
-
-
-  void DicomUserConnection::Move(const std::string& targetAet,
-                                 const DicomMap& fields)
-  {
     CheckIsOpen();
 
     const char* sopClass = UID_MOVEStudyRootQueryRetrieveInformationModel;
@@ -830,33 +828,86 @@
   }
 
 
-  void DicomUserConnection::MoveSeries(const std::string& targetAet,
-                                       const DicomMap& findResult)
+  static void TestAndCopyTag(DicomMap& result,
+                             const DicomMap& source,
+                             const DicomTag& tag)
+  {
+    if (!source.HasTag(tag))
+    {
+      throw OrthancException(ErrorCode_BadRequest);
+    }
+    else
+    {
+      result.SetValue(tag, source.GetValue(tag));
+    }
+  }
+
+
+  void DicomUserConnection::Move(const std::string& targetAet,
+                                 const DicomMap& findResult)
   {
-    DicomMap simplified;
-    simplified.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, findResult.GetValue(DICOM_TAG_STUDY_INSTANCE_UID));
-    simplified.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, findResult.GetValue(DICOM_TAG_SERIES_INSTANCE_UID));
-    Move(targetAet, simplified);
+    if (!findResult.HasTag(DICOM_TAG_QUERY_RETRIEVE_LEVEL))
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
+    const std::string tmp = findResult.GetValue(DICOM_TAG_QUERY_RETRIEVE_LEVEL).AsString();
+    ResourceType level = StringToResourceType(tmp.c_str());
+
+    DicomMap move;
+    switch (level)
+    {
+      case ResourceType_Patient:
+        TestAndCopyTag(move, findResult, DICOM_TAG_PATIENT_ID);
+        break;
+
+      case ResourceType_Study:
+        TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
+        break;
+
+      case ResourceType_Series:
+        TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
+        TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
+        break;
+
+      case ResourceType_Instance:
+        TestAndCopyTag(move, findResult, DICOM_TAG_STUDY_INSTANCE_UID);
+        TestAndCopyTag(move, findResult, DICOM_TAG_SERIES_INSTANCE_UID);
+        TestAndCopyTag(move, findResult, DICOM_TAG_SOP_INSTANCE_UID);
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_InternalError);
+    }
+
+    MoveInternal(targetAet, move);
+  }
+
+
+  void DicomUserConnection::MovePatient(const std::string& targetAet,
+                                        const std::string& patientId)
+  {
+    DicomMap query;
+    query.SetValue(DICOM_TAG_PATIENT_ID, patientId);
+    MoveInternal(targetAet, query);
+  }
+
+  void DicomUserConnection::MoveStudy(const std::string& targetAet,
+                                      const std::string& studyUid)
+  {
+    DicomMap query;
+    query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid);
+    MoveInternal(targetAet, query);
   }
 
   void DicomUserConnection::MoveSeries(const std::string& targetAet,
                                        const std::string& studyUid,
                                        const std::string& seriesUid)
   {
-    DicomMap map;
-    map.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid);
-    map.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid);
-    Move(targetAet, map);
-  }
-
-  void DicomUserConnection::MoveInstance(const std::string& targetAet,
-                                         const DicomMap& findResult)
-  {
-    DicomMap simplified;
-    simplified.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, findResult.GetValue(DICOM_TAG_STUDY_INSTANCE_UID));
-    simplified.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, findResult.GetValue(DICOM_TAG_SERIES_INSTANCE_UID));
-    simplified.SetValue(DICOM_TAG_SOP_INSTANCE_UID, findResult.GetValue(DICOM_TAG_SOP_INSTANCE_UID));
-    Move(targetAet, simplified);
+    DicomMap query;
+    query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid);
+    query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid);
+    MoveInternal(targetAet, query);
   }
 
   void DicomUserConnection::MoveInstance(const std::string& targetAet,
@@ -864,11 +915,11 @@
                                          const std::string& seriesUid,
                                          const std::string& instanceUid)
   {
-    DicomMap map;
-    map.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid);
-    map.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid);
-    map.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid);
-    Move(targetAet, map);
+    DicomMap query;
+    query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid);
+    query.SetValue(DICOM_TAG_SERIES_INSTANCE_UID, seriesUid);
+    query.SetValue(DICOM_TAG_SOP_INSTANCE_UID, instanceUid);
+    MoveInternal(targetAet, query);
   }
 
 
--- a/OrthancServer/DicomProtocol/DicomUserConnection.h	Wed May 27 10:50:59 2015 +0200
+++ b/OrthancServer/DicomProtocol/DicomUserConnection.h	Wed May 27 12:32:43 2015 +0200
@@ -46,14 +46,6 @@
   class DicomUserConnection : public boost::noncopyable
   {
   private:
-    enum FindRootModel
-    {
-      FindRootModel_Patient,
-      FindRootModel_Study,
-      FindRootModel_Series,
-      FindRootModel_Instance
-    };
-
     struct PImpl;
     boost::shared_ptr<PImpl> pimpl_;
 
@@ -72,12 +64,8 @@
 
     void SetupPresentationContexts(const std::string& preferredTransferSyntax);
 
-    void Find(DicomFindAnswers& result,
-              FindRootModel model,
-              const DicomMap& fields);
-
-    void Move(const std::string& targetAet,
-              const DicomMap& fields);
+    void MoveInternal(const std::string& targetAet,
+                      const DicomMap& fields);
 
     void ResetStorageSOPClasses();
 
@@ -150,29 +138,24 @@
 
     void StoreFile(const std::string& path);
 
-    void FindPatient(DicomFindAnswers& result,
-                     const DicomMap& fields);
-
-    void FindStudy(DicomFindAnswers& result,
-                   const DicomMap& fields);
+    void Find(DicomFindAnswers& result,
+              ResourceType level,
+              const DicomMap& fields);
 
-    void FindSeries(DicomFindAnswers& result,
-                    const DicomMap& fields);
+    void Move(const std::string& targetAet,
+              const DicomMap& findResult);
 
-    void FindInstance(DicomFindAnswers& result,
-                      const DicomMap& fields);
+    void MovePatient(const std::string& targetAet,
+                     const std::string& patientId);
 
-    void MoveSeries(const std::string& targetAet,
-                    const DicomMap& findResult);
+    void MoveStudy(const std::string& targetAet,
+                   const std::string& studyUid);
 
     void MoveSeries(const std::string& targetAet,
                     const std::string& studyUid,
                     const std::string& seriesUid);
 
     void MoveInstance(const std::string& targetAet,
-                      const DicomMap& findResult);
-
-    void MoveInstance(const std::string& targetAet,
                       const std::string& studyUid,
                       const std::string& seriesUid,
                       const std::string& instanceUid);
--- a/OrthancServer/FromDcmtkBridge.cpp	Wed May 27 10:50:59 2015 +0200
+++ b/OrthancServer/FromDcmtkBridge.cpp	Wed May 27 12:32:43 2015 +0200
@@ -624,7 +624,8 @@
 
 
   void FromDcmtkBridge::ToJson(Json::Value& result,
-                               const DicomMap& values)
+                               const DicomMap& values,
+                               bool simplify)
   {
     if (result.type() != Json::objectValue)
     {
@@ -636,7 +637,29 @@
     for (DicomMap::Map::const_iterator 
            it = values.map_.begin(); it != values.map_.end(); ++it)
     {
-      result[GetName(it->first)] = it->second->AsString();
+      if (simplify)
+      {
+        result[GetName(it->first)] = it->second->AsString();
+      }
+      else
+      {
+        Json::Value value = Json::objectValue;
+
+        value["Name"] = GetName(it->first);
+
+        if (it->second->IsNull())
+        {
+          value["Type"] = "Null";
+          value["Value"] = Json::nullValue;
+        }
+        else
+        {
+          value["Type"] = "String";
+          value["Value"] = it->second->AsString();
+        }
+
+        result[it->first.Format()] = value;
+      }
     }
   }
 
--- a/OrthancServer/FromDcmtkBridge.h	Wed May 27 10:50:59 2015 +0200
+++ b/OrthancServer/FromDcmtkBridge.h	Wed May 27 12:32:43 2015 +0200
@@ -99,7 +99,8 @@
                       const DicomMap& m);
 
     static void ToJson(Json::Value& result,
-                       const DicomMap& values);
+                       const DicomMap& values,
+                       bool simplify);
 
     static std::string GenerateUniqueIdentifier(ResourceType level);
 
--- a/OrthancServer/OrthancInitialization.cpp	Wed May 27 10:50:59 2015 +0200
+++ b/OrthancServer/OrthancInitialization.cpp	Wed May 27 12:32:43 2015 +0200
@@ -238,7 +238,8 @@
   {
     boost::mutex::scoped_lock lock(globalMutex_);
 
-    if (configuration_->isMember(parameter))
+    if (configuration_.get() != NULL &&
+        configuration_->isMember(parameter))
     {
       return (*configuration_) [parameter].asString();
     }
@@ -254,7 +255,8 @@
   {
     boost::mutex::scoped_lock lock(globalMutex_);
 
-    if (configuration_->isMember(parameter))
+    if (configuration_.get() != NULL &&
+        configuration_->isMember(parameter))
     {
       return (*configuration_) [parameter].asInt();
     }
@@ -270,7 +272,8 @@
   {
     boost::mutex::scoped_lock lock(globalMutex_);
 
-    if (configuration_->isMember(parameter))
+    if (configuration_.get() != NULL &&
+        configuration_->isMember(parameter))
     {
       return (*configuration_) [parameter].asBool();
     }
@@ -286,6 +289,11 @@
   {
     boost::mutex::scoped_lock lock(globalMutex_);
 
+    if (configuration_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+       
     if (!configuration_->isMember("DicomModalities"))
     {
       throw OrthancException(ErrorCode_BadFileFormat);
@@ -318,6 +326,11 @@
   {
     boost::mutex::scoped_lock lock(globalMutex_);
 
+    if (configuration_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_InexistentItem);
+    }
+       
     if (!configuration_->isMember("OrthancPeers"))
     {
       throw OrthancException(ErrorCode_BadFileFormat);
@@ -352,7 +365,8 @@
 
     target.clear();
   
-    if (!configuration_->isMember(parameter))
+    if (configuration_.get() == NULL ||
+        !configuration_->isMember(parameter))
     {
       return true;
     }
@@ -409,7 +423,8 @@
 
     httpServer.ClearUsers();
 
-    if (!configuration_->isMember("RegisteredUsers"))
+    if (configuration_.get() == NULL ||
+        !configuration_->isMember("RegisteredUsers"))
     {
       return;
     }
@@ -470,7 +485,8 @@
 
     target.clear();
   
-    if (!configuration_->isMember(key))
+    if (configuration_.get() == NULL ||
+        !configuration_->isMember(key))
     {
       return;
     }
@@ -571,6 +587,11 @@
   {
     boost::mutex::scoped_lock lock(globalMutex_);
 
+    if (configuration_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
     if (!configuration_->isMember("DicomModalities"))
     {
       (*configuration_) ["DicomModalities"] = Json::objectValue;
@@ -594,6 +615,11 @@
   {
     boost::mutex::scoped_lock lock(globalMutex_);
 
+    if (configuration_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
     if (!configuration_->isMember("DicomModalities"))
     {
       throw OrthancException(ErrorCode_BadFileFormat);
@@ -614,6 +640,11 @@
   {
     boost::mutex::scoped_lock lock(globalMutex_);
 
+    if (configuration_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
     if (!configuration_->isMember("OrthancPeers"))
     {
       (*configuration_) ["OrthancPeers"] = Json::objectValue;
@@ -637,6 +668,11 @@
   {
     boost::mutex::scoped_lock lock(globalMutex_);
 
+    if (configuration_.get() == NULL)
+    {
+      throw OrthancException(ErrorCode_InternalError);
+    }
+
     if (!configuration_->isMember("OrthancPeers"))
     {
       throw OrthancException(ErrorCode_BadFileFormat);
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Wed May 27 10:50:59 2015 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Wed May 27 12:32:43 2015 +0200
@@ -39,35 +39,16 @@
 #include "../Scheduler/ServerJob.h"
 #include "../Scheduler/StoreScuCommand.h"
 #include "../Scheduler/StorePeerCommand.h"
+#include "../QueryRetrieveHandler.h"
+#include "../ServerToolbox.h"
 
 #include <glog/logging.h>
 
 namespace Orthanc
 {
-  // DICOM SCU ----------------------------------------------------------------
-
-  static bool MergeQueryAndTemplate(DicomMap& result,
-                                    const std::string& postData)
-  {
-    Json::Value query;
-    Json::Reader reader;
-
-    if (!reader.parse(postData, query) ||
-        query.type() != Json::objectValue)
-    {
-      return false;
-    }
-
-    Json::Value::Members members = query.getMemberNames();
-    for (size_t i = 0; i < members.size(); i++)
-    {
-      DicomTag t = FromDcmtkBridge::ParseTag(members[i]);
-      result.SetValue(t, query[members[i]].asString());
-    }
-
-    return true;
-  }
-
+  /***************************************************************************
+   * DICOM C-Echo SCU
+   ***************************************************************************/
 
   static void DicomEcho(RestApiPostCall& call)
   {
@@ -94,13 +75,100 @@
   }
 
 
+
+  /***************************************************************************
+   * DICOM C-Find SCU => DEPRECATED!
+   ***************************************************************************/
+
+  static bool MergeQueryAndTemplate(DicomMap& result,
+                                    const std::string& postData)
+  {
+    Json::Value query;
+    Json::Reader reader;
+
+    if (!reader.parse(postData, query) ||
+        query.type() != Json::objectValue)
+    {
+      return false;
+    }
+
+    Json::Value::Members members = query.getMemberNames();
+    for (size_t i = 0; i < members.size(); i++)
+    {
+      DicomTag t = FromDcmtkBridge::ParseTag(members[i]);
+      result.SetValue(t, query[members[i]].asString());
+    }
+
+    return true;
+  }
+
+
+  static void FindPatient(DicomFindAnswers& result,
+                          DicomUserConnection& connection,
+                          const DicomMap& fields)
+  {
+    // Only keep the filters from "fields" that are related to the patient
+    DicomMap s;
+    fields.ExtractPatientInformation(s);
+    connection.Find(result, ResourceType_Patient, s);
+  }
+
+
+  static void FindStudy(DicomFindAnswers& result,
+                        DicomUserConnection& connection,
+                        const DicomMap& fields)
+  {
+    // Only keep the filters from "fields" that are related to the study
+    DicomMap s;
+    fields.ExtractStudyInformation(s);
+
+    s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID);
+    s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER);
+    s.CopyTagIfExists(fields, DICOM_TAG_MODALITIES_IN_STUDY);
+
+    connection.Find(result, ResourceType_Study, s);
+  }
+
+  static void FindSeries(DicomFindAnswers& result,
+                         DicomUserConnection& connection,
+                         const DicomMap& fields)
+  {
+    // Only keep the filters from "fields" that are related to the series
+    DicomMap s;
+    fields.ExtractSeriesInformation(s);
+
+    s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID);
+    s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER);
+    s.CopyTagIfExists(fields, DICOM_TAG_STUDY_INSTANCE_UID);
+
+    connection.Find(result, ResourceType_Series, s);
+  }
+
+  static void FindInstance(DicomFindAnswers& result,
+                           DicomUserConnection& connection,
+                           const DicomMap& fields)
+  {
+    // Only keep the filters from "fields" that are related to the instance
+    DicomMap s;
+    fields.ExtractInstanceInformation(s);
+
+    s.CopyTagIfExists(fields, DICOM_TAG_PATIENT_ID);
+    s.CopyTagIfExists(fields, DICOM_TAG_ACCESSION_NUMBER);
+    s.CopyTagIfExists(fields, DICOM_TAG_STUDY_INSTANCE_UID);
+    s.CopyTagIfExists(fields, DICOM_TAG_SERIES_INSTANCE_UID);
+
+    connection.Find(result, ResourceType_Instance, s);
+  }
+
+
   static void DicomFindPatient(RestApiPostCall& call)
   {
+    LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
     ServerContext& context = OrthancRestApi::GetContext(call);
 
-    DicomMap m;
-    DicomMap::SetupFindPatientTemplate(m);
-    if (!MergeQueryAndTemplate(m, call.GetPostBody()))
+    DicomMap fields;
+    DicomMap::SetupFindPatientTemplate(fields);
+    if (!MergeQueryAndTemplate(fields, call.GetPostBody()))
     {
       return;
     }
@@ -109,26 +177,27 @@
     ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), remote);
 
     DicomFindAnswers answers;
-    locker.GetConnection().FindPatient(answers, m);
+    FindPatient(answers, locker.GetConnection(), fields);
 
     Json::Value result;
-    answers.ToJson(result);
+    answers.ToJson(result, true);
     call.GetOutput().AnswerJson(result);
   }
 
   static void DicomFindStudy(RestApiPostCall& call)
   {
+    LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
     ServerContext& context = OrthancRestApi::GetContext(call);
 
-    DicomMap m;
-    DicomMap::SetupFindStudyTemplate(m);
-    if (!MergeQueryAndTemplate(m, call.GetPostBody()))
+    DicomMap fields;
+    DicomMap::SetupFindStudyTemplate(fields);
+    if (!MergeQueryAndTemplate(fields, call.GetPostBody()))
     {
       return;
     }
 
-    if (m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 &&
-        m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2)
+    if (fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 &&
+        fields.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2)
     {
       return;
     }        
@@ -137,27 +206,28 @@
     ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), remote);
 
     DicomFindAnswers answers;
-    locker.GetConnection().FindStudy(answers, m);
+    FindStudy(answers, locker.GetConnection(), fields);
 
     Json::Value result;
-    answers.ToJson(result);
+    answers.ToJson(result, true);
     call.GetOutput().AnswerJson(result);
   }
 
   static void DicomFindSeries(RestApiPostCall& call)
   {
+    LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
     ServerContext& context = OrthancRestApi::GetContext(call);
 
-    DicomMap m;
-    DicomMap::SetupFindSeriesTemplate(m);
-    if (!MergeQueryAndTemplate(m, call.GetPostBody()))
+    DicomMap fields;
+    DicomMap::SetupFindSeriesTemplate(fields);
+    if (!MergeQueryAndTemplate(fields, call.GetPostBody()))
     {
       return;
     }
 
-    if ((m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 &&
-         m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) ||
-        m.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString().size() <= 2)
+    if ((fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 &&
+         fields.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) ||
+        fields.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString().size() <= 2)
     {
       return;
     }        
@@ -166,28 +236,29 @@
     ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), remote);
 
     DicomFindAnswers answers;
-    locker.GetConnection().FindSeries(answers, m);
+    FindSeries(answers, locker.GetConnection(), fields);
 
     Json::Value result;
-    answers.ToJson(result);
+    answers.ToJson(result, true);
     call.GetOutput().AnswerJson(result);
   }
 
   static void DicomFindInstance(RestApiPostCall& call)
   {
+    LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
     ServerContext& context = OrthancRestApi::GetContext(call);
 
-    DicomMap m;
-    DicomMap::SetupFindInstanceTemplate(m);
-    if (!MergeQueryAndTemplate(m, call.GetPostBody()))
+    DicomMap fields;
+    DicomMap::SetupFindInstanceTemplate(fields);
+    if (!MergeQueryAndTemplate(fields, call.GetPostBody()))
     {
       return;
     }
 
-    if ((m.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 &&
-         m.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) ||
-        m.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString().size() <= 2 ||
-        m.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString().size() <= 2)
+    if ((fields.GetValue(DICOM_TAG_ACCESSION_NUMBER).AsString().size() <= 2 &&
+         fields.GetValue(DICOM_TAG_PATIENT_ID).AsString().size() <= 2) ||
+        fields.GetValue(DICOM_TAG_STUDY_INSTANCE_UID).AsString().size() <= 2 ||
+        fields.GetValue(DICOM_TAG_SERIES_INSTANCE_UID).AsString().size() <= 2)
     {
       return;
     }        
@@ -196,15 +267,17 @@
     ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), remote);
 
     DicomFindAnswers answers;
-    locker.GetConnection().FindInstance(answers, m);
+    FindInstance(answers, locker.GetConnection(), fields);
 
     Json::Value result;
-    answers.ToJson(result);
+    answers.ToJson(result, true);
     call.GetOutput().AnswerJson(result);
   }
 
+
   static void DicomFind(RestApiPostCall& call)
   {
+    LOG(WARNING) << "This URI is deprecated: " << call.FlattenUri();
     ServerContext& context = OrthancRestApi::GetContext(call);
 
     DicomMap m;
@@ -218,14 +291,14 @@
     ReusableDicomUserConnection::Locker locker(context.GetReusableDicomUserConnection(), remote);
 
     DicomFindAnswers patients;
-    locker.GetConnection().FindPatient(patients, m);
+    FindPatient(patients, locker.GetConnection(), m);
 
     // Loop over the found patients
     Json::Value result = Json::arrayValue;
     for (size_t i = 0; i < patients.GetSize(); i++)
     {
       Json::Value patient(Json::objectValue);
-      FromDcmtkBridge::ToJson(patient, patients.GetAnswer(i));
+      FromDcmtkBridge::ToJson(patient, patients.GetAnswer(i), true);
 
       DicomMap::SetupFindStudyTemplate(m);
       if (!MergeQueryAndTemplate(m, call.GetPostBody()))
@@ -235,7 +308,7 @@
       m.CopyTagIfExists(patients.GetAnswer(i), DICOM_TAG_PATIENT_ID);
 
       DicomFindAnswers studies;
-      locker.GetConnection().FindStudy(studies, m);
+      FindStudy(studies, locker.GetConnection(), m);
 
       patient["Studies"] = Json::arrayValue;
       
@@ -243,7 +316,7 @@
       for (size_t j = 0; j < studies.GetSize(); j++)
       {
         Json::Value study(Json::objectValue);
-        FromDcmtkBridge::ToJson(study, studies.GetAnswer(j));
+        FromDcmtkBridge::ToJson(study, studies.GetAnswer(j), true);
 
         DicomMap::SetupFindSeriesTemplate(m);
         if (!MergeQueryAndTemplate(m, call.GetPostBody()))
@@ -254,14 +327,14 @@
         m.CopyTagIfExists(studies.GetAnswer(j), DICOM_TAG_STUDY_INSTANCE_UID);
 
         DicomFindAnswers series;
-        locker.GetConnection().FindSeries(series, m);
+        FindSeries(series, locker.GetConnection(), m);
 
         // Loop over the found series
         study["Series"] = Json::arrayValue;
         for (size_t k = 0; k < series.GetSize(); k++)
         {
           Json::Value series2(Json::objectValue);
-          FromDcmtkBridge::ToJson(series2, series.GetAnswer(k));
+          FromDcmtkBridge::ToJson(series2, series.GetAnswer(k), true);
           study["Series"].append(series2);
         }
 
@@ -275,6 +348,205 @@
   }
 
 
+
+  /***************************************************************************
+   * DICOM C-Find and C-Move SCU => Recommended since Orthanc 0.9.0
+   ***************************************************************************/
+
+  static void DicomQuery(RestApiPostCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    Json::Value request;
+
+    if (call.ParseJsonRequest(request) &&
+        request.type() == Json::objectValue &&
+        request.isMember("Level") && request["Level"].type() == Json::stringValue &&
+        (!request.isMember("Query") || request["Query"].type() == Json::objectValue))
+    {
+      std::auto_ptr<QueryRetrieveHandler>  handler(new QueryRetrieveHandler(context));
+
+      handler->SetModality(call.GetUriComponent("id", ""));
+      handler->SetLevel(StringToResourceType(request["Level"].asString().c_str()));
+
+      if (request.isMember("Query"))
+      {
+        Json::Value::Members tags = request["Query"].getMemberNames();
+        for (size_t i = 0; i < tags.size(); i++)
+        {
+          handler->SetQuery(FromDcmtkBridge::ParseTag(tags[i].c_str()),
+                            request["Query"][tags[i]].asString());
+        }
+      }
+
+      handler->Run();
+
+      std::string s = context.GetQueryRetrieveArchive().Add(handler.release());
+      Json::Value result = Json::objectValue;
+      result["ID"] = s;
+      result["Path"] = "/queries/" + s;
+      call.GetOutput().AnswerJson(result);      
+    }
+  }
+
+
+  static void ListQueries(RestApiGetCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+
+    std::list<std::string> queries;
+    context.GetQueryRetrieveArchive().List(queries);
+
+    Json::Value result = Json::arrayValue;
+    for (std::list<std::string>::const_iterator
+           it = queries.begin(); it != queries.end(); ++it)
+    {
+      result.append(*it);
+    }
+
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+  namespace
+  {
+    class QueryAccessor
+    {
+    private:
+      ServerContext&            context_;
+      SharedArchive::Accessor   accessor_;
+      QueryRetrieveHandler&     handler_;
+
+    public:
+      QueryAccessor(RestApiCall& call) :
+        context_(OrthancRestApi::GetContext(call)),
+        accessor_(context_.GetQueryRetrieveArchive(), call.GetUriComponent("id", "")),
+        handler_(dynamic_cast<QueryRetrieveHandler&>(accessor_.GetItem()))
+      {
+      }                     
+
+      QueryRetrieveHandler* operator->()
+      {
+        return &handler_;
+      }
+    };
+
+    static void AnswerDicomMap(RestApiCall& call,
+                               const DicomMap& value,
+                               bool simplify)
+    {
+      Json::Value full = Json::objectValue;
+      FromDcmtkBridge::ToJson(full, value, simplify);
+      call.GetOutput().AnswerJson(full);
+    }
+  }
+
+
+  static void ListQueryAnswers(RestApiGetCall& call)
+  {
+    QueryAccessor query(call);
+    size_t count = query->GetAnswerCount();
+
+    Json::Value result = Json::arrayValue;
+    for (size_t i = 0; i < count; i++)
+    {
+      result.append(boost::lexical_cast<std::string>(i));
+    }
+
+    call.GetOutput().AnswerJson(result);
+  }
+
+
+  static void GetQueryOneAnswer(RestApiGetCall& call)
+  {
+    size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
+    QueryAccessor query(call);
+    AnswerDicomMap(call, query->GetAnswer(index), call.HasArgument("simplify"));
+  }
+
+
+  static void RetrieveOneAnswer(RestApiPostCall& call)
+  {
+    size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
+
+    LOG(WARNING) << "Driving C-Move SCU on modality: " << call.GetPostBody();
+
+    QueryAccessor query(call);
+    query->Retrieve(call.GetPostBody(), index);
+
+    // Retrieve has succeeded
+    call.GetOutput().AnswerBuffer("{}", "application/json");
+  }
+
+
+  static void RetrieveAllAnswers(RestApiPostCall& call)
+  {
+    LOG(WARNING) << "Driving C-Move SCU on modality: " << call.GetPostBody();
+
+    QueryAccessor query(call);
+    query->Retrieve(call.GetPostBody());
+
+    // Retrieve has succeeded
+    call.GetOutput().AnswerBuffer("{}", "application/json");
+  }
+
+
+  static void GetQueryArguments(RestApiGetCall& call)
+  {
+    QueryAccessor query(call);
+    AnswerDicomMap(call, query->GetQuery(), call.HasArgument("simplify"));
+  }
+
+
+  static void GetQueryLevel(RestApiGetCall& call)
+  {
+    QueryAccessor query(call);
+    call.GetOutput().AnswerBuffer(EnumerationToString(query->GetLevel()), "text/plain");
+  }
+
+
+  static void GetQueryModality(RestApiGetCall& call)
+  {
+    QueryAccessor query(call);
+    call.GetOutput().AnswerBuffer(query->GetModalitySymbolicName(), "text/plain");
+  }
+
+
+  static void DeleteQuery(RestApiDeleteCall& call)
+  {
+    ServerContext& context = OrthancRestApi::GetContext(call);
+    context.GetQueryRetrieveArchive().Remove(call.GetUriComponent("id", ""));
+    call.GetOutput().AnswerBuffer("", "text/plain");
+  }
+
+
+  static void ListQueryOperations(RestApiGetCall& call)
+  {
+    // Ensure that the query of interest does exist
+    QueryAccessor query(call);  
+
+    RestApi::AutoListChildren(call);
+  }
+
+
+  static void ListQueryAnswerOperations(RestApiGetCall& call)
+  {
+    // Ensure that the query of interest does exist
+    QueryAccessor query(call);
+
+    // Ensure that the answer of interest does exist
+    size_t index = boost::lexical_cast<size_t>(call.GetUriComponent("index", ""));
+    query->GetAnswer(index);
+
+    RestApi::AutoListChildren(call);
+  }
+
+
+
+
+  /***************************************************************************
+   * DICOM C-Store SCU
+   ***************************************************************************/
+
   static bool GetInstancesToExport(std::list<std::string>& instances,
                                    const std::string& remote,
                                    RestApiPostCall& call)
@@ -379,7 +651,9 @@
   }
 
 
-  // Orthanc Peers ------------------------------------------------------------
+  /***************************************************************************
+   * Orthanc Peers => Store client
+   ***************************************************************************/
 
   static bool IsExistingPeer(const OrthancRestApi::SetOfStrings& peers,
                              const std::string& id)
@@ -543,6 +817,20 @@
     Register("/modalities/{id}/find", DicomFind);
     Register("/modalities/{id}/store", DicomStore);
 
+    // For Query/Retrieve
+    Register("/modalities/{id}/query", DicomQuery);
+    Register("/queries", ListQueries);
+    Register("/queries/{id}", DeleteQuery);
+    Register("/queries/{id}", ListQueryOperations);
+    Register("/queries/{id}/answers", ListQueryAnswers);
+    Register("/queries/{id}/answers/{index}", ListQueryAnswerOperations);
+    Register("/queries/{id}/answers/{index}/content", GetQueryOneAnswer);
+    Register("/queries/{id}/answers/{index}/retrieve", RetrieveOneAnswer);
+    Register("/queries/{id}/level", GetQueryLevel);
+    Register("/queries/{id}/modality", GetQueryModality);
+    Register("/queries/{id}/query", GetQueryArguments);
+    Register("/queries/{id}/retrieve", RetrieveAllAnswers);
+
     Register("/peers", ListPeers);
     Register("/peers/{id}", ListPeerOperations);
     Register("/peers/{id}", UpdatePeer);
--- a/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Wed May 27 10:50:59 2015 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Wed May 27 12:32:43 2015 +0200
@@ -765,7 +765,7 @@
 
     typedef std::set<DicomTag> ModuleTags;
     ModuleTags moduleTags;
-    DicomTag::GetTagsForModule(moduleTags, module);
+    DicomTag::AddTagsForModule(moduleTags, module);
 
     Json::Value tags;
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/QueryRetrieveHandler.cpp	Wed May 27 12:32:43 2015 +0200
@@ -0,0 +1,132 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, 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 "QueryRetrieveHandler.h"
+
+#include "OrthancInitialization.h"
+
+
+namespace Orthanc
+{
+  void QueryRetrieveHandler::Invalidate()
+  {
+    done_ = false;
+    answers_.Clear();
+  }
+
+
+  void QueryRetrieveHandler::Run()
+  {
+    if (!done_)
+    {
+      ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), modality_);
+      locker.GetConnection().Find(answers_, level_, query_);
+      done_ = true;
+    }
+  }
+
+
+  QueryRetrieveHandler::QueryRetrieveHandler(ServerContext& context) : 
+    context_(context),
+    done_(false),
+    level_(ResourceType_Study)
+  {
+  }
+
+
+  void QueryRetrieveHandler::SetModality(const std::string& symbolicName)
+  {
+    Invalidate();
+    modalityName_ = symbolicName;
+    Configuration::GetDicomModalityUsingSymbolicName(modality_, symbolicName);
+  }
+
+
+  void QueryRetrieveHandler::SetLevel(ResourceType level)
+  {
+    Invalidate();
+    level_ = level;
+  }
+
+
+  void QueryRetrieveHandler::SetQuery(const DicomTag& tag,
+                                      const std::string& value)
+  {
+    Invalidate();
+    query_.SetValue(tag, value);
+  }
+
+
+  size_t QueryRetrieveHandler::GetAnswerCount()
+  {
+    Run();
+    return answers_.GetSize();
+  }
+
+
+  const DicomMap& QueryRetrieveHandler::GetAnswer(size_t i)
+  {
+    Run();
+
+    if (i >= answers_.GetSize())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    return answers_.GetAnswer(i);
+  }
+
+
+  void QueryRetrieveHandler::Retrieve(const std::string& target,
+                                      size_t i)
+  {
+    Run();
+
+    if (i >= answers_.GetSize())
+    {
+      throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
+    ReusableDicomUserConnection::Locker locker(context_.GetReusableDicomUserConnection(), modality_);
+    locker.GetConnection().Move(target, answers_.GetAnswer(i));
+  }
+
+
+  void QueryRetrieveHandler::Retrieve(const std::string& target)
+  {
+    for (size_t i = 0; i < GetAnswerCount(); i++)
+    {
+      Retrieve(target, i);
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancServer/QueryRetrieveHandler.h	Wed May 27 12:32:43 2015 +0200
@@ -0,0 +1,94 @@
+/**
+ * Orthanc - A Lightweight, RESTful DICOM Store
+ * Copyright (C) 2012-2015 Sebastien Jodogne, Medical Physics
+ * Department, University Hospital of Liege, Belgium
+ *
+ * This program is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * In addition, as a special exception, the copyright holders of this
+ * program give permission to link the code of its release with the
+ * OpenSSL project's "OpenSSL" library (or with modified versions of it
+ * that use the same license as the "OpenSSL" library), and distribute
+ * the linked executables. You must obey the GNU General Public License
+ * in all respects for all of the code used other than "OpenSSL". If you
+ * modify file(s) with this exception, you may extend this exception to
+ * your version of the file(s), but you are not obligated to do so. If
+ * you do not wish to do so, delete this exception statement from your
+ * version. If you delete this exception statement from all source files
+ * in the program, then also delete it here.
+ * 
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "ServerContext.h"
+
+namespace Orthanc
+{
+  class QueryRetrieveHandler : public IDynamicObject
+  {
+  private:
+    ServerContext&             context_;
+    bool                       done_;
+    RemoteModalityParameters   modality_;
+    ResourceType               level_;
+    DicomMap                   query_;
+    DicomFindAnswers           answers_;
+    std::string                modalityName_;
+
+    void Invalidate();
+
+
+  public:
+    QueryRetrieveHandler(ServerContext& context);
+
+    void SetModality(const std::string& symbolicName);
+
+    const RemoteModalityParameters& GetModality() const
+    {
+      return modality_;
+    }
+
+    const std::string& GetModalitySymbolicName() const
+    {
+      return modalityName_;
+    }
+
+    void SetLevel(ResourceType level);
+
+    ResourceType GetLevel() const
+    {
+      return level_;
+    }
+
+    void SetQuery(const DicomTag& tag,
+                  const std::string& value);
+
+    const DicomMap& GetQuery() const
+    {
+      return query_;
+    }
+
+    void Run();
+
+    size_t GetAnswerCount();
+
+    const DicomMap& GetAnswer(size_t i);
+
+    void Retrieve(const std::string& target,
+                  size_t i);
+
+    void Retrieve(const std::string& target);
+  };
+}
--- a/OrthancServer/ServerContext.cpp	Wed May 27 10:50:59 2015 +0200
+++ b/OrthancServer/ServerContext.cpp	Wed May 27 12:32:43 2015 +0200
@@ -78,7 +78,8 @@
     dicomCache_(provider_, DICOM_CACHE_SIZE),
     scheduler_(Configuration::GetGlobalIntegerParameter("LimitJobs", 10)),
     plugins_(NULL),
-    pluginsManager_(NULL)
+    pluginsManager_(NULL),
+    queryRetrieveArchive_(Configuration::GetGlobalIntegerParameter("QueryRetrieveSize", 10))
   {
     scu_.SetLocalApplicationEntityTitle(Configuration::GetGlobalStringParameter("DicomAet", "ORTHANC"));
 
--- a/OrthancServer/ServerContext.h	Wed May 27 10:50:59 2015 +0200
+++ b/OrthancServer/ServerContext.h	Wed May 27 12:32:43 2015 +0200
@@ -43,6 +43,7 @@
 #include "Scheduler/ServerScheduler.h"
 #include "DicomInstanceToStore.h"
 #include "ServerIndexChange.h"
+#include "../Core/Cache/SharedArchive.h"
 
 #include <boost/filesystem.hpp>
 
@@ -96,6 +97,8 @@
     OrthancPlugins* plugins_;  // TODO Turn it into a listener pattern (idem for Lua callbacks)
     const PluginsManager* pluginsManager_;
 
+    SharedArchive  queryRetrieveArchive_;
+
   public:
     class DicomCacheLocker : public boost::noncopyable
     {
@@ -223,5 +226,10 @@
     const PluginsManager& GetPluginsManager() const;
 
     const OrthancPlugins& GetOrthancPlugins() const;
+
+    SharedArchive& GetQueryRetrieveArchive()
+    {
+      return queryRetrieveArchive_;
+    }
   };
 }
--- a/OrthancServer/ServerIndex.cpp	Wed May 27 10:50:59 2015 +0200
+++ b/OrthancServer/ServerIndex.cpp	Wed May 27 12:32:43 2015 +0200
@@ -883,7 +883,7 @@
     DicomMap tags;
     db_.GetMainDicomTags(tags, resourceId);
     target["MainDicomTags"] = Json::objectValue;
-    FromDcmtkBridge::ToJson(target["MainDicomTags"], tags);
+    FromDcmtkBridge::ToJson(target["MainDicomTags"], tags, true);
   }
 
   bool ServerIndex::LookupResource(Json::Value& result,
--- a/OrthancServer/main.cpp	Wed May 27 10:50:59 2015 +0200
+++ b/OrthancServer/main.cpp	Wed May 27 12:32:43 2015 +0200
@@ -51,6 +51,7 @@
 #include "ServerToolbox.h"
 #include "../Plugins/Engine/PluginsManager.h"
 #include "../Plugins/Engine/OrthancPlugins.h"
+#include "FromDcmtkBridge.h"
 
 using namespace Orthanc;
 
@@ -383,6 +384,8 @@
 
 
 
+
+
 static bool StartOrthanc(int argc, char *argv[])
 {
 #if ENABLE_PLUGINS == 1
@@ -531,6 +534,7 @@
     }
 
     LOG(WARNING) << "Orthanc has started";
+
     Toolbox::ServerBarrier(restApi.ResetRequestReceivedFlag());
     isReset = restApi.ResetRequestReceivedFlag();
 
--- a/Resources/Configuration.json	Wed May 27 10:50:59 2015 +0200
+++ b/Resources/Configuration.json	Wed May 27 12:32:43 2015 +0200
@@ -222,5 +222,10 @@
   // are issued. This option sets the number of seconds of inactivity
   // to wait before automatically closing a DICOM association. If set
   // to 0, the connection is closed immediately.
-  "DicomAssociationCloseDelay" : 5
+  "DicomAssociationCloseDelay" : 5,
+
+  // Maximum number of query/retrieve DICOM requests that are
+  // maintained by Orthanc. The least recently used requests get
+  // deleted as new requests are issued.
+  "QueryRetrieveSize" : 10
 }
--- a/UnitTestsSources/DicomMapTests.cpp	Wed May 27 10:50:59 2015 +0200
+++ b/UnitTestsSources/DicomMapTests.cpp	Wed May 27 12:32:43 2015 +0200
@@ -153,7 +153,7 @@
                        DicomModule module)
 {
   std::set<DicomTag> moduleTags, main;
-  DicomTag::GetTagsForModule(moduleTags, module);
+  DicomTag::AddTagsForModule(moduleTags, module);
   DicomMap::GetMainDicomTags(main, level);
   
   // The main dicom tags are a subset of the module
--- a/UnitTestsSources/MemoryCacheTests.cpp	Wed May 27 10:50:59 2015 +0200
+++ b/UnitTestsSources/MemoryCacheTests.cpp	Wed May 27 12:32:43 2015 +0200
@@ -39,6 +39,7 @@
 #include <boost/lexical_cast.hpp>
 #include "../Core/IDynamicObject.h"
 #include "../Core/Cache/MemoryCache.h"
+#include "../Core/Cache/SharedArchive.h"
 
 
 TEST(LRU, Basic)
@@ -228,3 +229,66 @@
 
   ASSERT_EQ("45 42 43 47 44 42 ", provider.log_);
 }
+
+
+
+
+
+
+
+namespace
+{
+  class S : public Orthanc::IDynamicObject
+  {
+  private:
+    std::string value_;
+
+  public:
+    S(const std::string& value) : value_(value)
+    {
+    }
+
+    const std::string& GetValue() const
+    {
+      return value_;
+    }
+
+    static const std::string& Access(const Orthanc::IDynamicObject& obj)
+    {
+      return dynamic_cast<const S&>(obj).GetValue();
+    }
+  };
+}
+
+
+TEST(LRU, SharedArchive)
+{
+  std::string first, second;
+  Orthanc::SharedArchive a(3);
+  first = a.Add(new S("First item"));
+  second = a.Add(new S("Second item"));
+
+  for (int i = 1; i < 100; i++)
+  {
+    a.Add(new S("Item " + boost::lexical_cast<std::string>(i)));
+    // Continuously protect the two first items
+    try { Orthanc::SharedArchive::Accessor(a, first);  } catch (Orthanc::OrthancException&) {}
+    try { Orthanc::SharedArchive::Accessor(a, second); } catch (Orthanc::OrthancException&) {}
+  }
+
+  std::list<std::string> i;
+  a.List(i);
+
+  size_t count = 0;
+  for (std::list<std::string>::const_iterator
+         it = i.begin(); it != i.end(); it++)
+  {
+    if (*it == first ||
+        *it == second)
+    {
+      count++;
+    }
+  }
+
+  ASSERT_EQ(2, count);
+}