changeset 1385:772c8507c68d query-retrieve

integration mainline->query-retrieve
author Sebastien Jodogne <s.jodogne@gmail.com>
date Fri, 29 May 2015 15:00:45 +0200
parents 5c11c4e728eb (diff) d6971e18a324 (current diff)
children 72184773c62f
files OrthancServer/OrthancRestApi/OrthancRestResources.cpp
diffstat 34 files changed, 1633 insertions(+), 245 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Fri May 29 15:00:24 2015 +0200
+++ b/CMakeLists.txt	Fri May 29 15:00:45 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	Fri May 29 15:00:45 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	Fri May 29 15:00:45 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/DicomMap.cpp	Fri May 29 15:00:24 2015 +0200
+++ b/Core/DicomFormat/DicomMap.cpp	Fri May 29 15:00:45 2015 +0200
@@ -273,6 +273,16 @@
     result.SetValue(DICOM_TAG_ACCESSION_NUMBER, "");
     result.SetValue(DICOM_TAG_PATIENT_ID, "");
     result.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, "");
+
+    // These tags are considered as "main" by Orthanc, but are not in the Series module
+    result.Remove(DicomTag(0x0008, 0x0070));  // Manufacturer
+    result.Remove(DicomTag(0x0008, 0x1010));  // Station name
+    result.Remove(DicomTag(0x0018, 0x0024));  // Sequence name
+    result.Remove(DICOM_TAG_CARDIAC_NUMBER_OF_IMAGES);
+    result.Remove(DICOM_TAG_IMAGES_IN_ACQUISITION);
+    result.Remove(DICOM_TAG_NUMBER_OF_SLICES);
+    result.Remove(DICOM_TAG_NUMBER_OF_TEMPORAL_POSITIONS);
+    result.Remove(DICOM_TAG_NUMBER_OF_TIME_SLICES);
   }
 
   void DicomMap::SetupFindInstanceTemplate(DicomMap& result)
--- a/Core/DicomFormat/DicomTag.cpp	Fri May 29 15:00:24 2015 +0200
+++ b/Core/DicomFormat/DicomTag.cpp	Fri May 29 15:00:45 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	Fri May 29 15:00:24 2015 +0200
+++ b/Core/DicomFormat/DicomTag.h	Fri May 29 15:00:45 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	Fri May 29 15:00:24 2015 +0200
+++ b/Core/RestApi/RestApi.cpp	Fri May 29 15:00:45 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	Fri May 29 15:00:24 2015 +0200
+++ b/Core/RestApi/RestApiCall.cpp	Fri May 29 15:00:45 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	Fri May 29 15:00:24 2015 +0200
+++ b/Core/RestApi/RestApiCall.h	Fri May 29 15:00:45 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	Fri May 29 15:00:24 2015 +0200
+++ b/Core/RestApi/RestApiOutput.cpp	Fri May 29 15:00:45 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	Fri May 29 15:00:24 2015 +0200
+++ b/Core/RestApi/RestApiOutput.h	Fri May 29 15:00:45 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/NEWS	Fri May 29 15:00:24 2015 +0200
+++ b/NEWS	Fri May 29 15:00:45 2015 +0200
@@ -4,6 +4,8 @@
 Major
 -----
 
+* DICOM Query/Retrieve available from Orthanc Explorer
+* C-Move SCU and C-Find SCU are accessible through the REST API
 * "?expand" flag for URIs "/patients", "/studies" and "/series"
 * "/tools/find" URI to search for DICOM resources from REST
 * Support of FreeBSD
--- a/OrthancExplorer/explorer.html	Fri May 29 15:00:24 2015 +0200
+++ b/OrthancExplorer/explorer.html	Fri May 29 15:00:45 2015 +0200
@@ -30,6 +30,7 @@
     <link rel="stylesheet" href="explorer.css" />
     <script src="file-upload.js"></script>
     <script src="explorer.js"></script>
+    <script src="query-retrieve.js"></script>
     <script src="../plugins/explorer.js"></script>
   </head>
   <body>
@@ -37,7 +38,10 @@
       <div data-role="header" >
 	<h1><span class="orthanc-name"></span>Find a patient</h1>
         <a href="#plugins" data-icon="grid" class="ui-btn-left" data-direction="reverse">Plugins</a>
-        <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
+          <a href="#upload" data-icon="gear" data-role="button">Upload</a>
+          <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+        </div>
       </div>
       <div data-role="content">
         <ul id="all-patients" data-role="listview" data-inset="true" data-filter="true">
