changeset 14:1b383403c080

Support of WADO-RS - RetrieveMetadata
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 07 May 2015 17:07:25 +0200
parents e432ee128884
children 0ed8bbf35577
files CMakeLists.txt Core/Dicom.cpp Core/Dicom.h Core/DicomResults.cpp Core/DicomResults.h NEWS Plugin/Plugin.cpp Plugin/QidoRs.cpp Plugin/StowRs.cpp Plugin/WadoRs.cpp Plugin/WadoRs.h Samples/JavaScript/qido-rs.js Status.txt
diffstat 13 files changed, 1240 insertions(+), 814 deletions(-) [+]
line wrap: on
line diff
--- a/CMakeLists.txt	Thu Apr 30 15:53:07 2015 +0200
+++ b/CMakeLists.txt	Thu May 07 17:07:25 2015 +0200
@@ -79,9 +79,10 @@
   ${PUGIXML_SOURCES}
   Core/ChunkedBuffer.cpp
   Core/Configuration.cpp
+  Core/Dicom.cpp
+  Core/DicomResults.cpp
+  Core/MultipartWriter.cpp
   Core/Toolbox.cpp
-  Core/Dicom.cpp
-  Core/MultipartWriter.cpp
   )
 
 add_library(OrthancDicomWeb SHARED ${CORE_SOURCES}
--- a/Core/Dicom.cpp	Thu Apr 30 15:53:07 2015 +0200
+++ b/Core/Dicom.cpp	Thu May 07 17:07:25 2015 +0200
@@ -21,6 +21,7 @@
 #include "Dicom.h"
 
 #include "ChunkedBuffer.h"
+#include "MultipartWriter.h"
 
 #include <gdcmDictEntry.h>
 #include <boost/lexical_cast.hpp>
@@ -75,12 +76,11 @@
   }
 
 
-  bool ParsedDicomFile::GetTag(std::string& result,
-                               const gdcm::Tag& tag,
-                               bool stripSpaces) const
+  static bool GetTag(std::string& result,
+                     const gdcm::DataSet& dataset,
+                     const gdcm::Tag& tag,
+                     bool stripSpaces)
   {
-    const gdcm::DataSet& dataset = GetDataSet();
-
     if (dataset.FindDataElement(tag))
     {
       const gdcm::ByteValue* value = dataset.GetDataElement(tag).GetByteValue();
@@ -101,25 +101,42 @@
   }
 
 
-  std::string ParsedDicomFile::GetTagWithDefault(const gdcm::Tag& tag,
-                                                 const std::string& defaultValue,
-                                                 bool stripSpaces) const
+  static std::string GetTagWithDefault(const gdcm::DataSet& dataset,
+                                       const gdcm::Tag& tag,
+                                       const std::string& defaultValue,
+                                       bool stripSpaces)
   {
     std::string result;
-    if (!GetTag(result, tag, false))
+    if (!GetTag(result, dataset, tag, false))
     {
       result = defaultValue;
     }
 
     if (stripSpaces)
     {
-      result = OrthancPlugins::StripSpaces(result);
+      result = StripSpaces(result);
     }
 
     return result;
   }
 
 
+  bool ParsedDicomFile::GetTag(std::string& result,
+                               const gdcm::Tag& tag,
+                               bool stripSpaces) const
+  {
+    return OrthancPlugins::GetTag(result, GetDataSet(), tag, stripSpaces);
+  }
+
+
+  std::string ParsedDicomFile::GetTagWithDefault(const gdcm::Tag& tag,
+                                                 const std::string& defaultValue,
+                                                 bool stripSpaces) const
+  {
+    return OrthancPlugins::GetTagWithDefault(GetDataSet(), tag, defaultValue, stripSpaces);
+  }
+
+
   static std::string FormatTag(const gdcm::Tag& tag)
   {
     char tmp[16];
@@ -144,7 +161,8 @@
       return "RetrieveURL";
     }
 
-    throw std::runtime_error("Unknown keyword for tag: " + FormatTag(tag));
+    //throw std::runtime_error("Unknown keyword for tag: " + FormatTag(tag));
+    return NULL;
   }
 
 
@@ -166,6 +184,48 @@
   }
 
 
+  static bool IsBulkData(const std::string& vr)
+  {
+    /**
+     * Full list of VR (Value Representations) that are admissible for
+     * being retrieved as bulk data. We commented out some of them, as
+     * they correspond to strings and not to binary data.
+     **/
+    return (vr == "FL" ||
+            vr == "FD" ||
+            //vr == "IS" ||
+            vr == "LT" ||
+            vr == "OB" ||
+            vr == "OD" ||
+            vr == "OF" ||
+            vr == "OW" ||
+            vr == "SL" ||
+            vr == "SS" ||
+            //vr == "ST" ||
+            vr == "UL" ||
+            vr == "UN" ||
+            vr == "US" ||
+            vr == "UT");
+  }
+
+
+  static std::string GetBulkUriRoot(const gdcm::DataSet& dicom)
+  {
+    std::string study, series, instance;
+
+    if (!GetTag(study, dicom, DICOM_TAG_STUDY_INSTANCE_UID, true) ||
+        !GetTag(series, dicom, DICOM_TAG_SERIES_INSTANCE_UID, true) ||
+        !GetTag(instance, dicom, DICOM_TAG_SOP_INSTANCE_UID, true))
+    {
+      return "";
+    }
+    else
+    {
+      return "/wado-rs/studies/" + study + "/series/" + series + "/instances/" + instance + "/bulk/";
+    }
+  }
+
+
   static Encoding DetectEncoding(const gdcm::DataSet& dicom)
   {
     if (!dicom.FindDataElement(DICOM_TAG_SPECIFIC_CHARACTER_SET))
@@ -217,26 +277,38 @@
   static void DicomToXmlInternal(pugi::xml_node& target,
                                  const gdcm::Dict& dictionary,
                                  const gdcm::DataSet& dicom,
-                                 const Encoding sourceEncoding)
+                                 const Encoding sourceEncoding,
+                                 const std::string& bulkUri)
   {
     for (gdcm::DataSet::ConstIterator it = dicom.Begin();
          it != dicom.End(); ++it)  // "*it" represents a "gdcm::DataElement"
     {
+      char path[16];
+      sprintf(path, "%04x%04x", it->GetTag().GetGroup(), it->GetTag().GetElement());
+
       pugi::xml_node node = target.append_child("DicomAttribute");
       node.append_attribute("tag").set_value(FormatTag(it->GetTag()).c_str());
-      node.append_attribute("keyword").set_value(GetKeyword(dictionary, it->GetTag()));
+
+      const char* keyword = GetKeyword(dictionary, it->GetTag());
+      if (keyword != NULL)
+      {
+        node.append_attribute("keyword").set_value(keyword);
+      }
 
       bool isSequence = false;
+      std::string vr;
       if (it->GetTag() == DICOM_TAG_RETRIEVE_URL)
       {
         // The VR of this attribute has changed from UT to UR.
-        node.append_attribute("vr").set_value("UR");
+        vr = "UR";
       }
       else
       {
-        node.append_attribute("vr").set_value(GetVRName(isSequence, dictionary, *it));
+        vr = GetVRName(isSequence, dictionary, *it);
       }
 
+      node.append_attribute("vr").set_value(vr.c_str());
+
       if (isSequence)
       {
         gdcm::SmartPointer<gdcm::SequenceOfItems> seq = it->GetValueAsSQ();
@@ -246,7 +318,24 @@
           pugi::xml_node item = node.append_child("Item");
           std::string number = boost::lexical_cast<std::string>(i);
           item.append_attribute("number").set_value(number.c_str());
-          DicomToXmlInternal(item, dictionary, seq->GetItem(i).GetNestedDataSet(), sourceEncoding);
+
+          std::string childUri;
+          if (!bulkUri.empty())
+          {
+            childUri = bulkUri + std::string(path) + "/" + number + "/";
+          }
+
+          DicomToXmlInternal(item, dictionary, seq->GetItem(i).GetNestedDataSet(), sourceEncoding, childUri);
+        }
+      }
+      else if (IsBulkData(vr))
+      {
+        // Bulk data
+        if (!bulkUri.empty())
+        {
+          pugi::xml_node value = node.append_child("BulkData");
+          std::string uri = bulkUri + std::string(path);
+          value.append_attribute("uri").set_value(uri.c_str());
         }
       }
       else
@@ -265,9 +354,10 @@
   }
 
 
