changeset 113:04fbfd59a60e dev

integration mainline->dev
author Sebastien Jodogne <s.jodogne@gmail.com>
date Thu, 28 Apr 2016 17:22:02 +0200
parents bcc9e98bb725 (current diff) 528d18573c09 (diff)
children bee10bb1331b
files NEWS Plugin/Configuration.cpp Plugin/Configuration.h
diffstat 9 files changed, 792 insertions(+), 144 deletions(-) [+]
line wrap: on
line diff
--- a/NEWS	Thu Apr 28 09:14:06 2016 +0200
+++ b/NEWS	Thu Apr 28 17:22:02 2016 +0200
@@ -7,6 +7,10 @@
 * Better robustness in the STOW-RS server
 
 
+* Fix issue #13 (QIDO-RS study-level query is slow)
+* Fix issue #14 (Aggregate fields empty for QIDO-RS study/series-level queries)
+
+
 Version 0.2 (2015/12/10)
 ========================
 
--- a/Plugin/Configuration.cpp	Thu Apr 28 09:14:06 2016 +0200
+++ b/Plugin/Configuration.cpp	Thu Apr 28 17:22:02 2016 +0200
@@ -430,5 +430,27 @@
 
       return (ssl ? "https://" : "http://") + host + GetRoot(configuration);
     }
+
+
+
+    std::string GetWadoUrl(const std::string& wadoBase,
+                           const std::string& studyInstanceUid,
+                           const std::string& seriesInstanceUid,
+                           const std::string& sopInstanceUid)
+    {
+      if (studyInstanceUid.empty() ||
+          seriesInstanceUid.empty() ||
+          sopInstanceUid.empty())
+      {
+        return "";
+      }
+      else
+      {
+        return (wadoBase + 
+                "studies/" + studyInstanceUid + 
+                "/series/" + seriesInstanceUid + 
+                "/instances/" + sopInstanceUid + "/");
+      }
+    }
   }
 }
--- a/Plugin/Configuration.h	Thu Apr 28 09:14:06 2016 +0200
+++ b/Plugin/Configuration.h	Thu Apr 28 17:22:02 2016 +0200
@@ -93,5 +93,10 @@
       
     std::string GetBaseUrl(const Json::Value& configuration,
                            const OrthancPluginHttpRequest* request);
+
+    std::string GetWadoUrl(const std::string& wadoBase,
+                           const std::string& studyInstanceUid,
+                           const std::string& seriesInstanceUid,
+                           const std::string& sopInstanceUid);
   }
 }
--- a/Plugin/Dicom.cpp	Thu Apr 28 09:14:06 2016 +0200
+++ b/Plugin/Dicom.cpp	Thu Apr 28 17:22:02 2016 +0200
@@ -33,30 +33,6 @@
 
 namespace OrthancPlugins
 {
-  namespace
-  {
-    class ChunkedBufferWriter : public pugi::xml_writer
-    {
-    private:
-      ChunkedBuffer buffer_;
-
-    public:
-      virtual void write(const void *data, size_t size)
-      {
-        if (size > 0)
-        {
-          buffer_.AddChunk(reinterpret_cast<const char*>(data), size);
-        }
-      }
-
-      void Flatten(std::string& s)
-      {
-        buffer_.Flatten(s);
-      }
-    };
-  }
-
-
   static std::string MyStripSpaces(const std::string& source)
   {
     size_t first = 0;
@@ -89,12 +65,12 @@
 
   static const char* GetVRName(bool& isSequence,
                                const gdcm::Dict& dictionary,
-                               const gdcm::DataElement& element)
+                               const gdcm::Tag& tag,
+                               gdcm::VR vr)
   {
-    gdcm::VR vr = element.GetVR();
     if (vr == gdcm::VR::INVALID)
     {
-      const gdcm::DictEntry &entry = dictionary.GetDictEntry(element.GetTag());
+      const gdcm::DictEntry &entry = dictionary.GetDictEntry(tag);
       vr = entry.GetVR();
 
       if (vr == gdcm::VR::OB_OW)
@@ -125,6 +101,21 @@
   }
 
 
+  const char* GetVRName(bool& isSequence,
+                        const gdcm::Dict& dictionary,
+                        const gdcm::Tag& tag)
+  {
+    return GetVRName(isSequence, dictionary, tag, gdcm::VR::INVALID);
+  }
+
+
+  static const char* GetVRName(bool& isSequence,
+                               const gdcm::Dict& dictionary,
+                               const gdcm::DataElement& element)
+  {
+    return GetVRName(isSequence, dictionary, element.GetTag(), element.GetVR());
+  }
+
 
   static bool ConvertDicomStringToUtf8(std::string& result,
                                        const gdcm::Dict& dictionary,
@@ -295,7 +286,7 @@
 
 
 
-  static std::string FormatTag(const gdcm::Tag& tag)
+  std::string FormatTag(const gdcm::Tag& tag)
   {
     char tmp[16];
     sprintf(tmp, "%04X%04X", tag.GetGroup(), tag.GetElement());
@@ -303,8 +294,8 @@
   }
 
 
-  static const char* GetKeyword(const gdcm::Dict& dictionary,
-                                const gdcm::Tag& tag)
+  const char* GetKeyword(const gdcm::Dict& dictionary,
+                         const gdcm::Tag& tag)
   {
     const gdcm::DictEntry &entry = dictionary.GetDictEntry(tag);
     const char* keyword = entry.GetKeyword();
@@ -363,7 +354,7 @@
     }
     else
     {
-      return wadoBase + "studies/" + study + "/series/" + series + "/instances/" + instance + "/";
+      return Configuration::GetWadoUrl(wadoBase, study, series, instance);
     }
   }
 
@@ -424,12 +415,6 @@
       pugi::xml_node node = target.append_child("DicomAttribute");
       node.append_attribute("tag").set_value(FormatTag(it->GetTag()).c_str());
 
-      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)
@@ -444,6 +429,12 @@
 
       node.append_attribute("vr").set_value(vr.c_str());
 
+      const char* keyword = GetKeyword(dictionary, it->GetTag());
+      if (keyword != NULL)
+      {
+        node.append_attribute("keyword").set_value(keyword);
+      }
+
       if (isSequence)
       {
         gdcm::SmartPointer<gdcm::SequenceOfItems> seq = it->GetValueAsSQ();
@@ -486,6 +477,10 @@
         {
           value.append_child(pugi::node_pcdata).set_value(tmp.c_str());
         }
+        else
+        {
+          value.append_child(pugi::node_pcdata).set_value("");
+        }
       }
     }
   }