@@ -75,7 +79,10 @@
       <div data-role="header" >
 	<h1><span class="orthanc-name"></span>Patient</h1>
         <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
-        <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
+          <a href="#upload" data-icon="gear" data-role="button">Upload</a>
+          <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+        </div>
       </div>
       <div data-role="content">
         <div class="ui-grid-a">
@@ -129,7 +136,10 @@
           Study
         </h1>
         <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
-        <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
+          <a href="#upload" data-icon="gear" data-role="button">Upload</a>
+          <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+        </div>
       </div>
       <div data-role="content">
         <div class="ui-grid-a">
@@ -178,7 +188,10 @@
         </h1>
 
         <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
-        <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
+          <a href="#upload" data-icon="gear" data-role="button">Upload</a>
+          <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+        </div>
       </div>
       <div data-role="content">
         <div class="ui-grid-a">
@@ -228,7 +241,10 @@
           Instance
         </h1>
         <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
-        <a href="#upload" data-icon="gear" class="ui-btn-right">Upload DICOM</a>
+        <div data-type="horizontal" data-role="controlgroup" class="ui-btn-right"> 
+          <a href="#upload" data-icon="gear" data-role="button">Upload</a>
+          <a href="#query-retrieve" data-icon="search" data-role="button">Query/Retrieve</a>
+        </div>
       </div>
       <div data-role="content">
         <div class="ui-grid-a">
@@ -284,6 +300,90 @@
       </div>
     </div>
 
+    <div data-role="page" id="query-retrieve" >
+      <div data-role="header" >
+	<h1><span class="orthanc-name"></span>DICOM Query/Retrieve (1/3)</h1>
+        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
+      </div>
+      <div data-role="content">
+        <form data-ajax="false">
+          <div data-role="fieldcontain">
+	    <label for="qr-server">DICOM server:</label>
+            <select name="qr-server" id="qr-server">
+            </select>
+	  </div>
+
+          <div data-role="fieldcontain" id="qr-fields">
+	    <fieldset data-role="controlgroup">
+	      <legend>Field of interest:</legend>
+	      <input type="radio" name="qr-field" id="qr-patient-id" value="PatientID" checked="checked" />
+	      <label for="qr-patient-id">Patient ID</label>
+	      <input type="radio" name="qr-field" id="qr-patient-name" value="PatientName" />
+	      <label for="qr-patient-name">Patient Name</label>
+	      <input type="radio" name="qr-field" id="qr-accession-number" value="AccessionNumber" />
+	      <label for="qr-accession-number">Accession Number</label>
+	      <input type="radio" name="qr-field" id="qr-study-description" value="StudyDescription" />
+	      <label for="qr-study-description">Study Description</label>
+	    </fieldset>
+	  </div>
+
+          <div data-role="fieldcontain">
+	    <label for="qr-value">Value for this field:</label>
+	    <input type="text" name="qr-value" id="qr-value" value="*"  />
+	  </div>
+
+          <div data-role="fieldcontain">
+	    <label for="qr-date">Study date:</label>
+            <select name="qr-date" id="qr-date">
+            </select>
+	  </div>
+
+          <div data-role="fieldcontain" id="qr-modalities">
+            <div data-role="fieldcontain">
+	      <fieldset data-role="controlgroup" data-type="horizontal">
+                <legend>Modalities:</legend>
+	        <input type="checkbox" name="CR" id="qr-cr" class="custom" /> <label for="qr-cr">CR</label>
+	        <input type="checkbox" name="CT" id="qr-ct" class="custom" /> <label for="qr-ct">CT</label>
+	        <input type="checkbox" name="MR" id="qr-mr" class="custom" /> <label for="qr-mr">MR</label>
+	        <input type="checkbox" name="NM" id="qr-nm" class="custom" /> <label for="qr-nm">NM</label>
+	        <input type="checkbox" name="PT" id="qr-pt" class="custom" /> <label for="qr-pt">PT</label>
+	        <input type="checkbox" name="US" id="qr-us" class="custom" /> <label for="qr-us">US</label>
+	        <input type="checkbox" name="XA" id="qr-xa" class="custom" /> <label for="qr-xa">XA</label>
+	      </fieldset>
+            </div>
+          </div>
+
+          <button id="qr-submit" type="submit" data-theme="b">Search studies</button>
+        </form>
+      </div>
+    </div>
+
+
+    <div data-role="page" id="query-retrieve-2" >
+      <div data-role="header" >
+	<h1><span class="orthanc-name"></span>DICOM Query/Retrieve (2/3)</h1>
+        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
+        <a href="#query-retrieve" data-icon="search" class="ui-btn-right" data-direction="reverse">Query/Retrieve</a>
+      </div>
+      <div data-role="content">
+        <ul data-role="listview" data-inset="true" data-filter="true" data-split-icon="arrow-d" data-split-theme="b">
+        </ul>
+      </div>
+    </div>
+
+
+    <div data-role="page" id="query-retrieve-3" >
+      <div data-role="header" >
+	<h1><span class="orthanc-name"></span>DICOM Query/Retrieve (3/3)</h1>
+        <a href="#find-patients" data-icon="home" class="ui-btn-left" data-direction="reverse">Patients</a>
+        <a href="#query-retrieve" data-icon="search" class="ui-btn-right" data-direction="reverse">Query/Retrieve</a>
+      </div>
+      <div data-role="content">
+        <ul data-role="listview" data-inset="true" data-filter="true" data-split-icon="arrow-d" data-split-theme="b">
+        </ul>
+      </div>
+    </div>
+
 
     <div id="peer-store" style="display:none;" class="ui-body-c">
       <p align="center"><b>Sending to Orthanc peer...</b></p>