-  void DicomToXml(pugi::xml_document& target,
-                  const gdcm::Dict& dictionary,
-                  const gdcm::DataSet& dicom)
+  static void DicomToXml(pugi::xml_document& target,
+                         const gdcm::Dict& dictionary,
+                         const gdcm::DataSet& dicom,
+                         const std::string& bulkUriRoot)
   {
     pugi::xml_node root = target.append_child("NativeDicomModel");
     root.append_attribute("xmlns").set_value("http://dicom.nema.org/PS3.19/models/NativeDICOM");
@@ -275,7 +365,7 @@
     root.append_attribute("xmlns:xsi").set_value("http://www.w3.org/2001/XMLSchema-instance");
 
     Encoding encoding = DetectEncoding(dicom);
-    DicomToXmlInternal(root, dictionary, dicom, encoding);
+    DicomToXmlInternal(root, dictionary, dicom, encoding, bulkUriRoot);
 
     pugi::xml_node decl = target.prepend_child(pugi::node_declaration);
     decl.append_attribute("version").set_value("1.0");
@@ -286,6 +376,7 @@
   static void DicomToJsonInternal(Json::Value& target,
                                   const gdcm::Dict& dictionary,
                                   const gdcm::DataSet& dicom,
+                                  const std::string& bulkUri,
                                   Encoding sourceEncoding)
   {
     target = Json::objectValue;
@@ -293,19 +384,25 @@
     for (gdcm::DataSet::ConstIterator it = dicom.Begin();
          it != dicom.End(); ++it)  // "*it" represents a "gdcm::DataElement"
     {
+      char path[16];
+      sprintf(path, "%04x%04x", it->GetTag().GetGroup(), it->GetTag().GetElement());
+
       Json::Value node = Json::objectValue;
 
       bool isSequence = false;
+      std::string vr;
       if (it->GetTag() == DICOM_TAG_RETRIEVE_URL)
       {
         // The VR of this attribute has changed from UT to UR.
-        node["vr"] = "UR";
+        vr = "UR";
       }
       else
       {
-        node["vr"] = GetVRName(isSequence, dictionary, *it);
+        vr = GetVRName(isSequence, dictionary, *it);
       }
 
+      node["vr"] = vr.c_str();
+
       if (isSequence)
       {
         // Deal with sequences
@@ -316,10 +413,26 @@
         for (gdcm::SequenceOfItems::SizeType i = 1; i <= seq->GetNumberOfItems(); i++)
         {
           Json::Value child;
-          DicomToJsonInternal(child, dictionary, seq->GetItem(i).GetNestedDataSet(), sourceEncoding);
+
+          std::string childUri;
+          if (!bulkUri.empty())
+          {
+            std::string number = boost::lexical_cast<std::string>(i);
+            childUri = bulkUri + std::string(path) + "/" + number + "/";
+          }
+
+          DicomToJsonInternal(child, dictionary, seq->GetItem(i).GetNestedDataSet(), childUri, sourceEncoding);
           node["Value"].append(child);
         }
       }
+      else if (IsBulkData(vr))
+      {
+        // Bulk data
+        if (!bulkUri.empty())
+        {
+          node["BulkDataURI"] = bulkUri + std::string(path);
+        }
+      }
       else
       {
         // Deal with other value representations
@@ -337,24 +450,32 @@
   }
 
 
-  void DicomToJson(Json::Value& target,
-                   const gdcm::Dict& dictionary,
-                   const gdcm::DataSet& dicom)
+  static void DicomToJson(Json::Value& target,
+                          const gdcm::Dict& dictionary,
+                          const gdcm::DataSet& dicom,
+                          const std::string& bulkUriRoot)
   {
     Encoding encoding = DetectEncoding(dicom);
-    DicomToJsonInternal(target, dictionary, dicom, encoding);
+    DicomToJsonInternal(target, dictionary, dicom, bulkUriRoot, encoding);
   }
 
 
   void GenerateSingleDicomAnswer(std::string& result,
                                  const gdcm::Dict& dictionary,
                                  const gdcm::DataSet& dicom,
-                                 bool isXml)
+                                 bool isXml,
+                                 bool isBulkAccessible)
   {
+    std::string bulkUriRoot;
+    if (isBulkAccessible)
+    {
+      bulkUriRoot = GetBulkUriRoot(dicom);
+    }
+
     if (isXml)
     {
       pugi::xml_document doc;
-      DicomToXml(doc, dictionary, dicom);
+      DicomToXml(doc, dictionary, dicom, bulkUriRoot);
     
       ChunkedBufferWriter writer;
       doc.save(writer, "  ", pugi::format_default, pugi::encoding_utf8);
@@ -364,7 +485,7 @@
     else
     {
       Json::Value v;
-      DicomToJson(v, dictionary, dicom);
+      DicomToJson(v, dictionary, dicom, bulkUriRoot);
 
       Json::FastWriter writer;
       result = writer.write(v); 
@@ -376,10 +497,11 @@
                    OrthancPluginRestOutput* output,
                    const gdcm::Dict& dictionary,
                    const gdcm::DataSet& dicom,
-                   bool isXml)
+                   bool isXml,
+                   bool isBulkAccessible)
   {
     std::string answer;
-    GenerateSingleDicomAnswer(answer, dictionary, dicom, isXml);
+    GenerateSingleDicomAnswer(answer, dictionary, dicom, isXml, isBulkAccessible);
     OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), 
                               isXml ? "application/dicom+xml" : "application/json");
   }
--- a/Core/Dicom.h	Thu Apr 30 15:53:07 2015 +0200
+++ b/Core/Dicom.h	Thu May 07 17:07:25 2015 +0200
@@ -26,6 +26,7 @@
 #include <gdcmDataSet.h>
 #include <pugixml.hpp>
 #include <gdcmDict.h>
+#include <list>
 
 
 namespace OrthancPlugins
@@ -74,22 +75,16 @@
   };
 
 
-  void DicomToXml(pugi::xml_document& target,
-                  const gdcm::Dict& dictionary,
-                  const gdcm::DataSet& dicom);
-
-  void DicomToJson(Json::Value& target,
-                  const gdcm::Dict& dictionary,
-                   const gdcm::DataSet& dicom);
-
   void GenerateSingleDicomAnswer(std::string& result,
                                  const gdcm::Dict& dictionary,
                                  const gdcm::DataSet& dicom,
-                                 bool isXml);
+                                 bool isXml,
+                                 bool isBulkAccessible);
 
   void AnswerDicom(OrthancPluginContext* context,
                    OrthancPluginRestOutput* output,
                    const gdcm::Dict& dictionary,
                    const gdcm::DataSet& dicom,
-                   bool isXml);
+                   bool isXml,
+                   bool isBulkAccessible);
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomResults.cpp	Thu May 07 17:07:25 2015 +0200
@@ -0,0 +1,78 @@
+/**
+ * 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 Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * 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
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#include "DicomResults.h"
+
+#include "Dicom.h"
+
+namespace OrthancPlugins
+{
+  DicomResults::DicomResults(const gdcm::Dict& dictionary,
+                             bool isXml,
+                             bool isBulkAccessible) :
+    dictionary_(dictionary),
+    xmlWriter_("application/dicom+xml"),
+    isFirst_(true),
+    isXml_(isXml),
+    isBulkAccessible_(isBulkAccessible)
+  {
+    jsonWriter_.AddChunk("[\n");
+  }
+
+  void DicomResults::Add(const gdcm::DataSet& dicom)
+  {
+    if (isXml_)
+    {
+      std::string answer;
+      GenerateSingleDicomAnswer(answer, dictionary_, dicom, true, isBulkAccessible_);
+      xmlWriter_.AddPart(answer);
+    }
+    else
+    {
+      if (!isFirst_)
+      {
+        jsonWriter_.AddChunk(",\n");
+      }
+
+      std::string item;
+      GenerateSingleDicomAnswer(item, dictionary_, dicom, false, isBulkAccessible_);
+      jsonWriter_.AddChunk(item);
+    }
+
+    isFirst_ = false;
+  }
+
+  void DicomResults::Answer(OrthancPluginContext* context,
+                            OrthancPluginRestOutput* output)
+  {
+    if (isXml_)
+    {
+      xmlWriter_.Answer(context, output);
+    }
+    else
+    {
+      jsonWriter_.AddChunk("]\n");
+
+      std::string answer;
+      jsonWriter_.Flatten(answer);
+      OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json");
+    }
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Core/DicomResults.h	Thu May 07 17:07:25 2015 +0200
@@ -0,0 +1,51 @@
+/**
+ * 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 Affero General Public License
+ * as published by the Free Software Foundation, either version 3 of
+ * the License, or (at your option) any later version.
+ *
+ * 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
+ * Affero General Public License for more details.
+ * 
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ **/
+
+
+#pragma once
+
+#include "MultipartWriter.h"
+#include "ChunkedBuffer.h"
+
+#include <gdcmDataSet.h>
+#include <gdcmDict.h>
+
+namespace OrthancPlugins
+{
+  class DicomResults
+  {
+  private:
+    const gdcm::Dict& dictionary_;
+    MultipartWriter   xmlWriter_;  // Used for XML output
+    ChunkedBuffer     jsonWriter_;  // Used for JSON output
+    bool              isFirst_; 
+    bool              isXml_;
+    bool              isBulkAccessible_;
+
+  public:
+    DicomResults(const gdcm::Dict& dictionary,
+                 bool isXml,
+                 bool isBulkAccessible);
+
+    void Add(const gdcm::DataSet& dicom);
+
+    void Answer(OrthancPluginContext* context,
+                OrthancPluginRestOutput* output);
+  };
+}
--- a/NEWS	Thu Apr 30 15:53:07 2015 +0200
+++ b/NEWS	Thu May 07 17:07:25 2015 +0200
@@ -3,6 +3,7 @@
 
 No official release yet. Still work in progress.
 
+* Support of WADO-RS - RetrieveMetadata
 * Support of Visual Studio 2008
 * Support of FreeBSD
 * Support of OS X
--- a/Plugin/Plugin.cpp	Thu Apr 30 15:53:07 2015 +0200
+++ b/Plugin/Plugin.cpp	Thu May 07 17:07:25 2015 +0200
@@ -74,6 +74,9 @@
     OrthancPluginRegisterRestCallback(context, "/wado-rs/studies/([^/]*)", RetrieveDicomStudy);
     OrthancPluginRegisterRestCallback(context, "/wado-rs/studies/([^/]*)/series/([^/]*)", RetrieveDicomSeries);
     OrthancPluginRegisterRestCallback(context, "/wado-rs/studies/([^/]*)/series/([^/]*)/instances/([^/]*)", RetrieveDicomInstance);
+    OrthancPluginRegisterRestCallback(context, "/wado-rs/studies/([^/]*)/metadata", RetrieveStudyMetadata);
+    OrthancPluginRegisterRestCallback(context, "/wado-rs/studies/([^/]*)/series/([^/]*)/metadata", RetrieveSeriesMetadata);
+    OrthancPluginRegisterRestCallback(context, "/wado-rs/studies/([^/]*)/series/([^/]*)/instances/([^/]*)/metadata", RetrieveInstanceMetadata);
 
     // STOW-RS callbacks
     OrthancPluginRegisterRestCallback(context, "/stow-rs/studies", StowCallback);
--- a/Plugin/QidoRs.cpp	Thu Apr 30 15:53:07 2015 +0200
+++ b/Plugin/QidoRs.cpp	Thu May 07 17:07:25 2015 +0200
@@ -23,6 +23,7 @@
 #include "Plugin.h"
 #include "StowRs.h"  // For IsXmlExpected()
 #include "../Core/Dicom.h"
+#include "../Core/DicomResults.h"
 #include "../Core/Toolbox.h"
 #include "../Core/Configuration.h"
 #include "../Core/MultipartWriter.h"
@@ -40,768 +41,692 @@
 
 
 