@@ -542,6 +537,7 @@
 
       node["vr"] = vr.c_str();
 
+      bool ok = true;
       if (isSequence)
       {
         // Deal with sequences
@@ -565,6 +561,8 @@
             node["Value"].append(child);
           }
         }
+
+        ok = true;
       }
       else if (IsBulkData(vr))
       {
@@ -572,6 +570,7 @@
         if (!bulkUri.empty())
         {
           node["BulkDataURI"] = bulkUri + std::string(path);
+          ok = true;
         }
       }
       else
@@ -584,9 +583,18 @@
         {
           node["Value"].append(value.c_str());
         }
+        else
+        {
+          node["Value"].append("");
+        }
+
+        ok = true;
       }
 
-      target[FormatTag(it->GetTag())] = node;
+      if (ok)
+      {
+        target[FormatTag(it->GetTag())] = node;
+      }
     }
   }
 
@@ -657,4 +665,88 @@
     const std::string base = OrthancPlugins::Configuration::GetBaseUrl(configuration_, request);
     return OrthancPlugins::GetWadoUrl(base, GetDataSet());
   }
+
+
+  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]));
+  }
+
+
+  gdcm::Tag ParseTag(const gdcm::Dict& dictionary,
+                     const std::string& key)
+  {
+    if (key.find('.') != std::string::npos)
+    {
+      std::string s = "This DICOMweb plugin does not support hierarchical queries: " + key;
+      OrthancPluginLogError(context_, s.c_str());
+      throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+    }
+
+    if (key.size() == 8 &&  // This is the DICOMweb convention
+        isxdigit(key[0]) &&
+        isxdigit(key[1]) &&
+        isxdigit(key[2]) &&
+        isxdigit(key[3]) &&
+        isxdigit(key[4]) &&
+        isxdigit(key[5]) &&
+        isxdigit(key[6]) &&
+        isxdigit(key[7]))        
+    {
+      return gdcm::Tag(GetTagValue(key.c_str()),
+                       GetTagValue(key.c_str() + 4));
+    }
+    else if (key.size() == 9 &&  // This is the Orthanc convention
+             isxdigit(key[0]) &&
+             isxdigit(key[1]) &&
+             isxdigit(key[2]) &&
+             isxdigit(key[3]) &&
+             key[4] == ',' &&
+             isxdigit(key[5]) &&
+             isxdigit(key[6]) &&
+             isxdigit(key[7]) &&
+             isxdigit(key[8]))        
+    {
+      return gdcm::Tag(GetTagValue(key.c_str()),
+                       GetTagValue(key.c_str() + 5));
+    }
+    else
+    {
+      gdcm::Tag tag;
+      dictionary.GetDictEntryByKeyword(key.c_str(), tag);
+
+      if (tag.IsIllegal() || tag.IsPrivate())
+      {
+        if (key.find('.') != std::string::npos)
+        {
+          std::string s = "This QIDO-RS implementation does not support search over sequences: " + key;
+          OrthancPluginLogError(context_, s.c_str());
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
+        }
+        else
+        {
+          std::string s = "Illegal tag name in QIDO-RS: " + key;
+          OrthancPluginLogError(context_, s.c_str());
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownDicomTag);
+        }
+      }
+
+      return tag;
+    }
+  }
 }