@@ -295,6 +395,11 @@
       <p><img src="libs/images/ajax-loader2.gif" alt="" /></p>
     </div>
 
+    <div id="info-retrieve" style="display:none;" class="ui-body-c">
+      <p align="center"><b>Retrieving images from DICOM modality...</b></p>
+      <p><img src="libs/images/ajax-loader2.gif" alt="" /></p>
+    </div>
+
     <div id="dialog" style="display:none" >
     </div>
   </body>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/OrthancExplorer/query-retrieve.js	Fri May 29 15:00:45 2015 +0200
@@ -0,0 +1,265 @@
+function JavascriptDateToDicom(date)
+{
+  var s = date.toISOString();
+  return s.substring(0, 4) + s.substring(5, 7) + s.substring(8, 10);
+}
+
+function GenerateDicomDate(days)
+{
+  var today = new Date();
+  var other = new Date(today);
+  other.setDate(today.getDate() + days);
+  return JavascriptDateToDicom(other);
+}
+
+
+$('#query-retrieve').live('pagebeforeshow', function() {
+  $.ajax({
+    url: '../modalities',
+    dataType: 'json',
+    async: false,
+    cache: false,
+    success: function(modalities) {
+      var target = $('#qr-server');
+      $('option', target).remove();
+
+      for (var i = 0; i < modalities.length; i++) {
+        var option = $('<option>').text(modalities[i]);
+        target.append(option);
+      }
+
+      target.selectmenu('refresh');
+    }
+  });
+
+  var target = $('#qr-date');
+  $('option', target).remove();
+  target.append($('<option>').attr('value', '*').text('Any date'));
+  target.append($('<option>').attr('value', GenerateDicomDate(0)).text('Today'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-1)).text('Yesterday'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-7) + '-').text('Last 7 days'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-31) + '-').text('Last 31 days'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-31 * 3) + '-').text('Last 3 months'));
+  target.append($('<option>').attr('value', GenerateDicomDate(-365) + '-').text('Last year'));
+  target.selectmenu('refresh');
+});
+
+
+$('#qr-submit').live('click', function() {
+  var query = {
+    'Level' : 'Study',
+    'Query' : {
+      'AccessionNumber' : '*',
+      'PatientBirthDate' : '*',
+      'PatientID' : '*',
+      'PatientName' : '*',
+      'PatientSex' : '*',
+      'SpecificCharacterSet' : 'ISO_IR 192',  // UTF-8
+      'StudyDate' : $('#qr-date').val(),
+      'StudyDescription' : '*'
+    }
+  };
+
+  var field = $('#qr-fields input:checked').val();
+  query['Query'][field] = $('#qr-value').val().toUpperCase();
+
+  var modalities = '';
+  $('#qr-modalities input:checked').each(function() {
+    var s = $(this).attr('name');
+    if (modalities == '*')
+      modalities = s;
+    else
+      modalities += '\\' + s;
+  });
+
+  if (modalities.length > 0) {
+    query['Query']['ModalitiesInStudy'] = modalities;
+  }
+
+
+  var server = $('#qr-server').val();
+  $.ajax({
+    url: '../modalities/' + server + '/query',
+    type: 'POST', 
+    data: JSON.stringify(query),
+    dataType: 'json',
+    async: false,
+    error: function() {
+      alert('Error during query (C-Find)');
+    },
+    success: function(result) {
+      window.location.assign('explorer.html#query-retrieve-2?server=' + server + '&uuid=' + result['ID']);
+    }
+  });
+
+  return false;
+});
+
+
+
+function Retrieve(url)
+{
+  $.ajax({
+    url: '../system',
+    dataType: 'json',
+    async: false,
+    success: function(system) {
+      $('<div>').simpledialog2({
+        mode: 'button',
+        headerText: 'Target',
+        headerClose: true,
+        buttonPrompt: 'Enter Application Entity Title (AET):',
+        buttonInput: true,
+        buttonInputDefault: system['DicomAet'],
+        buttons : {
+          'OK': {
+            click: function () { 
+              var aet = $.mobile.sdLastInput;
+              if (aet.length == 0)
+                aet = system['DicomAet'];
+
+              $.ajax({
+                url: url,
+                type: 'POST',
+                async: true,  // Necessary to block UI
+                dataType: 'text',
+                data: aet,
+                beforeSend: function() {
+                  $.blockUI({ message: $('#info-retrieve') });
+                },
+                complete: function(s) {
+                  $.unblockUI();
+                },
+                error: function() {
+                  alert('Error during retrieve');
+                }
+              });
+            }
+          }
+        }
+      });
+    }
+  });
+}
+
+
+
+
+$('#query-retrieve-2').live('pagebeforeshow', function() {
+  if ($.mobile.pageData) {
+    var uri = '../queries/' + $.mobile.pageData.uuid + '/answers';
+
+    $.ajax({
+      url: uri,
+      dataType: 'json',
+      async: false,
+      success: function(answers) {
+        var target = $('#query-retrieve-2 ul');
+        $('li', target).remove();
+
+        for (var i = 0; i < answers.length; i++) {
+          $.ajax({
+            url: uri + '/' + answers[i] + '/content?simplify',
+            dataType: 'json',
+            async: false,
+            success: function(study) {
+              var series = '#query-retrieve-3?server=' + $.mobile.pageData.server + '&uuid=' + study['StudyInstanceUID'];
+              var info = $('<a>').attr('href', series).html(
+                ('<h3>{0} - {1}</h3>' + 
+                 '<p>Accession number: <b>{2}</b></p>' +
+                 '<p>Birth date: <b>{3}</b></p>' +
+                 '<p>Patient sex: <b>{4}</b></p>' +
+                 '<p>Study description: <b>{5}</b></p>' +
+                 '<p>Study date: <b>{6}</b></p>').format(
+                   study['PatientID'],
+                   study['PatientName'],
+                   study['AccessionNumber'],
+                   FormatDicomDate(study['PatientBirthDate']),
+                   study['PatientSex'],
+                   study['StudyDescription'],
+                   FormatDicomDate(study['StudyDate'])));
+
+              var studyUri = uri + '/' + answers[i] + '/retrieve';
+              var retrieve = $('<a>').text('Retrieve').click(function() {
+                Retrieve(studyUri);
+              });
+
+              target.append($('<li>').append(info).append(retrieve));
+            }
+          });
+        }
+
+        target.listview('refresh');
+      }
+    });
+  }
+});
+
+
+$('#query-retrieve-3').live('pagebeforeshow', function() {
+  if ($.mobile.pageData) {
+    var query = {
+      'Level' : 'Series',
+      'Query' : {
+        'Modality' : '*',
+        'ProtocolName' : '*',
+        'SeriesDescription' : '*',
+        'SeriesInstanceUID' : '*',
+        'StudyInstanceUID' : $.mobile.pageData.uuid
+      }
+    };
+
+    $.ajax({
+      url: '../modalities/' + $.mobile.pageData.server + '/query',
+      type: 'POST', 
+      data: JSON.stringify(query),
+      dataType: 'json',
+      async: false,
+      error: function() {
+        alert('Error during query (C-Find)');
+      },
+      success: function(answer) {
+        var uri = '../queries/' + answer['ID'] + '/answers';
+
+        $.ajax({
+          url: uri,
+          dataType: 'json',
+          async: false,
+          success: function(answers) {
+            
+            var target = $('#query-retrieve-3 ul');
+            $('li', target).remove();
+
+            for (var i = 0; i < answers.length; i++) {
+              $.ajax({
+                url: uri + '/' + answers[i] + '/content?simplify',
+                dataType: 'json',
+                async: false,
+                success: function(series) {
+                  var info = $('<a>').html(
+                    ('<h3>{0}</h3>'  + 
+                     '<p>Modality: <b>{1}</b></p>' +
+                     '<p>Protocol name: <b>{2}</b></p>'
+                    ).format(
+                      series['SeriesDescription'],
+                      series['Modality'],
+                      series['ProtocolName']
+                    ));
+
+                  var seriesUri = uri + '/' + answers[i] + '/retrieve';
+                  var retrieve = $('<a>').text('Retrieve').click(function() {
+                    Retrieve(seriesUri);
+                  });
+
+                  target.append($('<li>').append(info).append(retrieve));
+                }
+              });
+            }
+
+            target.listview('refresh');
+          }
+        });
+      }
+    });
+  }
+});
--- a/OrthancServer/DicomProtocol/DicomFindAnswers.cpp	Fri May 29 15:00:24 2015 +0200
+++ b/OrthancServer/DicomProtocol/DicomFindAnswers.cpp	Fri May 29 15:00:45 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	Fri May 29 15:00:24 2015 +0200
+++ b/OrthancServer/DicomProtocol/DicomFindAnswers.h	Fri May 29 15:00:45 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	Fri May 29 15:00:24 2015 +0200
+++ b/OrthancServer/DicomProtocol/DicomUserConnection.cpp	Fri May 29 15:00:45 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,103 @@
     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);