-enum QueryLevel
+namespace
 {
-  QueryLevel_Study,
-  QueryLevel_Series,
-  QueryLevel_Instance
-};
+
+  enum QueryLevel
+  {
+    QueryLevel_Study,
+    QueryLevel_Series,
+    QueryLevel_Instance
+  };
 
 
-class ModuleMatcher
-{
-private:
-  typedef std::map<gdcm::Tag, std::string>  Filters;
+  class ModuleMatcher
+  {
+  private:
+    typedef std::map<gdcm::Tag, std::string>  Filters;
 
-  const gdcm::Dict&     dictionary_;
-  bool                  fuzzy_;
-  unsigned int          offset_;
-  unsigned int          limit_;
-  std::list<gdcm::Tag>  includeFields_;
-  bool                  includeAllFields_;
-  Filters               filters_;
+    const gdcm::Dict&     dictionary_;
+    bool                  fuzzy_;
+    unsigned int          offset_;
+    unsigned int          limit_;
+    std::list<gdcm::Tag>  includeFields_;
+    bool                  includeAllFields_;
+    Filters               filters_;
 
 
 
-  static inline uint16_t GetCharValue(char c)
-  {
-    if (c >= '0' && c <= '9')
-      return c - '0';
-    else if (c >= 'a' && c <= 'f')
-      return c - 'a' + 10;
-    else if (c >= 'A' && c <= 'F')
-      return c - 'A' + 10;
-    else
-      return 0;
-  }
+    static inline uint16_t GetCharValue(char c)
+    {
+      if (c >= '0' && c <= '9')
+        return c - '0';
+      else if (c >= 'a' && c <= 'f')
+        return c - 'a' + 10;
+      else if (c >= 'A' && c <= 'F')
+        return c - 'A' + 10;
+      else
+        return 0;
+    }
 
-  static inline uint16_t GetTagValue(const char* c)
-  {
-    return ((GetCharValue(c[0]) << 12) + 
-            (GetCharValue(c[1]) << 8) + 
-            (GetCharValue(c[2]) << 4) + 
-            GetCharValue(c[3]));
-  }
+    static inline uint16_t GetTagValue(const char* c)
+    {
+      return ((GetCharValue(c[0]) << 12) + 
+              (GetCharValue(c[1]) << 8) + 
+              (GetCharValue(c[2]) << 4) + 
+              GetCharValue(c[3]));
+    }
 
 
-  gdcm::Tag  ParseTag(const std::string& key) const
-  {
-    if (key.size() == 8 &&
-        isxdigit(key[0]) &&
-        isxdigit(key[1]) &&
-        isxdigit(key[2]) &&
-        isxdigit(key[3]) &&
-        isxdigit(key[4]) &&
-        isxdigit(key[5]) &&
-        isxdigit(key[6]) &&
-        isxdigit(key[7]))        
+    gdcm::Tag  ParseTag(const std::string& key) const
     {
-      return gdcm::Tag(GetTagValue(key.c_str()),
-                       GetTagValue(key.c_str() + 4));
-    }
-    else
-    {
-      gdcm::Tag tag;
-      dictionary_.GetDictEntryByKeyword(key.c_str(), tag);
-
-      if (tag.IsIllegal() || tag.IsPrivate())
+      if (key.size() == 8 &&
+          isxdigit(key[0]) &&
+          isxdigit(key[1]) &&
+          isxdigit(key[2]) &&
+          isxdigit(key[3]) &&
+          isxdigit(key[4]) &&
+          isxdigit(key[5]) &&
+          isxdigit(key[6]) &&
+          isxdigit(key[7]))        
       {
-        if (key.find('.') != std::string::npos)
-        {
-          throw std::runtime_error("This QIDO-RS implementation does not support search over sequences: " + key);
-        }
-        else
-        {
-          throw std::runtime_error("Illegal tag name in QIDO-RS: " + key);
-        }
+        return gdcm::Tag(GetTagValue(key.c_str()),
+                         GetTagValue(key.c_str() + 4));
       }
-
-      return tag;
-    }
-  }
-
-
-  static bool IsWildcard(const std::string& constraint)
-  {
-    return (constraint.find('-') != std::string::npos ||
-            constraint.find('*') != std::string::npos ||
-            constraint.find('\\') != std::string::npos ||
-            constraint.find('?') != std::string::npos);
-  }
+      else
+      {
+        gdcm::Tag tag;
+        dictionary_.GetDictEntryByKeyword(key.c_str(), tag);
 
-  static bool ApplyRangeConstraint(const std::string& value,
-                                   const std::string& constraint)
-  {
-    size_t separator = constraint.find('-');
-    std::string lower(constraint.substr(0, separator));
-    std::string upper(constraint.substr(separator + 1));
-    std::string v(value);
-
-    OrthancPlugins::ToLowerCase(lower);
-    OrthancPlugins::ToLowerCase(upper);
-    OrthancPlugins::ToLowerCase(v);
-
-    if (lower.size() == 0 && upper.size() == 0)
-    {
-      return false;
-    }
-
-    if (lower.size() == 0)
-    {
-      return v <= upper;
-    }
+        if (tag.IsIllegal() || tag.IsPrivate())
+        {
+          if (key.find('.') != std::string::npos)
+          {
+            throw std::runtime_error("This QIDO-RS implementation does not support search over sequences: " + key);
+          }
+          else
+          {
+            throw std::runtime_error("Illegal tag name in QIDO-RS: " + key);
+          }
+        }
 
-    if (upper.size() == 0)
-    {
-      return v >= lower;
-    }
-    
-    return (v >= lower && v <= upper);
-  }
-
-
-  static bool ApplyListConstraint(const std::string& value,
-                                  const std::string& constraint)
-  {
-    std::string v1(value);
-    OrthancPlugins::ToLowerCase(v1);
-
-    std::vector<std::string> items;
-    OrthancPlugins::TokenizeString(items, constraint, '\\');
-
-    for (size_t i = 0; i < items.size(); i++)
-    {
-      std::string lower(items[i]);
-      OrthancPlugins::ToLowerCase(lower);
-      if (lower == v1)
-      {
-        return true;
+        return tag;
       }
     }
 
-    return false;
-  }
 
-
-  static std::string WildcardToRegularExpression(const std::string& source)
-  {
-    std::string result = source;
-
-    // Escape all special characters
-    boost::replace_all(result, "\\", "\\\\");
-    boost::replace_all(result, "^", "\\^");
-    boost::replace_all(result, ".", "\\.");
-    boost::replace_all(result, "$", "\\$");
-    boost::replace_all(result, "|", "\\|");
-    boost::replace_all(result, "(", "\\(");
-    boost::replace_all(result, ")", "\\)");
-    boost::replace_all(result, "[", "\\[");
-    boost::replace_all(result, "]", "\\]");
-    boost::replace_all(result, "+", "\\+");
-    boost::replace_all(result, "/", "\\/");
-    boost::replace_all(result, "{", "\\{");
-    boost::replace_all(result, "}", "\\}");
-
-    // Convert wildcards '*' and '?' to their regex equivalents
-    boost::replace_all(result, "?", ".");
-    boost::replace_all(result, "*", ".*");
-
-    return result;
-  }
-
-
-  static bool Matches(const std::string& value,
-                      const std::string& constraint)
-  {
-    // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
-    // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html  
-
-    if (constraint.find('-') != std::string::npos)
+    static bool IsWildcard(const std::string& constraint)
     {
-      return ApplyRangeConstraint(value, constraint);
-    }
-    
-    if (constraint.find('\\') != std::string::npos)
-    {
-      return ApplyListConstraint(value, constraint);
+      return (constraint.find('-') != std::string::npos ||
+              constraint.find('*') != std::string::npos ||
+              constraint.find('\\') != std::string::npos ||
+              constraint.find('?') != std::string::npos);
     }
 
-    if (constraint.find('*') != std::string::npos ||
-        constraint.find('?') != std::string::npos)
-    {
-      boost::regex pattern(WildcardToRegularExpression(constraint),
-                           boost::regex::icase /* case insensitive search */);
-      return boost::regex_match(value, pattern);
-    }
-    else
-    {
-      std::string v(value), c(constraint);
-      OrthancPlugins::ToLowerCase(v);
-      OrthancPlugins::ToLowerCase(c);
-      return v == c;
-    }
-  }
-
-
-
-  static void AddResultAttributesForLevel(std::list<gdcm::Tag>& result,
-                                          QueryLevel level)
-  {
-    switch (level)
+    static bool ApplyRangeConstraint(const std::string& value,
+                                     const std::string& constraint)
     {
-      case QueryLevel_Study:
-        // http://medical.nema.org/medical/dicom/current/output/html/part18.html#table_6.7.1-2
-        result.push_back(gdcm::Tag(0x0008, 0x0005));  // Specific Character Set
-        result.push_back(gdcm::Tag(0x0008, 0x0020));  // Study Date
-        result.push_back(gdcm::Tag(0x0008, 0x0030));  // Study Time
-        result.push_back(gdcm::Tag(0x0008, 0x0050));  // Accession Number
-        result.push_back(gdcm::Tag(0x0008, 0x0056));  // Instance Availability
-        result.push_back(gdcm::Tag(0x0008, 0x0061));  // Modalities in Study
-        result.push_back(gdcm::Tag(0x0008, 0x0090));  // Referring Physician's Name
-        result.push_back(gdcm::Tag(0x0008, 0x0201));  // Timezone Offset From UTC
-        //result.push_back(gdcm::Tag(0x0008, 0x1190));  // Retrieve URL  => SPECIAL CASE
-        result.push_back(gdcm::Tag(0x0010, 0x0010));  // Patient's Name
-        result.push_back(gdcm::Tag(0x0010, 0x0020));  // Patient ID
-        result.push_back(gdcm::Tag(0x0010, 0x0030));  // Patient's Birth Date
-        result.push_back(gdcm::Tag(0x0010, 0x0040));  // Patient's Sex
-        result.push_back(gdcm::Tag(0x0020, 0x000D));  // Study Instance UID
-        result.push_back(gdcm::Tag(0x0020, 0x0010));  // Study ID
-        result.push_back(gdcm::Tag(0x0020, 0x1206));  // Number of Study Related Series
-        result.push_back(gdcm::Tag(0x0020, 0x1208));  // Number of Study Related Instances
-        break;
-
-      case QueryLevel_Series:
-        // http://medical.nema.org/medical/dicom/current/output/html/part18.html#table_6.7.1-2a
-        result.push_back(gdcm::Tag(0x0008, 0x0005));  // Specific Character Set
-        result.push_back(gdcm::Tag(0x0008, 0x0056));  // Modality
-        result.push_back(gdcm::Tag(0x0008, 0x0201));  // Timezone Offset From UTC
-        result.push_back(gdcm::Tag(0x0008, 0x103E));  // Series Description
-        //result.push_back(gdcm::Tag(0x0008, 0x1190));  // Retrieve URL  => SPECIAL CASE
-        result.push_back(gdcm::Tag(0x0020, 0x000E));  // Series Instance UID
-        result.push_back(gdcm::Tag(0x0020, 0x0011));  // Series Number
-        result.push_back(gdcm::Tag(0x0020, 0x1209));  // Number of Series Related Instances
-        result.push_back(gdcm::Tag(0x0040, 0x0244));  // Performed Procedure Step Start Date
-        result.push_back(gdcm::Tag(0x0040, 0x0245));  // Performed Procedure Step Start Time
-        result.push_back(gdcm::Tag(0x0040, 0x0275));  // Request Attribute Sequence
-        break;
-
-      case QueryLevel_Instance:
-        // http://medical.nema.org/medical/dicom/current/output/html/part18.html#table_6.7.1-2b
-        result.push_back(gdcm::Tag(0x0008, 0x0005));  // Specific Character Set
-        result.push_back(gdcm::Tag(0x0008, 0x0016));  // SOP Class UID
-        result.push_back(gdcm::Tag(0x0008, 0x0018));  // SOP Instance UID
-        result.push_back(gdcm::Tag(0x0008, 0x0056));  // Instance Availability
-        result.push_back(gdcm::Tag(0x0008, 0x0201));  // Timezone Offset From UTC
-        result.push_back(gdcm::Tag(0x0008, 0x1190));  // Retrieve URL
-        result.push_back(gdcm::Tag(0x0020, 0x0013));  // Instance Number
-        result.push_back(gdcm::Tag(0x0028, 0x0010));  // Rows
-        result.push_back(gdcm::Tag(0x0028, 0x0011));  // Columns
-        result.push_back(gdcm::Tag(0x0028, 0x0100));  // Bits Allocated
-        result.push_back(gdcm::Tag(0x0028, 0x0008));  // Number of Frames
-        break;
-
-      default:
-        throw std::runtime_error("Internal error");
-    }
-  }
-
-
+      size_t separator = constraint.find('-');
+      std::string lower(constraint.substr(0, separator));
+      std::string upper(constraint.substr(separator + 1));
+      std::string v(value);
 
-public:
-  ModuleMatcher(const OrthancPluginHttpRequest* request) :
-  dictionary_(gdcm::Global::GetInstance().GetDicts().GetPublicDict()),
-  fuzzy_(false),
-  offset_(0),
-  limit_(0),
-  includeAllFields_(false)
-  {
-    for (int32_t i = 0; i < request->getCount; i++)
-    {
-      std::string key(request->getKeys[i]);
-      std::string value(request->getValues[i]);
+      OrthancPlugins::ToLowerCase(lower);
+      OrthancPlugins::ToLowerCase(upper);
+      OrthancPlugins::ToLowerCase(v);
 
-      if (key == "limit")
-      {
-        limit_ = boost::lexical_cast<unsigned int>(value);
-      }
-      else if (key == "offset")
-      {
-        offset_ = boost::lexical_cast<unsigned int>(value);
-      }
-      else if (key == "fuzzymatching")
-      {
-        if (value == "true")
-        {
-          fuzzy_ = true;
-        }
-        else if (value == "false")
-        {
-          fuzzy_ = false;
-        }
-        else
-        {
-          throw std::runtime_error("Not a proper value for fuzzy matching (true or false): " + value);
-        }
-      }
-      else if (key == "includefield")
-      {
-        if (key == "all")
-        {
-          includeAllFields_ = true;
-        }
-        else
-        {
-          includeFields_.push_back(ParseTag(key));
-        }
-      }
-      else
-      {
-        filters_[ParseTag(key)] = value;
-      }
-    }
-  }
-
-  unsigned int GetLimit() const
-  {
-    return limit_;
-  }
-
-  unsigned int GetOffset() const
-  {
-    return offset_;
-  }
-
-  void AddFilter(const gdcm::Tag& tag,
-                 const std::string& constraint)
-  {
-    filters_[tag] = constraint;
-  }
-
-  bool LookupExactFilter(std::string& constraint,
-                         const gdcm::Tag& tag) const
-  {
-    Filters::const_iterator it = filters_.find(tag);
-    if (it != filters_.end() &&
-        !IsWildcard(it->second))
-    {
-      constraint = it->second;
-      return true;
-    }
-    else
-    {
-      return false;
-    }
-  }
-
-  bool Matches(const OrthancPlugins::ParsedDicomFile& dicom) const
-  {
-    for (Filters::const_iterator it = filters_.begin();
-         it != filters_.end(); ++it)
-    {
-      std::string value;
-      if (!dicom.GetTag(value, it->first, true))
+      if (lower.size() == 0 && upper.size() == 0)
       {
         return false;
       }
 
-      if (!Matches(value, it->second))
+      if (lower.size() == 0)
+      {
+        return v <= upper;
+      }
+
+      if (upper.size() == 0)
+      {
+        return v >= lower;
+      }
+    
+      return (v >= lower && v <= upper);
+    }
+
+
+    static bool ApplyListConstraint(const std::string& value,
+                                    const std::string& constraint)
+    {
+      std::string v1(value);
+      OrthancPlugins::ToLowerCase(v1);
+
+      std::vector<std::string> items;
+      OrthancPlugins::TokenizeString(items, constraint, '\\');
+
+      for (size_t i = 0; i < items.size(); i++)
+      {
+        std::string lower(items[i]);
+        OrthancPlugins::ToLowerCase(lower);
+        if (lower == v1)
+        {
+          return true;
+        }
+      }
+
+      return false;
+    }
+
+
+    static std::string WildcardToRegularExpression(const std::string& source)
+    {
+      std::string result = source;
+
+      // Escape all special characters
+      boost::replace_all(result, "\\", "\\\\");
+      boost::replace_all(result, "^", "\\^");
+      boost::replace_all(result, ".", "\\.");
+      boost::replace_all(result, "$", "\\$");
+      boost::replace_all(result, "|", "\\|");
+      boost::replace_all(result, "(", "\\(");
+      boost::replace_all(result, ")", "\\)");
+      boost::replace_all(result, "[", "\\[");
+      boost::replace_all(result, "]", "\\]");
+      boost::replace_all(result, "+", "\\+");
+      boost::replace_all(result, "/", "\\/");
+      boost::replace_all(result, "{", "\\{");
+      boost::replace_all(result, "}", "\\}");
+
+      // Convert wildcards '*' and '?' to their regex equivalents
+      boost::replace_all(result, "?", ".");
+      boost::replace_all(result, "*", ".*");
+
+      return result;
+    }
+
+
+    static bool Matches(const std::string& value,
+                        const std::string& constraint)
+    {
+      // http://www.itk.org/Wiki/DICOM_QueryRetrieve_Explained
+      // http://dicomiseasy.blogspot.be/2012/01/dicom-queryretrieve-part-i.html  
+
+      if (constraint.find('-') != std::string::npos)
+      {
+        return ApplyRangeConstraint(value, constraint);
+      }
+    
+      if (constraint.find('\\') != std::string::npos)
+      {
+        return ApplyListConstraint(value, constraint);
+      }
+
+      if (constraint.find('*') != std::string::npos ||
+          constraint.find('?') != std::string::npos)
+      {
+        boost::regex pattern(WildcardToRegularExpression(constraint),
+                             boost::regex::icase /* case insensitive search */);
+        return boost::regex_match(value, pattern);
+      }
+      else
+      {
+        std::string v(value), c(constraint);
+        OrthancPlugins::ToLowerCase(v);
+        OrthancPlugins::ToLowerCase(c);
+        return v == c;
+      }
+    }
+
+
+
+    static void AddResultAttributesForLevel(std::list<gdcm::Tag>& result,
+                                            QueryLevel level)
+    {
+      switch (level)
+      {
+        case QueryLevel_Study:
+          // http://medical.nema.org/medical/dicom/current/output/html/part18.html#table_6.7.1-2
+          result.push_back(gdcm::Tag(0x0008, 0x0005));  // Specific Character Set
+          result.push_back(gdcm::Tag(0x0008, 0x0020));  // Study Date
+          result.push_back(gdcm::Tag(0x0008, 0x0030));  // Study Time
+          result.push_back(gdcm::Tag(0x0008, 0x0050));  // Accession Number
+          result.push_back(gdcm::Tag(0x0008, 0x0056));  // Instance Availability
+          result.push_back(gdcm::Tag(0x0008, 0x0061));  // Modalities in Study
+          result.push_back(gdcm::Tag(0x0008, 0x0090));  // Referring Physician's Name
+          result.push_back(gdcm::Tag(0x0008, 0x0201));  // Timezone Offset From UTC
+          //result.push_back(gdcm::Tag(0x0008, 0x1190));  // Retrieve URL  => SPECIAL CASE
+          result.push_back(gdcm::Tag(0x0010, 0x0010));  // Patient's Name
+          result.push_back(gdcm::Tag(0x0010, 0x0020));  // Patient ID
+          result.push_back(gdcm::Tag(0x0010, 0x0030));  // Patient's Birth Date
+          result.push_back(gdcm::Tag(0x0010, 0x0040));  // Patient's Sex
+          result.push_back(gdcm::Tag(0x0020, 0x000D));  // Study Instance UID
+          result.push_back(gdcm::Tag(0x0020, 0x0010));  // Study ID
+          result.push_back(gdcm::Tag(0x0020, 0x1206));  // Number of Study Related Series
+          result.push_back(gdcm::Tag(0x0020, 0x1208));  // Number of Study Related Instances
+          break;
+
+        case QueryLevel_Series:
+          // http://medical.nema.org/medical/dicom/current/output/html/part18.html#table_6.7.1-2a
+          result.push_back(gdcm::Tag(0x0008, 0x0005));  // Specific Character Set
+          result.push_back(gdcm::Tag(0x0008, 0x0056));  // Modality
+          result.push_back(gdcm::Tag(0x0008, 0x0201));  // Timezone Offset From UTC
+          result.push_back(gdcm::Tag(0x0008, 0x103E));  // Series Description
+          //result.push_back(gdcm::Tag(0x0008, 0x1190));  // Retrieve URL  => SPECIAL CASE
+          result.push_back(gdcm::Tag(0x0020, 0x000E));  // Series Instance UID
+          result.push_back(gdcm::Tag(0x0020, 0x0011));  // Series Number
+          result.push_back(gdcm::Tag(0x0020, 0x1209));  // Number of Series Related Instances
+          result.push_back(gdcm::Tag(0x0040, 0x0244));  // Performed Procedure Step Start Date
+          result.push_back(gdcm::Tag(0x0040, 0x0245));  // Performed Procedure Step Start Time
+          result.push_back(gdcm::Tag(0x0040, 0x0275));  // Request Attribute Sequence
+          break;
+
+        case QueryLevel_Instance:
+          // http://medical.nema.org/medical/dicom/current/output/html/part18.html#table_6.7.1-2b
+          result.push_back(gdcm::Tag(0x0008, 0x0005));  // Specific Character Set
+          result.push_back(gdcm::Tag(0x0008, 0x0016));  // SOP Class UID
+          result.push_back(gdcm::Tag(0x0008, 0x0018));  // SOP Instance UID
+          result.push_back(gdcm::Tag(0x0008, 0x0056));  // Instance Availability
+          result.push_back(gdcm::Tag(0x0008, 0x0201));  // Timezone Offset From UTC
+          result.push_back(gdcm::Tag(0x0008, 0x1190));  // Retrieve URL
+          result.push_back(gdcm::Tag(0x0020, 0x0013));  // Instance Number
+          result.push_back(gdcm::Tag(0x0028, 0x0010));  // Rows
+          result.push_back(gdcm::Tag(0x0028, 0x0011));  // Columns
+          result.push_back(gdcm::Tag(0x0028, 0x0100));  // Bits Allocated
+          result.push_back(gdcm::Tag(0x0028, 0x0008));  // Number of Frames
+          break;
+
+        default:
+          throw std::runtime_error("Internal error");
+      }
+    }
+
+
+
+  public:
+    ModuleMatcher(const OrthancPluginHttpRequest* request) :
+      dictionary_(gdcm::Global::GetInstance().GetDicts().GetPublicDict()),
+      fuzzy_(false),
+      offset_(0),
+      limit_(0),
+      includeAllFields_(false)
+    {
+      for (int32_t i = 0; i < request->getCount; i++)
+      {
+        std::string key(request->getKeys[i]);
+        std::string value(request->getValues[i]);
+
+        if (key == "limit")
+        {
+          limit_ = boost::lexical_cast<unsigned int>(value);
+        }
+        else if (key == "offset")
+        {
+          offset_ = boost::lexical_cast<unsigned int>(value);
+        }
+        else if (key == "fuzzymatching")
+        {
+          if (value == "true")
+          {
+            fuzzy_ = true;
+          }
+          else if (value == "false")
+          {
+            fuzzy_ = false;
+          }
+          else
+          {
+            throw std::runtime_error("Not a proper value for fuzzy matching (true or false): " + value);
+          }
+        }
+        else if (key == "includefield")
+        {
+          if (key == "all")
+          {
+            includeAllFields_ = true;
+          }
+          else
+          {
+            includeFields_.push_back(ParseTag(key));
+          }
+        }
+        else
+        {
+          filters_[ParseTag(key)] = value;
+        }
+      }
+    }
+
+    unsigned int GetLimit() const
+    {
+      return limit_;
+    }
+
+    unsigned int GetOffset() const
+    {
+      return offset_;
+    }
+
+    void AddFilter(const gdcm::Tag& tag,
+                   const std::string& constraint)
+    {
+      filters_[tag] = constraint;
+    }
+
+    bool LookupExactFilter(std::string& constraint,
+                           const gdcm::Tag& tag) const
+    {
+      Filters::const_iterator it = filters_.find(tag);
+      if (it != filters_.end() &&
+          !IsWildcard(it->second))
+      {
+        constraint = it->second;
+        return true;
+      }
+      else
       {
         return false;
       }
     }
 
-    return true;
-  }
-
-
-  void ExtractFields(gdcm::DataSet& result,
-                     const OrthancPlugins::ParsedDicomFile& dicom,
-                     const std::string& wadoBase,
-                     QueryLevel level) const
-  {
-    std::list<gdcm::Tag> fields = includeFields_;
-
-    // The list of attributes for this query level
-    AddResultAttributesForLevel(fields, level);
+    bool Matches(const OrthancPlugins::ParsedDicomFile& dicom) const
+    {
+      for (Filters::const_iterator it = filters_.begin();
+           it != filters_.end(); ++it)
+      {
+        std::string value;
+        if (!dicom.GetTag(value, it->first, true))
+        {
+          return false;
+        }
 
-    // All other attributes passed as query keys
-    for (Filters::const_iterator it = filters_.begin();
-         it != filters_.end(); ++it)
-    {
-      fields.push_back(it->first);
-    }
+        if (!Matches(value, it->second))
+        {
+          return false;
+        }
+      }
 
-    // For instances and series, add all Study-level attributes if
-    // {StudyInstanceUID} is not specified.
-    if ((level == QueryLevel_Instance  || level == QueryLevel_Series) 
-        && filters_.find(OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID) == filters_.end()
-      )
-    {
-      AddResultAttributesForLevel(fields, QueryLevel_Study);
+      return true;
     }
 
-    // For instances, add all Series-level attributes if
-    // {SeriesInstanceUID} is not specified.
-    if (level == QueryLevel_Instance
-        && filters_.find(OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID) == filters_.end()
-      )
+
+    void ExtractFields(gdcm::DataSet& result,
+                       const OrthancPlugins::ParsedDicomFile& dicom,
+                       const std::string& wadoBase,
+                       QueryLevel level) const
     {
-      AddResultAttributesForLevel(fields, QueryLevel_Series);
-    }
+      std::list<gdcm::Tag> fields = includeFields_;
+
+      // The list of attributes for this query level
+      AddResultAttributesForLevel(fields, level);
 
-    // Copy all the required fields to the target
-    for (std::list<gdcm::Tag>::const_iterator
-           it = fields.begin(); it != fields.end(); it++)
-    {
-      if (dicom.GetDataSet().FindDataElement(*it))
+      // All other attributes passed as query keys
+      for (Filters::const_iterator it = filters_.begin();
+           it != filters_.end(); ++it)
       {
-        const gdcm::DataElement& element = dicom.GetDataSet().GetDataElement(*it);
-        result.Replace(element);
+        fields.push_back(it->first);
       }
-    }
+
+      // For instances and series, add all Study-level attributes if
+      // {StudyInstanceUID} is not specified.
+      if ((level == QueryLevel_Instance  || level == QueryLevel_Series) 
+          && filters_.find(OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID) == filters_.end()
+        )
+      {
+        AddResultAttributesForLevel(fields, QueryLevel_Study);
+      }
 
-    // Set the retrieve URL for WADO-RS
-    std::string url = (wadoBase + "/studies/" + 
-                       dicom.GetTagWithDefault(OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID, "", true));
+      // For instances, add all Series-level attributes if
+      // {SeriesInstanceUID} is not specified.
+      if (level == QueryLevel_Instance
+          && filters_.find(OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID) == filters_.end()
+        )
+      {
+        AddResultAttributesForLevel(fields, QueryLevel_Series);
+      }
 
-    if (level == QueryLevel_Series || level == QueryLevel_Instance)
-    {
-      url += "/series/" + dicom.GetTagWithDefault(OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID, "", true);
-    }
+      // Copy all the required fields to the target
+      for (std::list<gdcm::Tag>::const_iterator
+             it = fields.begin(); it != fields.end(); it++)
+      {
+        if (dicom.GetDataSet().FindDataElement(*it))
+        {
+          const gdcm::DataElement& element = dicom.GetDataSet().GetDataElement(*it);
+          result.Replace(element);
+        }
+      }
 
-    if (level == QueryLevel_Instance)
-    {
-      url += "/instances/" + dicom.GetTagWithDefault(OrthancPlugins::DICOM_TAG_SOP_INSTANCE_UID, "", true);
-    }
+      // Set the retrieve URL for WADO-RS
+      std::string url = (wadoBase + "/studies/" + 
+                         dicom.GetTagWithDefault(OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID, "", true));
+
+      if (level == QueryLevel_Series || level == QueryLevel_Instance)
+      {
+        url += "/series/" + dicom.GetTagWithDefault(OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID, "", true);
+      }
+
+      if (level == QueryLevel_Instance)
+      {
+        url += "/instances/" + dicom.GetTagWithDefault(OrthancPlugins::DICOM_TAG_SOP_INSTANCE_UID, "", true);
+      }
     
-    gdcm::DataElement element(OrthancPlugins::DICOM_TAG_RETRIEVE_URL);
-    element.SetByteValue(url.c_str(), url.size());
-    result.Replace(element);
-  }
-};
+      gdcm::DataElement element(OrthancPlugins::DICOM_TAG_RETRIEVE_URL);
+      element.SetByteValue(url.c_str(), url.size());
+      result.Replace(element);
+    }
+  };
 
 
 
 
-class CandidateResources
-{
-private:
-  typedef std::set<std::string>  Resources;
-
-  bool        all_;
-  QueryLevel  level_;
-  Resources   resources_;
-
-  static bool CallLookup(std::string& orthancId,
-                         const std::string& dicomId,
-                         char* (lookup) (OrthancPluginContext*, const char*))
+  class CandidateResources
   {
-    bool result = false;
-
-    char* tmp = lookup(context_, dicomId.c_str());
-    if (tmp != NULL)
-    {
-      orthancId = tmp;
-      result = true;
-    }
-
-    OrthancPluginFreeString(context_, tmp);
-
-    return result;
-  }
-
+  private:
+    typedef std::set<std::string>  Resources;
 
-  void FilterByIdentifierInternal(const ModuleMatcher& matcher,
-                                  const gdcm::Tag& tag,
-                                  char* (lookup) (OrthancPluginContext*, const char*))
-  {
-    std::string orthancId, dicomId;
-
-    if (!matcher.LookupExactFilter(dicomId, tag))
-    {
-      // There is no restriction at this level
-      return;
-    }
-
-    if (CallLookup(orthancId, dicomId, lookup) &&
-        (all_ || resources_.find(orthancId) != resources_.end()))
-    {
-      // There remains a single candidate resource
-      resources_.clear();
-      resources_.insert(orthancId);
-    }
-    else
-    {
-      // No matching resource remains
-      resources_.clear();            
-    }
-
-    all_ = false;
-  }
-
+    bool        all_;
+    QueryLevel  level_;
+    Resources   resources_;
 
-  bool PickOneInstance(std::string& instance,
-                       const std::string& resource) const
-  {
-    if (level_ == QueryLevel_Instance)
-    {
-      instance = resource;
-      return true;
-    }
-
-    std::string uri;
-    if (level_ == QueryLevel_Study)
-    {
-      uri = "/studies/" + resource + "/instances";
-    }
-    else
-    {
-      assert(level_ == QueryLevel_Series);
-      uri = "/series/" + resource + "/instances";
-    }
-
-    Json::Value instances;
-    if (!OrthancPlugins::RestApiGetJson(instances, context_, uri) ||
-        instances.type() != Json::arrayValue ||
-        instances.size() == 0)
-    {
-      return false;
-    }
-    else
+    static bool CallLookup(std::string& orthancId,
+                           const std::string& dicomId,
+                           char* (lookup) (OrthancPluginContext*, const char*))
     {
-      instance = instances[0]["ID"].asString();
-      return true;
-    }
-  }
-
-
-public:
-  CandidateResources() : all_(true), level_(QueryLevel_Study)
-  {
-  }
+      bool result = false;
 
-  void GoDown()
-  {
-    std::string baseUri;
-    std::string nextLevel;
-    switch (level_)
-    {
-      case QueryLevel_Study:
-        baseUri = "/studies/";
-        nextLevel = "Series";
-        break;
+      char* tmp = lookup(context_, dicomId.c_str());
+      if (tmp != NULL)
+      {
+        orthancId = tmp;
+        result = true;
+      }
 
-      case QueryLevel_Series:
-        baseUri = "/series/";
-        nextLevel = "Instances";
-        break;
+      OrthancPluginFreeString(context_, tmp);
 
-      default:
-        throw std::runtime_error("Internal error");
+      return result;
     }
 
 
-    if (!all_)
+    void FilterByIdentifierInternal(const ModuleMatcher& matcher,
+                                    const gdcm::Tag& tag,
+                                    char* (lookup) (OrthancPluginContext*, const char*))
     {
-      Resources  children;
-      
-      for (Resources::const_iterator it = resources_.begin();
-           it != resources_.end(); it++)
+      std::string orthancId, dicomId;
+
+      if (!matcher.LookupExactFilter(dicomId, tag))
       {
-        Json::Value tmp;
-        if (OrthancPlugins::RestApiGetJson(tmp, context_, baseUri + *it) &&
-            tmp.type() == Json::objectValue &&
-            tmp.isMember(nextLevel) &&
-            tmp[nextLevel].type() == Json::arrayValue)
-        {
-          for (Json::Value::ArrayIndex i = 0; i < tmp[nextLevel].size(); i++)
-          {
-            children.insert(tmp[nextLevel][i].asString());
-          }
-        }
+        // There is no restriction at this level
+        return;
       }
 
-      resources_ = children;
+      if (CallLookup(orthancId, dicomId, lookup) &&
+          (all_ || resources_.find(orthancId) != resources_.end()))
+      {
+        // There remains a single candidate resource
+        resources_.clear();
+        resources_.insert(orthancId);
+      }
+      else
+      {
+        // No matching resource remains
+        resources_.clear();            
+      }
+
+      all_ = false;
     }
 
 
-    switch (level_)
+    bool PickOneInstance(std::string& instance,
+                         const std::string& resource) const
     {
-      case QueryLevel_Study:
-        level_ = QueryLevel_Series;
-        break;
+      if (level_ == QueryLevel_Instance)
+      {
+        instance = resource;
+        return true;
+      }
 
-      case QueryLevel_Series:
-        level_ = QueryLevel_Instance;
-        break;
+      std::string uri;
+      if (level_ == QueryLevel_Study)
+      {
+        uri = "/studies/" + resource + "/instances";
+      }
+      else
+      {
+        assert(level_ == QueryLevel_Series);
+        uri = "/series/" + resource + "/instances";
+      }
 
-      default:
-        throw std::runtime_error("Internal error");
+      Json::Value instances;
+      if (!OrthancPlugins::RestApiGetJson(instances, context_, uri) ||
+          instances.type() != Json::arrayValue ||
+          instances.size() == 0)
+      {
+        return false;
+      }
+      else
+      {
+        instance = instances[0]["ID"].asString();
+        return true;
+      }
     }
-  }
 
 
-  void FilterByIdentifier(const ModuleMatcher& matcher)
-  {
-    switch (level_)
+  public:
+    CandidateResources() : all_(true), level_(QueryLevel_Study)
     {
-      case QueryLevel_Study:
-        FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID,
-                                   OrthancPluginLookupStudy);
-        FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_ACCESSION_NUMBER,
-                                   OrthancPluginLookupStudyWithAccessionNumber);
-        break;
-
-      case QueryLevel_Series:
-        FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID,
-                                   OrthancPluginLookupSeries);
-        break;
+    }
 