--- a/Plugin/Dicom.h	Thu Apr 28 09:14:06 2016 +0200
+++ b/Plugin/Dicom.h	Thu Apr 28 17:22:02 2016 +0200
@@ -22,6 +22,7 @@
 
 #include "Configuration.h"
 
+#include "../Orthanc/Core/ChunkedBuffer.h"
 #include "../Orthanc/Core/Enumerations.h"
 
 #include <gdcmReader.h>
@@ -99,6 +100,10 @@
   };
 
 
+  const char* GetVRName(bool& isSequence /* out */,
+                        const gdcm::Dict& dictionary,
+                        const gdcm::Tag& tag);
+
   void GenerateSingleDicomAnswer(std::string& result,
                                  const std::string& wadoBase,
                                  const gdcm::Dict& dictionary,
@@ -114,4 +119,32 @@
                    const gdcm::DataSet& dicom,
                    bool isXml,
                    bool isBulkAccessible);
+
+  gdcm::Tag ParseTag(const gdcm::Dict& dictionary,
+                     const std::string& key);
+
+  std::string FormatTag(const gdcm::Tag& tag);
+
+  const char* GetKeyword(const gdcm::Dict& dictionary,
+                         const gdcm::Tag& tag);
+
+  class ChunkedBufferWriter : public pugi::xml_writer
+  {
+  private:
+    Orthanc::ChunkedBuffer buffer_;
+
+  public:
+    virtual void write(const void *data, size_t size)
+    {
+      if (size > 0)
+      {
+        buffer_.AddChunk(reinterpret_cast<const char*>(data), size);
+      }
+    }
+
+    void Flatten(std::string& s)
+    {
+      buffer_.Flatten(s);
+    }
+  };
 }
--- a/Plugin/DicomResults.cpp	Thu Apr 28 09:14:06 2016 +0200
+++ b/Plugin/DicomResults.cpp	Thu Apr 28 17:22:02 2016 +0200
@@ -22,6 +22,10 @@
 
 #include "Dicom.h"
 #include "../Orthanc/Core/OrthancException.h"
+#include "../Orthanc/Core/Toolbox.h"
+
+#include <boost/lexical_cast.hpp>
+#include <boost/noncopyable.hpp>
 
 namespace OrthancPlugins
 {
@@ -50,15 +54,11 @@
   }
 
 
-  void DicomResults::AddInternal(const gdcm::File* file,
-                                 const gdcm::DataSet& dicom)
+  void DicomResults::AddInternal(const std::string& item)
   {
     if (isXml_)
     {
-      std::string answer;
-      GenerateSingleDicomAnswer(answer, wadoBase_, dictionary_, file, dicom, true, isBulkAccessible_);
-
-      if (OrthancPluginSendMultipartItem(context_, output_, answer.c_str(), answer.size()) != 0)
+      if (OrthancPluginSendMultipartItem(context_, output_, item.c_str(), item.size()) != 0)
       {
         OrthancPluginLogError(context_, "Unable to create a multipart stream of DICOM+XML answers");
         throw Orthanc::OrthancException(Orthanc::ErrorCode_NetworkProtocol);
@@ -71,14 +71,342 @@
         jsonWriter_.AddChunk(",\n");
       }
 
-      std::string item;
-      GenerateSingleDicomAnswer(item, wadoBase_, dictionary_, file, dicom, false, isBulkAccessible_);
       jsonWriter_.AddChunk(item);
     }
 
     isFirst_ = false;
   }
 