+    }
+
+    if (level == ResourceType_Study)
+    {
+      allowedTags.insert(DICOM_TAG_MODALITIES_IN_STUDY);
+    }
+
+    allowedTags.insert(DICOM_TAG_SPECIFIC_CHARACTER_SET);
+
+    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 +468,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 +497,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 +525,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,67 +539,48 @@
   }
 
 
-  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,
+                                         ResourceType level,
                                          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;
     std::auto_ptr<DcmDataset> dataset(ToDcmtkBridge::Convert(fields));
 
+    switch (level)
+    {
+      case ResourceType_Patient:
+        DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "PATIENT");
+        break;
+
+      case ResourceType_Study:
+        DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "STUDY");
+        break;
+
+      case ResourceType_Series:
+        DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "SERIES");
+        break;
+
+      case ResourceType_Instance:
+        if (manufacturer_ == ModalityManufacturer_ClearCanvas ||
+            manufacturer_ == ModalityManufacturer_Dcm4Chee)
+        {
+          // This is a particular case for ClearCanvas, thanks to Peter Somlo <peter.somlo@gmail.com>.
+          // https://groups.google.com/d/msg/orthanc-users/j-6C3MAVwiw/iolB9hclom8J
+          // http://www.clearcanvas.ca/Home/Community/OldForums/tabid/526/aff/11/aft/14670/afv/topic/Default.aspx
+          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "IMAGE");
+        }
+        else
+        {
+          DU_putStringDOElement(dataset.get(), DcmTagKey(0x0008, 0x0052), "INSTANCE");
+        }
+        break;
+
+      default:
+        throw OrthancException(ErrorCode_ParameterOutOfRange);
+    }
+
     // Figure out which of the accepted presentation contexts should be used
     int presID = ASC_findAcceptedPresentationContextID(pimpl_->assoc_, sopClass);
     if (presID == 0)