-      case QueryLevel_Instance:
-        FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_SOP_INSTANCE_UID,
-                                   OrthancPluginLookupInstance);
-        break;
-
-      default:
-        throw std::runtime_error("Internal error");
-    }
-  }
-
-
-  void Flatten(std::list<std::string>& result) const
-  {
-    std::string instance;
-
-    result.clear();
-
-    if (all_)
+    void GoDown()
     {
-      std::string uri;
+      std::string baseUri;
+      std::string nextLevel;
       switch (level_)
       {
         case QueryLevel_Study:
-          uri = "/studies/";
+          baseUri = "/studies/";
+          nextLevel = "Series";
           break;
 
         case QueryLevel_Series:
-          uri = "/series/";
-          break;
-
-        case QueryLevel_Instance:
-          uri = "/instances/";
+          baseUri = "/series/";
+          nextLevel = "Instances";
           break;
 
         default:
           throw std::runtime_error("Internal error");
       }
 
-      Json::Value tmp;
-      if (OrthancPlugins::RestApiGetJson(tmp, context_, uri) &&
-          tmp.type() == Json::arrayValue)
+
+      if (!all_)
+      {
+        Resources  children;
+      
+        for (Resources::const_iterator it = resources_.begin();
+             it != resources_.end(); it++)
+        {
+          Json::Value tmp;
+          if (OrthancPlugins::RestApiGetJson(tmp, context_, baseUri + *it) &&
+              tmp.type() == Json::objectValue &&
+              tmp.isMember(nextLevel) &&
+              tmp[nextLevel].type() == Json::arrayValue)
+          {
+            for (Json::Value::ArrayIndex i = 0; i < tmp[nextLevel].size(); i++)
+            {
+              children.insert(tmp[nextLevel][i].asString());
+            }
+          }
+        }
+
+        resources_ = children;
+      }
+
+
+      switch (level_)
+      {
+        case QueryLevel_Study:
+          level_ = QueryLevel_Series;
+          break;
+
+        case QueryLevel_Series:
+          level_ = QueryLevel_Instance;
+          break;
+
+        default:
+          throw std::runtime_error("Internal error");
+      }
+    }
+
+
+    void FilterByIdentifier(const ModuleMatcher& matcher)
+    {
+      switch (level_)
       {
-        for (Json::Value::ArrayIndex i = 0; i < tmp.size(); i++)
+        case QueryLevel_Study:
+          FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID,
+                                     OrthancPluginLookupStudy);
+          FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_ACCESSION_NUMBER,
+                                     OrthancPluginLookupStudyWithAccessionNumber);
+          break;
+
+        case QueryLevel_Series:
+          FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID,
+                                     OrthancPluginLookupSeries);
+          break;
+
+        case QueryLevel_Instance:
+          FilterByIdentifierInternal(matcher, OrthancPlugins::DICOM_TAG_SOP_INSTANCE_UID,
+                                     OrthancPluginLookupInstance);
+          break;
+
+        default:
+          throw std::runtime_error("Internal error");
+      }
+    }
+
+
+    void Flatten(std::list<std::string>& result) const
+    {
+      std::string instance;
+
+      result.clear();
+
+      if (all_)
+      {
+        std::string uri;
+        switch (level_)
         {
-          if (PickOneInstance(instance, tmp[i].asString()))
+          case QueryLevel_Study:
+            uri = "/studies/";
+            break;
+
+          case QueryLevel_Series:
+            uri = "/series/";
+            break;
+
+          case QueryLevel_Instance:
+            uri = "/instances/";
+            break;
+
+          default:
+            throw std::runtime_error("Internal error");
+        }
+
+        Json::Value tmp;
+        if (OrthancPlugins::RestApiGetJson(tmp, context_, uri) &&
+            tmp.type() == Json::arrayValue)
+        {
+          for (Json::Value::ArrayIndex i = 0; i < tmp.size(); i++)
+          {
+            if (PickOneInstance(instance, tmp[i].asString()))
+            {
+              result.push_back(instance);
+            }
+          }
+        }
+      }
+      else
+      {
+        for (Resources::const_iterator 
+               it = resources_.begin(); it != resources_.end(); it++)
+        {
+          if (PickOneInstance(instance, *it))
           {
             result.push_back(instance);
           }
         }
       }
     }