+
+  void DicomResults::AddInternal(const gdcm::File* file,
+                                 const gdcm::DataSet& dicom)
+  {
+    std::string item;
+
+    if (isXml_)
+    {
+      GenerateSingleDicomAnswer(item, wadoBase_, dictionary_, file, dicom, true, isBulkAccessible_);
+    }
+    else
+    {
+      GenerateSingleDicomAnswer(item, wadoBase_, dictionary_, file, dicom, false, isBulkAccessible_);
+    }
+
+    AddInternal(item);
+
+    isFirst_ = false;
+  }
+
+
+
+  namespace
+  {
+    class ITagVisitor : public boost::noncopyable
+    {
+    public:
+      virtual ~ITagVisitor()
+      {
+      }
+
+      virtual void Visit(const gdcm::Tag& tag,
+                         bool isSequence,
+                         const std::string& vr,
+                         const std::string& type,
+                         const Json::Value& value) = 0;
+
+      static void Apply(ITagVisitor& visitor,
+                        const Json::Value& source,
+                        const gdcm::Dict& dictionary)
+      {
+        if (source.type() != Json::objectValue)
+        {
+          throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+        }
+
+        Json::Value::Members members = source.getMemberNames();
+        for (size_t i = 0; i < members.size(); i++)
+        {
+          if (members[i].size() != 9 ||
+              members[i][4] != ',' ||
+              source[members[i]].type() != Json::objectValue ||
+              !source[members[i]].isMember("Value") ||
+              !source[members[i]].isMember("Type") ||
+              source[members[i]]["Type"].type() != Json::stringValue)
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          }        
+
+          const Json::Value& value = source[members[i]]["Value"];
+          const std::string type = source[members[i]]["Type"].asString();
+
+          gdcm::Tag tag(OrthancPlugins::ParseTag(dictionary, members[i]));
+
+          bool isSequence = false;
+          std::string vr = GetVRName(isSequence, dictionary, tag);
+
+          if (tag == DICOM_TAG_RETRIEVE_URL)
+          {
+            // The VR of this attribute has changed from UT to UR.
+            vr = "UR";
+          }
+          else
+          {
+            vr = GetVRName(isSequence, dictionary, tag);
+          }
+
+          visitor.Visit(tag, isSequence, vr, type, value);
+        }
+      }
+    };
+
+
+    class TagVisitorBase : public ITagVisitor
+    {
+    protected:
+      const Json::Value&  source_;
+      const gdcm::Dict&   dictionary_;
+      const std::string&  bulkUri_;
+
+    public:
+      TagVisitorBase(const Json::Value&  source,
+                     const gdcm::Dict&   dictionary,
+                     const std::string&  bulkUri) :
+        source_(source),
+        dictionary_(dictionary),
+        bulkUri_(bulkUri)
+      {
+      }
+    };
+
+
+    class JsonVisitor : public TagVisitorBase
+    {
+    private:
+      Json::Value&   target_;
+
+    public:
+      JsonVisitor(Json::Value&        target,
+                  const Json::Value&  source,
+                  const gdcm::Dict&   dictionary,
+                  const std::string&  bulkUri) :
+        TagVisitorBase(source, dictionary, bulkUri),
+        target_(target)
+      {
+        target_ = Json::objectValue;
+      }
+
+      virtual void Visit(const gdcm::Tag& tag,
+                         bool isSequence,
+                         const std::string& vr,
+                         const std::string& type,
+                         const Json::Value& value)
+      {
+        const std::string formattedTag = OrthancPlugins::FormatTag(tag);
+
+        Json::Value node = Json::objectValue;
+        node["vr"] = vr;
+
+        bool ok = false;
+        if (isSequence)
+        {
+          // Deal with sequences
+          if (type != "Sequence" ||
+              value.type() != Json::arrayValue)
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          }
+
+          node["Value"] = Json::arrayValue;
+
+          for (Json::Value::ArrayIndex i = 0; i < value.size(); i++)
+          {
+            if (value[i].type() != Json::objectValue)
+            {
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+            }
+
+            Json::Value child;
+
+            std::string childUri;
+            if (!bulkUri_.empty())
+            {
+              std::string number = boost::lexical_cast<std::string>(i);
+              childUri = bulkUri_ + formattedTag + "/" + number + "/";
+            }
+
+            JsonVisitor visitor(child, value[i], dictionary_, childUri);
+            JsonVisitor::Apply(visitor, value[i], dictionary_);
+
+            node["Value"].append(child);
+          }
+
+          ok = true;
+        }
+        else if (type == "String" &&
+                 value.type() == Json::stringValue)
+        {
+          // Deal with string representations
+          node["Value"] = Json::arrayValue;
+          node["Value"].append(value.asString());
+          ok = true;
+        }
+        else
+        {
+          // Bulk data
+          if (!bulkUri_.empty())
+          {
+            node["BulkDataURI"] = bulkUri_ + formattedTag;
+            ok = true;
+          }
+        }
+
+        if (ok)
+        {
+          target_[formattedTag] = node;
+        }
+      }
+    };
+
+
+    class XmlVisitor : public TagVisitorBase
+    {
+    private:
+      pugi::xml_node&  target_;
+
+    public:
+      XmlVisitor(pugi::xml_node&     target,
+                 const Json::Value&  source,
+                 const gdcm::Dict&   dictionary,
+                 const std::string&  bulkUri) :
+        TagVisitorBase(source, dictionary, bulkUri),
+        target_(target)
+      {
+      }
+
+      virtual void Visit(const gdcm::Tag& tag,
+                         bool isSequence,
+                         const std::string& vr,
+                         const std::string& type,
+                         const Json::Value& value)
+      {
+        const std::string formattedTag = OrthancPlugins::FormatTag(tag);
+
+        pugi::xml_node node = target_.append_child("DicomAttribute");
+        node.append_attribute("tag").set_value(formattedTag.c_str());
+        node.append_attribute("vr").set_value(vr.c_str());
+
+        const char* keyword = GetKeyword(dictionary_, tag);
+        if (keyword != NULL)
+        {
+          node.append_attribute("keyword").set_value(keyword);
+        }
+
+        if (isSequence)
+        {
+          // Deal with sequences
+          if (type != "Sequence" ||
+              value.type() != Json::arrayValue)
+          {
+            throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+          }
+
+          for (Json::Value::ArrayIndex i = 0; i < value.size(); i++)
+          {
+            if (value[i].type() != Json::objectValue)
+            {
+              throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
+            }
+
+            pugi::xml_node child = node.append_child("Item");
+            std::string number = boost::lexical_cast<std::string>(i + 1);
+            child.append_attribute("number").set_value(number.c_str());
+
+            std::string childUri;
+            if (!bulkUri_.empty())
+            {
+              childUri = bulkUri_ + formattedTag + "/" + number + "/";
+            }
+
+            XmlVisitor visitor(child, value[i], dictionary_, childUri);
+            XmlVisitor::Apply(visitor, value[i], dictionary_);
+          }
+        }
+        else if (type == "String" &&
+                 value.type() == Json::stringValue)
+        {
+          // Deal with string representations
+          pugi::xml_node item = node.append_child("Value");
+          item.append_attribute("number").set_value("1");
+          item.append_child(pugi::node_pcdata).set_value(value.asCString());
+        }
+        else
+        {
+          // Bulk data
+          if (!bulkUri_.empty())
+          {
+            pugi::xml_node value = node.append_child("BulkData");
+            std::string uri = bulkUri_ + formattedTag;
+            value.append_attribute("uri").set_value(uri.c_str());
+          }
+        }
+      }
+    };
+  }
+
+
+  static void OrthancToDicomWebXml(pugi::xml_document& target,
+                                   const Json::Value& source,
+                                   const gdcm::Dict& dictionary,
+                                   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");
+    root.append_attribute("xsi:schemaLocation").set_value("http://dicom.nema.org/PS3.19/models/NativeDICOM");
+    root.append_attribute("xmlns:xsi").set_value("http://www.w3.org/2001/XMLSchema-instance");
+
+    XmlVisitor visitor(root, source, dictionary, bulkUriRoot);
+    ITagVisitor::Apply(visitor, source, dictionary);
+
+    pugi::xml_node decl = target.prepend_child(pugi::node_declaration);
+    decl.append_attribute("version").set_value("1.0");
+    decl.append_attribute("encoding").set_value("utf-8");
+  }
+
+
+  void DicomResults::AddFromOrthanc(const Json::Value& dicom,
+                                    const std::string& wadoUrl)
+  { 
+    std::string bulkUriRoot;
+    if (isBulkAccessible_)
+    {
+      bulkUriRoot = wadoUrl + "bulk/";
+    }
+
+    if (isXml_)
+    {
+      pugi::xml_document doc;
+      OrthancToDicomWebXml(doc, dicom, dictionary_, bulkUriRoot);
+    
+      ChunkedBufferWriter writer;
+      doc.save(writer, "  ", pugi::format_default, pugi::encoding_utf8);
+
+      std::string item;
+      writer.Flatten(item);
+
+      AddInternal(item);
+    }
+    else
+    {
+      Json::Value v;
+      JsonVisitor visitor(v, dicom, dictionary_, bulkUriRoot);
+      ITagVisitor::Apply(visitor, dicom, dictionary_);
+
+      Json::FastWriter writer;
+      AddInternal(writer.write(v));
+    }
+  }
+
+
   void DicomResults::Answer()
   {
     if (isXml_)
--- a/Plugin/DicomResults.h	Thu Apr 28 09:14:06 2016 +0200
+++ b/Plugin/DicomResults.h	Thu Apr 28 17:22:02 2016 +0200
@@ -26,6 +26,7 @@
 #include <gdcmDataSet.h>
 #include <gdcmDict.h>
 #include <gdcmFile.h>
+#include <json/value.h>
 
 namespace OrthancPlugins
 {
@@ -41,6 +42,8 @@
     bool                      isXml_;
     bool                      isBulkAccessible_;
 
+    void AddInternal(const std::string& item);
+
     void AddInternal(const gdcm::File* file,
                      const gdcm::DataSet& dicom);
 
@@ -63,6 +66,9 @@
       AddInternal(&file, subset);
     }
 
+    void AddFromOrthanc(const Json::Value& dicom,
+                        const std::string& wadoUrl);
+
     void Answer();
   };
 }