@@ -830,33 +869,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, level, move);
+  }
+
+
+  void DicomUserConnection::MovePatient(const std::string& targetAet,
+                                        const std::string& patientId)
+  {
+    DicomMap query;
+    query.SetValue(DICOM_TAG_PATIENT_ID, patientId);
+    MoveInternal(targetAet, ResourceType_Patient, query);
+  }
+
+  void DicomUserConnection::MoveStudy(const std::string& targetAet,
+                                      const std::string& studyUid)
+  {
+    DicomMap query;
+    query.SetValue(DICOM_TAG_STUDY_INSTANCE_UID, studyUid);
+    MoveInternal(targetAet, ResourceType_Study, 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, ResourceType_Series, query);
   }
 
   void DicomUserConnection::MoveInstance(const std::string& targetAet,
@@ -864,11 +956,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, ResourceType_Instance, query);
   }
 
 
--- a/OrthancServer/DicomProtocol/DicomUserConnection.h	Fri May 29 15:00:24 2015 +0200
+++ b/OrthancServer/DicomProtocol/DicomUserConnection.h	Fri May 29 15:00:45 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,9 @@
 
     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,
+                      ResourceType level,
+                      const DicomMap& fields);
 
     void ResetStorageSOPClasses();
 
@@ -150,29 +139,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	Fri May 29 15:00:24 2015 +0200
+++ b/OrthancServer/FromDcmtkBridge.cpp	Fri May 29 15:00:45 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	Fri May 29 15:00:24 2015 +0200
+++ b/OrthancServer/FromDcmtkBridge.h	Fri May 29 15:00:45 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	Fri May 29 15:00:24 2015 +0200
+++ b/OrthancServer/OrthancInitialization.cpp	Fri May 29 15:00:45 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/OrthancMoveRequestHandler.cpp	Fri May 29 15:00:24 2015 +0200
+++ b/OrthancServer/OrthancMoveRequestHandler.cpp	Fri May 29 15:00:45 2015 +0200
@@ -36,6 +36,8 @@
 #include <glog/logging.h>
 
 #include "OrthancInitialization.h"