-    else
-    {
-      for (Resources::const_iterator 
-             it = resources_.begin(); it != resources_.end(); it++)
-      {
-        if (PickOneInstance(instance, *it))
-        {
-          result.push_back(instance);
-        }
-      }
-    }
-  }
-};
-
-
-
-class SearchResults
-{
-private:
-  typedef std::list<gdcm::DataSet*>   Results;
-
-  Results results_;
-
-public:
-  ~SearchResults()
-  {
-    for (Results::iterator it = results_.begin();
-         it != results_.end(); it++)
-    {
-      delete *it;
-    }
-  }
-
-  void Add(const OrthancPlugins::ParsedDicomFile& dicom,
-           const ModuleMatcher& matcher,
-           const std::string& wadoBase,
-           QueryLevel level)
-  {
-    std::auto_ptr<gdcm::DataSet> result(new gdcm::DataSet);
-    matcher.ExtractFields(*result, dicom, wadoBase, level);
-    results_.push_back(result.release());
-  }
+  };
+}
 
-  void Answer(OrthancPluginContext* context,
-              OrthancPluginRestOutput* output,
-              bool isXml)
-  {
-    if (isXml)
-    {
-      OrthancPlugins::MultipartWriter writer("application/dicom+xml");
-
-      for (Results::const_iterator it = results_.begin();
-           it != results_.end(); it++)
-      {
-        std::string answer;
-        OrthancPlugins::GenerateSingleDicomAnswer(answer, *dictionary_, **it, true);
-        writer.AddPart(answer);
-      }
-
-      writer.Answer(context_, output);
-    }
-    else
-    {
-      OrthancPlugins::ChunkedBuffer chunks;
-      chunks.AddChunk("[\n");
-
-      std::string s = "[\n";
-
-      bool isFirst = true;
-      for (Results::const_iterator it = results_.begin();
-           it != results_.end(); it++)
-      {
-        if (isFirst)
-        {
-          isFirst = false;
-        }
-        else
-        {
-          chunks.AddChunk(",\n");
-        }
-
-        std::string item;
-        OrthancPlugins::GenerateSingleDicomAnswer(item, *dictionary_, **it, false);
-        chunks.AddChunk(item);
-      }
-
-      chunks.AddChunk("]\n");
-
-      std::string answer;
-      chunks.Flatten(answer);
-      OrthancPluginAnswerBuffer(context, output, answer.c_str(), answer.size(), "application/json");
-    }
-  }
-};
 
 
 