--- a/Plugin/QidoRs.cpp	Thu Apr 28 09:14:06 2016 +0200
+++ b/Plugin/QidoRs.cpp	Thu Apr 28 17:22:02 2016 +0200
@@ -42,6 +42,35 @@
 
 namespace
 {
+  static std::string FormatOrthancTag(const gdcm::Tag& tag)
+  {
+    char b[16];
+    sprintf(b, "%04x,%04x", tag.GetGroup(), tag.GetElement());
+    return std::string(b);
+  }
+
+
+  static std::string GetOrthancTag(const Json::Value& source,
+                                   const gdcm::Tag& tag,
+                                   const std::string& defaultValue)
+  {
+    std::string s = FormatOrthancTag(tag);
+      
+    if (source.isMember(s) &&
+        source[s].type() == Json::objectValue &&
+        source[s].isMember("Value") &&
+        source[s].isMember("Type") &&
+        source[s]["Type"] == "String" &&
+        source[s]["Value"].type() == Json::stringValue)
+    {
+      return source[s]["Value"].asString();
+    }
+    else
+    {
+      return defaultValue;
+    }
+  }
+
 
   enum QueryLevel
   {
@@ -53,10 +82,10 @@
 
   class ModuleMatcher
   {
-  private:
+  public:
     typedef std::map<gdcm::Tag, std::string>  Filters;
 
-    const gdcm::Dict&     dictionary_;
+  private:
     bool                  fuzzy_;
     unsigned int          offset_;
     unsigned int          limit_;
@@ -65,84 +94,6 @@
     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 GetTagValue(const char* c)
-    {
-      return ((GetCharValue(c[0]) << 12) + 
-              (GetCharValue(c[1]) << 8) + 
-              (GetCharValue(c[2]) << 4) + 
-              GetCharValue(c[3]));
-    }
-
-
-    static std::string Format(const gdcm::Tag& tag)
-    {
-      char b[16];
-      sprintf(b, "%04x,%04x", tag.GetGroup(), tag.GetElement());
-      return std::string(b);
-    }
-
-
-    gdcm::Tag  ParseTag(const std::string& key) const
-    {
-      if (key.find('.') != std::string::npos)
-      {
-        std::string s = "This DICOMweb plugin does not support hierarchical queries: " + key;
-        OrthancPluginLogError(context_, s.c_str());
-        throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-      }
-
-      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]))        
-      {
-        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.find('.') != std::string::npos)
-          {
-            std::string s = "This QIDO-RS implementation does not support search over sequences: " + key;
-            OrthancPluginLogError(context_, s.c_str());
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_NotImplemented);
-          }
-          else
-          {
-            std::string s = "Illegal tag name in QIDO-RS: " + key;
-            OrthancPluginLogError(context_, s.c_str());
-            throw Orthanc::OrthancException(Orthanc::ErrorCode_UnknownDicomTag);
-          }
-        }
-
-        return tag;
-      }
-    }
-
-
     static void AddResultAttributesForLevel(std::list<gdcm::Tag>& result,
                                             QueryLevel level)
     {
@@ -155,7 +106,7 @@
           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, 0x0061));  // Modalities in Study  => SPECIAL CASE
           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