+#include "FromDcmtkBridge.h"
+#include "../Core/DicomFormat/DicomArray.h"
 
 namespace Orthanc
 {
@@ -132,6 +134,21 @@
   {
     LOG(WARNING) << "Move-SCU request received for AET \"" << aet << "\"";
 
+    {
+      DicomArray query(input);
+      for (size_t i = 0; i < query.GetSize(); i++)
+      {
+        if (!query.GetElement(i).GetValue().IsNull())
+        {
+          LOG(INFO) << "  " << query.GetElement(i).GetTag()
+                    << "  " << FromDcmtkBridge::GetName(query.GetElement(i).GetTag())
+                    << " = " << query.GetElement(i).GetValue().AsString();
+        }
+      }
+    }
+
+
+
     /**
      * Retrieve the query level.
      **/
--- a/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Fri May 29 15:00:24 2015 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestModalities.cpp	Fri May 29 15:00:45 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	Fri May 29 15:00:24 2015 +0200
+++ b/OrthancServer/OrthancRestApi/OrthancRestResources.cpp	Fri May 29 15:00:45 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	Fri May 29 15:00:45 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	Fri May 29 15:00:45 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	Fri May 29 15:00:24 2015 +0200
+++ b/OrthancServer/ServerContext.cpp	Fri May 29 15:00:45 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	Fri May 29 15:00:24 2015 +0200
+++ b/OrthancServer/ServerContext.h	Fri May 29 15:00:45 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	Fri May 29 15:00:24 2015 +0200
+++ b/OrthancServer/ServerIndex.cpp	Fri May 29 15:00:45 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	Fri May 29 15:00:24 2015 +0200
+++ b/OrthancServer/main.cpp	Fri May 29 15:00:45 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	Fri May 29 15:00:24 2015 +0200
+++ b/Resources/Configuration.json	Fri May 29 15:00:45 2015 +0200
@@ -224,6 +224,11 @@
   // to 0, the connection is closed immediately.
   "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,
+
   // When handling a C-Find SCP request, setting this flag to "false"
   // will enable case-insensitive match for PN value representation
   // (such as PatientName). By default, the search is case-insensitive.
--- a/Resources/DicomConformanceStatement.txt	Fri May 29 15:00:24 2015 +0200
+++ b/Resources/DicomConformanceStatement.txt	Fri May 29 15:00:45 2015 +0200
@@ -186,7 +186,16 @@
 
   FINDPatientRootQueryRetrieveInformationModel  | 1.2.840.10008.5.1.4.1.2.1.1
   FINDStudyRootQueryRetrieveInformationModel    | 1.2.840.10008.5.1.4.1.2.2.1
-  FINDStudyRootQueryRetrieveInformationModel    | 1.2.840.10008.5.1.4.1.2.2.1
+
+
+--------------------
+Move SCU Conformance
+--------------------
+
+Orthanc supports the following SOP Classes as an SCU for C-Move:
+
+  MOVEPatientRootQueryRetrieveInformationModel  | 1.2.840.10008.5.1.4.1.2.1.2
+  MOVEStudyRootQueryRetrieveInformationModel    | 1.2.840.10008.5.1.4.1.2.2.2
 
 
 -----------------
--- a/UnitTestsSources/DicomMapTests.cpp	Fri May 29 15:00:24 2015 +0200
+++ b/UnitTestsSources/DicomMapTests.cpp	Fri May 29 15:00:45 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	Fri May 29 15:00:24 2015 +0200
+++ b/UnitTestsSources/MemoryCacheTests.cpp	Fri May 29 15:00:45 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);
+}