@@ -815,7 +740,9 @@
   candidates.Flatten(resources);
 
   std::string wadoBase = OrthancPlugins::Configuration::GetBaseUrl(configuration_, request) + "/wado-rs";
-  SearchResults results;
+
+  OrthancPlugins::DicomResults results(*dictionary_, IsXmlExpected(request), true);
+
   for (std::list<std::string>::const_iterator
          it = resources.begin(); it != resources.end(); it++)
   {
@@ -825,12 +752,14 @@
       OrthancPlugins::ParsedDicomFile dicom(file);
       if (matcher.Matches(dicom))
       {
-        results.Add(dicom, matcher, wadoBase, level);
+        std::auto_ptr<gdcm::DataSet> result(new gdcm::DataSet);
+        matcher.ExtractFields(*result, dicom, wadoBase, level);
+        results.Add(*result);
       }
     }
   }
 
-  results.Answer(context_, output, IsXmlExpected(request));
+  results.Answer(context_, output);
 }
 
 
--- a/Plugin/StowRs.cpp	Thu Apr 30 15:53:07 2015 +0200
+++ b/Plugin/StowRs.cpp	Thu May 07 17:07:25 2015 +0200
@@ -223,7 +223,7 @@
     SetSequenceTag(result, OrthancPlugins::DICOM_TAG_FAILED_SOP_SEQUENCE, failed);
     SetSequenceTag(result, OrthancPlugins::DICOM_TAG_REFERENCED_SOP_SEQUENCE, success);
 