@@ -165,20 +116,20 @@
           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
+          //result.push_back(gdcm::Tag(0x0020, 0x1206));  // Number of Study Related Series  => SPECIAL CASE
+          //result.push_back(gdcm::Tag(0x0020, 0x1208));  // Number of Study Related Instances  => SPECIAL CASE
           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, 0x0060));  // 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(0x0020, 0x1209));  // Number of Series Related Instances  => SPECIAL CASE
           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
@@ -205,10 +156,8 @@
     }
 
 
-
   public:
     ModuleMatcher(const OrthancPluginHttpRequest* request) :
-    dictionary_(gdcm::Global::GetInstance().GetDicts().GetPublicDict()),
     fuzzy_(false),
     offset_(0),
     limit_(0),
@@ -257,13 +206,13 @@
             Orthanc::Toolbox::TokenizeString(tags, value, ',');
             for (size_t i = 0; i < tags.size(); i++)
             {
-              includeFields_.push_back(ParseTag(tags[i]));
+              includeFields_.push_back(OrthancPlugins::ParseTag(*dictionary_, tags[i]));
             }
           }
         }
         else
         {
-          filters_[ParseTag(key)] = value;
+          filters_[OrthancPlugins::ParseTag(*dictionary_, key)] = value;
         }
       }
     }
@@ -323,11 +272,88 @@
       for (Filters::const_iterator it = filters_.begin(); 
            it != filters_.end(); ++it)
       {
-        result["Query"][Format(it->first)] = it->second;
+        result["Query"][FormatOrthancTag(it->first)] = it->second;
       }
     }
 
 