-    OrthancPlugins::AnswerDicom(context_, output, *dictionary_, result, isXml);
+    OrthancPlugins::AnswerDicom(context_, output, *dictionary_, result, isXml, false);
 
     return 0;
   }
--- a/Plugin/WadoRs.cpp	Thu Apr 30 15:53:07 2015 +0200
+++ b/Plugin/WadoRs.cpp	Thu May 07 17:07:25 2015 +0200
@@ -22,6 +22,7 @@
 
 #include "../Core/Configuration.h"
 #include "../Core/Dicom.h"
+#include "../Core/DicomResults.h"
 #include "../Core/MultipartWriter.h"
 
 
@@ -52,7 +53,62 @@
     OrthancPlugins::ToLowerCase(s);
     if (s != "application/dicom")
     {
-      std::string s = "This WADO-RS plugin only supports application/dicom return type (" + accept + ")";
+      std::string s = "This WADO-RS plugin only supports application/dicom return type for DICOM retrieval (" + accept + ")";
+      OrthancPluginLogError(context_, s.c_str());
+      return false;
+    }
+  }
+
+  if (attributes.find("transfer-syntax") != attributes.end())
+  {
+    std::string s = "This WADO-RS plugin cannot change the transfer syntax to " + attributes["transfer-syntax"];
+    OrthancPluginLogError(context_, s.c_str());
+    return false;
+  }
+
+  return true;
+}
+
+
+
+static bool AcceptMetadata(const OrthancPluginHttpRequest* request,
+                           bool& isXml)
+{
+  isXml = true;
+
+  std::string accept;
+
+  if (!OrthancPlugins::LookupHttpHeader(accept, request, "accept"))
+  {
+    // By default, return "multipart/related; type=application/dicom+xml;"
+    return true;
+  }
+
+  std::string application;
+  std::map<std::string, std::string> attributes;
+  OrthancPlugins::ParseContentType(application, attributes, accept);
+
+  if (application == "application/json")
+  {
+    isXml = false;
+    return true;
+  }
+
+  if (application != "multipart/related" &&
+      application != "*/*")
+  {
+    std::string s = "This WADO-RS plugin cannot generate the following content type: " + accept;
+    OrthancPluginLogError(context_, s.c_str());
+    return false;
+  }
+
+  if (attributes.find("type") != attributes.end())
+  {
+    std::string s = attributes["type"];
+    OrthancPlugins::ToLowerCase(s);
+    if (s != "application/dicom+xml")
+    {
+      std::string s = "This WADO-RS plugin only supports application/json or application/dicom+xml return types for metadata (" + accept + ")";
       OrthancPluginLogError(context_, s.c_str());
       return false;
     }
@@ -99,41 +155,195 @@
 
 
 
+static void AnswerMetadata(OrthancPluginRestOutput* output,
+                           const std::string& resource,
+                           bool isInstance,
+                           bool isXml)
+{
+  std::list<std::string> files;
+  if (isInstance)
+  {
+    files.push_back(resource + "/file");
+  }
+  else
+  {
+    Json::Value instances;
+    if (!OrthancPlugins::RestApiGetJson(instances, context_, resource + "/instances"))
+    {
+      // Internal error
+      OrthancPluginSendHttpStatusCode(context_, output, 400);
+      return;
+    }
+
+    for (Json::Value::ArrayIndex i = 0; i < instances.size(); i++)
+    {
+      files.push_back("/instances/" + instances[i]["ID"].asString() + "/file");
+    }
+  }
+
+  OrthancPlugins::DicomResults results(*dictionary_, isXml, true);
+  
+  for (std::list<std::string>::const_iterator
+         it = files.begin(); it != files.end(); ++it)
+  {
+    std::string content; 
+    if (OrthancPlugins::RestApiGetString(content, context_, *it))
+    {
+      OrthancPlugins::ParsedDicomFile dicom(content);
+      results.Add(dicom.GetDataSet());
+    }
+  }
+
+  results.Answer(context_, output);
+}
+
+
+
+
+static bool LocateStudy(OrthancPluginRestOutput* output,
+                        std::string& uri,
+                        const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context_, output, "GET");
+    return false;
+  }
+
+  std::string id;
+
+  {
+    char* tmp = OrthancPluginLookupStudy(context_, request->groups[0]);
+    if (tmp == NULL)
+    {
+      std::string s = "Accessing an inexistent study with WADO-RS: " + std::string(request->groups[0]);
+      OrthancPluginLogError(context_, s.c_str());
+      OrthancPluginSendHttpStatusCode(context_, output, 404);
+      return false;
+    }
+
+    id.assign(tmp);
+    OrthancPluginFreeString(context_, tmp);
+  }
+  
+  uri = "/studies/" + id;
+  return true;
+}
+
+
+static bool LocateSeries(OrthancPluginRestOutput* output,
+                         std::string& uri,
+                         const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context_, output, "GET");
+    return false;
+  }
+
+  std::string id;
+
+  {
+    char* tmp = OrthancPluginLookupSeries(context_, request->groups[1]);
+    if (tmp == NULL)
+    {
+      std::string s = "Accessing an inexistent series with WADO-RS: " + std::string(request->groups[1]);
+      OrthancPluginLogError(context_, s.c_str());
+      OrthancPluginSendHttpStatusCode(context_, output, 404);
+      return false;
+    }
+
+    id.assign(tmp);
+    OrthancPluginFreeString(context_, tmp);
+  }
+  
+  Json::Value study;
+  if (!OrthancPlugins::RestApiGetJson(study, context_, "/series/" + id + "/study"))
+  {
+    OrthancPluginSendHttpStatusCode(context_, output, 404);
+    return false;
+  }
+
+  if (study["MainDicomTags"]["StudyInstanceUID"].asString() != std::string(request->groups[0]))
+  {
+    std::string s = "No series " + std::string(request->groups[1]) + " in study " + std::string(request->groups[0]);
+    OrthancPluginLogError(context_, s.c_str());
+    OrthancPluginSendHttpStatusCode(context_, output, 404);
+    return false;
+  }
+  
+  uri = "/series/" + id;
+  return true;
+}
+
+
+static bool LocateInstance(OrthancPluginRestOutput* output,
+                           std::string& uri,
+                           const OrthancPluginHttpRequest* request)
+{
+  if (request->method != OrthancPluginHttpMethod_Get)
+  {
+    OrthancPluginSendMethodNotAllowed(context_, output, "GET");
+    return false;
+  }
+
+  std::string id;
+
+  {
+    char* tmp = OrthancPluginLookupInstance(context_, request->groups[2]);
+    if (tmp == NULL)
+    {
+      std::string s = "Accessing an inexistent instance with WADO-RS: " + std::string(request->groups[2]);
+      OrthancPluginLogError(context_, s.c_str());
+      OrthancPluginSendHttpStatusCode(context_, output, 404);
+      return false;
+    }
+
+    id.assign(tmp);
+    OrthancPluginFreeString(context_, tmp);
+  }
+  
+  Json::Value study, series;
+  if (!OrthancPlugins::RestApiGetJson(series, context_, "/instances/" + id + "/series") ||
+      !OrthancPlugins::RestApiGetJson(study, context_, "/instances/" + id + "/study"))
+  {
+    OrthancPluginSendHttpStatusCode(context_, output, 404);
+    return false;
+  }
+
+  if (study["MainDicomTags"]["StudyInstanceUID"].asString() != std::string(request->groups[0]) ||
+      series["MainDicomTags"]["SeriesInstanceUID"].asString() != std::string(request->groups[1]))
+  {
+    std::string s = ("No instance " + std::string(request->groups[2]) + 
+                     " in study " + std::string(request->groups[0]) + " or " +
+                     " in series " + std::string(request->groups[1]));
+    OrthancPluginLogError(context_, s.c_str());
+    OrthancPluginSendHttpStatusCode(context_, output, 404);
+    return false;
+  }
+
+  uri = "/instances/" + id;
+  return true;
+}
+
+
 int32_t RetrieveDicomStudy(OrthancPluginRestOutput* output,
                            const char* url,
                            const OrthancPluginHttpRequest* request)
 {
   try
   {
-    if (request->method != OrthancPluginHttpMethod_Get)
-    {
-      OrthancPluginSendMethodNotAllowed(context_, output, "GET");
-      return 0;
-    }
-
     if (!AcceptMultipartDicom(request))
     {
       OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */);
-      return 0;
+      return false;
     }
 
-    std::string id;
-
+    std::string uri;
+    if (LocateStudy(output, uri, request))
     {
-      char* tmp = OrthancPluginLookupStudy(context_, request->groups[0]);
-      if (tmp == NULL)
-      {
-        std::string s = "Accessing an inexistent study with WADO-RS: " + std::string(request->groups[0]);
-        OrthancPluginLogError(context_, s.c_str());
-        OrthancPluginSendHttpStatusCode(context_, output, 404);
-        return 0;
-      }
-
-      id.assign(tmp);
-      OrthancPluginFreeString(context_, tmp);
+      AnswerListOfDicomInstances(output, uri);
     }
-  
-    AnswerListOfDicomInstances(output, "/studies/" + id);
 
     return 0;
   }
@@ -151,50 +361,17 @@
 {
   try
   {
-    if (request->method != OrthancPluginHttpMethod_Get)
-    {
-      OrthancPluginSendMethodNotAllowed(context_, output, "GET");
-      return 0;
-    }
-
     if (!AcceptMultipartDicom(request))
     {
       OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */);
-      return 0;
+      return false;
     }
 
-    std::string id;
-
+    std::string uri;
+    if (LocateSeries(output, uri, request))
     {
-      char* tmp = OrthancPluginLookupSeries(context_, request->groups[1]);
-      if (tmp == NULL)
-      {
-        std::string s = "Accessing an inexistent series with WADO-RS: " + std::string(request->groups[1]);
-        OrthancPluginLogError(context_, s.c_str());
-        OrthancPluginSendHttpStatusCode(context_, output, 404);
-        return 0;
-      }
-
-      id.assign(tmp);
-      OrthancPluginFreeString(context_, tmp);
+      AnswerListOfDicomInstances(output, uri);
     }
-  
-    Json::Value study;
-    if (!OrthancPlugins::RestApiGetJson(study, context_, "/series/" + id + "/study"))
-    {
-      OrthancPluginSendHttpStatusCode(context_, output, 404);
-      return 0;
-    }
-
-    if (study["MainDicomTags"]["StudyInstanceUID"].asString() != std::string(request->groups[0]))
-    {
-      std::string s = "No series " + std::string(request->groups[1]) + " in study " + std::string(request->groups[0]);
-      OrthancPluginLogError(context_, s.c_str());
-      OrthancPluginSendHttpStatusCode(context_, output, 404);
-      return 0;
-    }
-
-    AnswerListOfDicomInstances(output, "/series/" + id);
 
     return 0;
   }
@@ -213,62 +390,55 @@
 {
   try
   {
-    if (request->method != OrthancPluginHttpMethod_Get)
-    {
-      OrthancPluginSendMethodNotAllowed(context_, output, "GET");
-      return 0;
-    }
-
     if (!AcceptMultipartDicom(request))
     {
       OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */);
-      return 0;
+      return false;
     }
 
-    std::string id;
-
+    std::string uri;
+    if (LocateInstance(output, uri, request))
     {
-      char* tmp = OrthancPluginLookupInstance(context_, request->groups[2]);
-      if (tmp == NULL)
+      OrthancPlugins::MultipartWriter writer("application/dicom");
+      std::string dicom;
+      if (OrthancPlugins::RestApiGetString(dicom, context_, uri + "/file"))
       {
-        std::string s = "Accessing an inexistent instance with WADO-RS: " + std::string(request->groups[2]);
-        OrthancPluginLogError(context_, s.c_str());
-        OrthancPluginSendHttpStatusCode(context_, output, 404);
-        return 0;
+        writer.AddPart(dicom);
       }
 
-      id.assign(tmp);
-      OrthancPluginFreeString(context_, tmp);
-    }
-  
-    Json::Value study, series;
-    if (!OrthancPlugins::RestApiGetJson(series, context_, "/instances/" + id + "/series") ||
-        !OrthancPlugins::RestApiGetJson(study, context_, "/instances/" + id + "/study"))
-    {
-      OrthancPluginSendHttpStatusCode(context_, output, 404);
-      return 0;
+      writer.Answer(context_, output);
     }
 
-    if (study["MainDicomTags"]["StudyInstanceUID"].asString() != std::string(request->groups[0]) ||
-        series["MainDicomTags"]["SeriesInstanceUID"].asString() != std::string(request->groups[1]))
+    return 0;
+  }
+  catch (std::runtime_error& e)
+  {
+    OrthancPluginLogError(context_, e.what());
+    return -1;
+  }
+}
+
+
+
+int32_t RetrieveStudyMetadata(OrthancPluginRestOutput* output,
+                              const char* url,
+                              const OrthancPluginHttpRequest* request)
+{
+  try
+  {
+    bool isXml;
+    if (!AcceptMetadata(request, isXml))
     {
-      std::string s = ("No instance " + std::string(request->groups[2]) + 
-                       " in study " + std::string(request->groups[0]) + " or " +
-                       " in series " + std::string(request->groups[1]));
-      OrthancPluginLogError(context_, s.c_str());
-      OrthancPluginSendHttpStatusCode(context_, output, 404);
-      return 0;
+      OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */);
+      return false;
     }
 
-    OrthancPlugins::MultipartWriter writer("application/dicom");
-    std::string dicom;
-    if (OrthancPlugins::RestApiGetString(dicom, context_, "/instances/" + id + "/file"))
+    std::string uri;
+    if (LocateStudy(output, uri, request))
     {
-      writer.AddPart(dicom);
+      AnswerMetadata(output, uri, false, isXml);
     }
 
-    writer.Answer(context_, output);
-
     return 0;
   }
   catch (std::runtime_error& e)
@@ -277,3 +447,61 @@
     return -1;
   }
 }
+
+
+int32_t RetrieveSeriesMetadata(OrthancPluginRestOutput* output,
+                               const char* url,
+                               const OrthancPluginHttpRequest* request)
+{
+  try
+  {
+    bool isXml;
+    if (!AcceptMetadata(request, isXml))
+    {
+      OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */);
+      return false;
+    }
+
+    std::string uri;
+    if (LocateSeries(output, uri, request))
+    {
+      AnswerMetadata(output, uri, false, isXml);
+    }
+
+    return 0;
+  }
+  catch (std::runtime_error& e)
+  {
+    OrthancPluginLogError(context_, e.what());
+    return -1;
+  }
+}
+
+
+int32_t RetrieveInstanceMetadata(OrthancPluginRestOutput* output,
+                                 const char* url,
+                                 const OrthancPluginHttpRequest* request)
+{
+  try
+  {
+    bool isXml;
+    if (!AcceptMetadata(request, isXml))
+    {
+      OrthancPluginSendHttpStatusCode(context_, output, 400 /* Bad request */);
+      return false;
+    }
+
+    std::string uri;
+    if (LocateInstance(output, uri, request))
+    {
+      AnswerMetadata(output, uri, true, isXml);
+    }
+
+    return 0;
+  }
+  catch (std::runtime_error& e)
+  {
+    OrthancPluginLogError(context_, e.what());
+    return -1;
+  }
+}
--- a/Plugin/WadoRs.h	Thu Apr 30 15:53:07 2015 +0200
+++ b/Plugin/WadoRs.h	Thu May 07 17:07:25 2015 +0200
@@ -34,3 +34,15 @@
 int32_t RetrieveDicomInstance(OrthancPluginRestOutput* output,
                               const char* url,
                               const OrthancPluginHttpRequest* request);
+
+int32_t RetrieveStudyMetadata(OrthancPluginRestOutput* output,
+                              const char* url,
+                              const OrthancPluginHttpRequest* request);
+
+int32_t RetrieveSeriesMetadata(OrthancPluginRestOutput* output,
+                               const char* url,
+                               const OrthancPluginHttpRequest* request);
+
+int32_t RetrieveInstanceMetadata(OrthancPluginRestOutput* output,
+                                 const char* url,
+                                 const OrthancPluginHttpRequest* request);
--- a/Samples/JavaScript/qido-rs.js	Thu Apr 30 15:53:07 2015 +0200
+++ b/Samples/JavaScript/qido-rs.js	Thu May 07 17:07:25 2015 +0200
@@ -57,7 +57,7 @@
           $('#qido-series-results').append(
             '<li>' + patientId + ' - ' + patientName + ' - ' +
               studyDescription + ' - ' + seriesDescription +
-              + ' - ' + url + '</li>');
+              ' - ' + '<a href="' + url + '">WADO-RS URL</a></li>');
         }
       },
       error: function() {
--- a/Status.txt	Thu Apr 30 15:53:07 2015 +0200
+++ b/Status.txt	Thu May 07 17:07:25 2015 +0200
@@ -27,10 +27,16 @@
 ================================
 6.5.4 WADO-RS / RetrieveFrames
 6.5.5 WADO-RS / RetrieveBulkdata
+================================
+
+Not supported.
+
+
+================================
 6.5.6 WADO-RS / RetrieveMetadata
 ================================
 
-Not supported.
+Supported.