+    void ComputeDerivedTags(Filters& target,
+                            QueryLevel level,
+                            const std::string& resource) const
+    {
+      target.clear();
+
+      switch (level)
+      {
+        case QueryLevel_Study:
+        {
+          Json::Value series, instances;
+          if (OrthancPlugins::RestApiGetJson(series, context_, "/studies/" + resource + "/series?expand") &&
+              OrthancPlugins::RestApiGetJson(instances, context_, "/studies/" + resource + "/instances"))
+          {
+            // Number of Study Related Series
+            target[gdcm::Tag(0x0020, 0x1206)] = boost::lexical_cast<std::string>(series.size());
+
+            // Number of Study Related Instances
+            target[gdcm::Tag(0x0020, 0x1208)] = boost::lexical_cast<std::string>(instances.size());
+
+            // Collect the Modality of all the child series
+            std::set<std::string> modalities;
+            for (Json::Value::ArrayIndex i = 0; i < series.size(); i++)
+            {
+              if (series[i].isMember("MainDicomTags") &&
+                  series[i]["MainDicomTags"].isMember("Modality"))
+              {
+                modalities.insert(series[i]["MainDicomTags"]["Modality"].asString());
+              }
+            }
+
+            std::string s;
+            for (std::set<std::string>::const_iterator 
+                   it = modalities.begin(); it != modalities.end(); ++it)
+            {
+              if (!s.empty())
+              {
+                s += "\\";
+              }
+
+              s += *it;
+            }
+
+            target[gdcm::Tag(0x0008, 0x0061)] = s;  // Modalities in Study
+          }
+          else
+          {
+            target[gdcm::Tag(0x0008, 0x0061)] = "";   // Modalities in Study
+            target[gdcm::Tag(0x0020, 0x1206)] = "0";  // Number of Study Related Series
+            target[gdcm::Tag(0x0020, 0x1208)] = "0";  // Number of Study Related Instances
+          }
+
+          break;
+        }
+
+        case QueryLevel_Series:
+        {
+          Json::Value instances;
+          if (OrthancPlugins::RestApiGetJson(instances, context_, "/series/" + resource + "/instances"))
+          {
+            // Number of Series Related Instances
+            target[gdcm::Tag(0x0020, 0x1209)] = boost::lexical_cast<std::string>(instances.size());
+          }
+          else
+          {
+            target[gdcm::Tag(0x0020, 0x1209)] = "0";  // Number of Series Related Instances
+          }
+
+          break;
+        }
+
+        default:
+          break;
+      }
+    }                              
+
+
     void ExtractFields(gdcm::DataSet& result,
                        const OrthancPlugins::ParsedDicomFile& dicom,
                        const std::string& wadoBase,
@@ -365,7 +391,7 @@
 
       // Copy all the required fields to the target
       for (std::list<gdcm::Tag>::const_iterator
-             it = fields.begin(); it != fields.end(); it++)
+             it = fields.begin(); it != fields.end(); ++it)
       {
         if (dicom.GetDataSet().FindDataElement(*it))
         {
@@ -392,6 +418,76 @@
       element.SetByteValue(url.c_str(), url.size());
       result.Replace(element);
     }
+
+
+    void ExtractFields(Json::Value& result,
+                       const Json::Value& source,
+                       const std::string& wadoBase,
+                       QueryLevel level) const
+    {
+      result = Json::objectValue;
+      std::list<gdcm::Tag> fields = includeFields_;
+
+      // The list of attributes for this query level
+      AddResultAttributesForLevel(fields, level);
+
+      // All other attributes passed as query keys
+      for (Filters::const_iterator it = filters_.begin();
+           it != filters_.end(); ++it)
+      {
+        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);
+      }
+
+      // 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);
+      }
+
+      // Copy all the required fields to the target
+      for (std::list<gdcm::Tag>::const_iterator
+             it = fields.begin(); it != fields.end(); ++it)
+      {
+        std::string tag = FormatOrthancTag(*it);
+        if (source.isMember(tag))
+        {
+          result[tag] = source[tag];
+        }
+      }
+
+      // Set the retrieve URL for WADO-RS
+      std::string url = (wadoBase + "studies/" + 
+                         GetOrthancTag(source, OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID, ""));
+
+      if (level == QueryLevel_Series || level == QueryLevel_Instance)
+      {
+        url += "/series/" + GetOrthancTag(source, OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID, "");
+      }
+
+      if (level == QueryLevel_Instance)
+      {
+        url += "/instances/" + GetOrthancTag(source, OrthancPlugins::DICOM_TAG_SOP_INSTANCE_UID, "");
+      }
+    
+      Json::Value tmp = Json::objectValue;
+      tmp["Name"] = "RetrieveURL";
+      tmp["Type"] = "String";
+      tmp["Value"] = url;
+
+      result[FormatOrthancTag(OrthancPlugins::DICOM_TAG_RETRIEVE_URL)] = tmp;
+    }
   };
 }
 
@@ -415,26 +511,30 @@
     throw Orthanc::OrthancException(Orthanc::ErrorCode_InternalError);
   }
 
-  std::list<std::string> instances;
+  typedef std::list< std::pair<std::string, std::string> > ResourcesAndInstances;
+
+  ResourcesAndInstances resourcesAndInstances;
   std::string root = (level == QueryLevel_Study ? "/studies/" : "/series/");
     
   for (Json::Value::ArrayIndex i = 0; i < resources.size(); i++)
   {
+    const std::string resource = resources[i].asString();
+
     if (level == QueryLevel_Study ||
         level == QueryLevel_Series)
     {
       // Find one child instance of this resource
       Json::Value tmp;
-      if (OrthancPlugins::RestApiGetJson(tmp, context_, root + resources[i].asString() + "/instances") &&
+      if (OrthancPlugins::RestApiGetJson(tmp, context_, root + resource + "/instances") &&
           tmp.type() == Json::arrayValue &&
           tmp.size() > 0)
       {
-        instances.push_back(tmp[0]["ID"].asString());
+        resourcesAndInstances.push_back(std::make_pair(resource, tmp[0]["ID"].asString()));
       }
     }
     else
     {
-      instances.push_back(resources[i].asString());
+      resourcesAndInstances.push_back(std::make_pair(resource, resource));
     }
   }
   
@@ -442,20 +542,77 @@
 
   OrthancPlugins::DicomResults results(context_, output, wadoBase, *dictionary_, IsXmlExpected(request), true);
 
-  for (std::list<std::string>::const_iterator
-         it = instances.begin(); it != instances.end(); it++)
+#if 0
+  // Implementation up to version 0.2 of the plugin. Each instance is
+  // downloaded and decoded using GDCM, which slows down things
+  // wrt. the new implementation below that directly uses the Orthanc
+  // pre-computed JSON summary.
+  for (ResourcesAndInstances::const_iterator
+         it = resourcesAndInstances.begin(); it != resourcesAndInstances.end(); ++it)
   {
+    ModuleMatcher::Filters derivedTags;
+    matcher.ComputeDerivedTags(derivedTags, level, it->first);
+
     std::string file;
-    if (OrthancPlugins::RestApiGetString(file, context_, "/instances/" + *it + "/file"))
+    if (OrthancPlugins::RestApiGetString(file, context_, "/instances/" + it->second + "/file"))
     {
       OrthancPlugins::ParsedDicomFile dicom(file);
 
       std::auto_ptr<gdcm::DataSet> result(new gdcm::DataSet);
       matcher.ExtractFields(*result, dicom, wadoBase, level);
+
+      // Inject the derived tags
+      ModuleMatcher::Filters derivedTags;
+      matcher.ComputeDerivedTags(derivedTags, level, it->first);
+
+      for (ModuleMatcher::Filters::const_iterator
+             tag = derivedTags.begin(); tag != derivedTags.end(); ++tag)
+      {
+        gdcm::DataElement element(tag->first);
+        element.SetByteValue(tag->second.c_str(), tag->second.size());
+        result->Replace(element);
+      }
+
       results.Add(dicom.GetFile(), *result);
     }
   }
 
+#else
+  // Fix of issue #13
+  for (ResourcesAndInstances::const_iterator
+         it = resourcesAndInstances.begin(); it != resourcesAndInstances.end(); ++it)
+  {
+    Json::Value tags;
+    if (OrthancPlugins::RestApiGetJson(tags, context_, "/instances/" + it->second + "/tags"))
+    {
+      std::string wadoUrl = OrthancPlugins::Configuration::GetWadoUrl(
+        wadoBase, 
+        GetOrthancTag(tags, OrthancPlugins::DICOM_TAG_STUDY_INSTANCE_UID, ""),
+        GetOrthancTag(tags, OrthancPlugins::DICOM_TAG_SERIES_INSTANCE_UID, ""),
+        GetOrthancTag(tags, OrthancPlugins::DICOM_TAG_SOP_INSTANCE_UID, ""));
+
+      Json::Value result;
+      matcher.ExtractFields(result, tags, wadoBase, level);
+
+      // Inject the derived tags
+      ModuleMatcher::Filters derivedTags;
+      matcher.ComputeDerivedTags(derivedTags, level, it->first);
+
+      for (ModuleMatcher::Filters::const_iterator
+             tag = derivedTags.begin(); tag != derivedTags.end(); ++tag)
+      {
+        Json::Value tmp = Json::objectValue;
+        tmp["Name"] = OrthancPlugins::GetKeyword(*dictionary_, tag->first);
+        tmp["Type"] = "String";
+        tmp["Value"] = tag->second;
+        result[FormatOrthancTag(tag->first)] = tmp;
+      }
+
+      results.AddFromOrthanc(result, wadoUrl);
+    }
+  }
+#endif
+
   results.Answer();
 }
 
--- a/UnitTestsSources/UnitTestsMain.cpp	Thu Apr 28 09:14:06 2016 +0200
+++ b/UnitTestsSources/UnitTestsMain.cpp	Thu Apr 28 17:22:02 2016 +0200
@@ -27,6 +27,7 @@
 using namespace OrthancPlugins;
 
 Json::Value configuration_ = Json::objectValue;
+OrthancPluginContext* context_ = NULL;
 
 
 TEST(ContentType, Parse)